def __init__(self, text=None): _QrCodeTextEdit.__init__(self, text) self.setReadOnly(1) self.qr_button = self.addButton(self.get_qr_icon(), self.qr_show, _("Show as QR code")) run_hook('show_text_edit', self)
def __init__(self, text="", allow_multi=False): _QrCodeTextEdit.__init__(self, text) self.allow_multi = allow_multi self.setReadOnly(0) self.qr_button = self.addButton(self.get_qr_icon(), self.qr_input, _("Read QR code")) self.addButton(":icons/file.png", self.file_input, _("Read text or image file")) run_hook('scan_text_edit', self)
def create_menu(self, position): item = self.currentItem() if not item: return column = self.currentColumn() tx_hash = item.data(0, Qt.UserRole) if not tx_hash: return if column == 0: column_title = "ID" column_data = tx_hash else: column_title = self.headerItem().text(column) column_data = item.text(column) tx_URL = web.BE_URL(self.config, 'tx', tx_hash) height, conf, timestamp = self.wallet.get_tx_height(tx_hash) tx = self.wallet.transactions.get(tx_hash) if not tx: return # this happens sometimes on wallet synch when first starting up. is_unconfirmed = height <= 0 pr_key = self.wallet.invoices.paid.get(tx_hash) menu = QMenu() menu.addAction( _("&Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data.strip())) if column in self.editable_columns: # We grab a fresh reference to the current item, as it has been deleted in a reported issue. menu.addAction( _("&Edit {}").format(column_title), lambda: self.currentItem() and self.editItem( self.currentItem(), column)) label = self.wallet.get_label(tx_hash) or None menu.addAction(_("&Details"), lambda: self.parent.show_transaction(tx, label)) if is_unconfirmed and tx: child_tx = self.wallet.cpfp(tx, 0) if child_tx: menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp(tx, child_tx)) if pr_key: menu.addAction(self.invoiceIcon, _("View invoice"), lambda: self.parent.show_invoice(pr_key)) if tx_URL: menu.addAction(_("View on block explorer"), lambda: webopen(tx_URL)) run_hook("history_list_context_menu_setup", self, menu, item, tx_hash) # Plugins can modify menu menu.exec_(self.viewport().mapToGlobal(position))
def install_plugin_confirmed(self, plugin_archive_path): plugin_manager = self.main_window.gui_object.plugins result_code = plugin_manager.install_external_plugin( plugin_archive_path) if result_code != ExternalPluginCodes.SUCCESS: self.show_error( INSTALL_ERROR_MESSAGES.get( result_code, _("Unexpected error %d") % result_code)) else: run_hook('init_qt', self.main_window.gui_object) self.refresh_ui()
def on_toggle_plugin(self): selected_key = self.pluginsList.get_selected_key() if selected_key is not None: package_name = selected_key plugin_manager = self.main_window.gui_object.plugins plugin = plugin_manager.external_plugins.get(package_name, None) if plugin is not None and plugin.is_enabled(): plugin_manager.disable_external_plugin(package_name) else: plugin_manager.enable_external_plugin(package_name) run_hook('init_qt', self.main_window.gui_object) self.refresh_ui()
def __init__(self, parent=None, msg=None): msg = msg or _('Please enter your password') WindowModalDialog.__init__(self, parent, _("Enter Password")) self.pw = pw = QLineEdit() pw.setEchoMode(2) vbox = QVBoxLayout() vbox.addWidget(QLabel(msg)) grid = QGridLayout() grid.setSpacing(8) grid.addWidget(QLabel(_('Password')), 1, 0) grid.addWidget(pw, 1, 1) vbox.addLayout(grid) vbox.addLayout(Buttons(CancelButton(self), OkButton(self))) self.setLayout(vbox) run_hook('password_dialog', pw, grid, 1)
def new_cash_account_contact_dialog(self): ''' Context menu callback. Shows the "New Cash Account Contact" interface. ''' items = cashacctqt.lookup_cash_account_dialog( self.parent, self.wallet, title=_("New Cash Account Contact"), blurb=_("<br>Add anyone's Cash Account to your Contacts"), button_type=cashacctqt.InfoGroupBox.ButtonType.Radio) if items: info, min_chash, name = items[0] self.parent.set_contact(name, info.address.to_ui_string(), typ='cashacct') run_hook('update_contacts_tab', self)
def item_changed(self, item, column): # Run the label of the changed item thru the filter hook if column != 3: return label = item.text(3) # NB: 'h_item' parameter is None due to performance reasons should_skip = run_hook( "history_list_filter", self, None, label, multi=True) or [] if any(should_skip): item.setHidden(True)
def create_menu(self, position): item = self.itemAt(position) if not item: return self.setCurrentItem(item) # sometimes it's not the current item. addr = item.data(0, Qt.UserRole) req = self.wallet.receive_requests[addr] column = self.currentColumn() column_title = self.headerItem().text(column) column_data = item.text(column) menu = QMenu(self) menu.addAction( _("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data.strip())) menu.addAction( _("Copy URI"), lambda: self.parent.view_and_paste( 'URI', '', self.parent.get_request_URI(addr))) menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr)) menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr)) run_hook('receive_list_menu', menu, addr) menu.exec_(self.viewport().mapToGlobal(position))
def on_update(self): if self.cleaned_up: return item = self.currentItem() current_contact = item.data(0, self.DataRoles.Contact) if item else None selected = self.selectedItems() or [] selected_contacts = set( item.data(0, self.DataRoles.Contact) for item in selected) del item, selected # must not hold a reference to a C++ object that will soon be deleted in self.clear().. self.clear() type_names = defaultdict(lambda: _("Unknown")) type_names.update({ 'openalias': _('OpenAlias'), 'cashacct': _('Cash Account'), 'cashacct_W': _('Cash Account') + ' [' + _('Mine') + ']', 'cashacct_T': _('Cash Account') + ' [' + _('Pend') + ']', 'address': _('Address'), }) type_icons = { 'openalias': self.icon_openalias, 'cashacct': self.icon_cashacct, 'cashacct_W': self.icon_cashacct, 'cashacct_T': self.icon_unverif, 'address': self.icon_contacts, } selected_items, current_item = [], None edited = self._edited_item_cur_sel for contact in self.get_full_contacts( include_pseudo=self.show_my_cashaccts): _type, name, address = contact.type, contact.name, contact.address label_key = address if _type in ('cashacct', 'cashacct_W', 'cashacct_T', 'address'): try: # try and re-parse and re-display the address based on current UI string settings addy = Address.from_string(address) address = addy.to_ui_string() label_key = addy.to_storage_string() del addy except: ''' This may happen because we may not have always enforced this as strictly as we could have in legacy code. Just move on.. ''' label = self.wallet.get_label(label_key) item = QTreeWidgetItem( ["", name, label, address, type_names[_type]]) item.setData(0, self.DataRoles.Contact, contact) item.DataRole = self.DataRoles.Contact if _type in ('cashacct', 'cashacct_W', 'cashacct_T'): ca_info = self.wallet.cashacct.get_verified(name) tt_warn = None if ca_info: if self.wallet.is_mine( ca_info.address) and not self.show_my_cashaccts: # user may have added the contact to "self" manually # but since they asked to not see their own cashaccts, # we must do this to suppress it from being shown regardless continue item.setText(0, ca_info.emoji) tt = _( 'Validated Cash Account: <b><pre>{emoji} {account_string}</pre></b>' ).format( emoji=ca_info.emoji, account_string= f'{ca_info.name}#{ca_info.number}.{ca_info.collision_hash};' ) else: item.setIcon(0, self.icon_unverif) if _type == 'cashacct_T': tt_warn = tt = _( 'Cash Account pending confirmation and/or verification' ) else: tt_warn = tt = _( 'Warning: This Cash Account is not verified') item.setToolTip(0, tt) if tt_warn: item.setToolTip(1, tt_warn) if _type in type_icons: item.setIcon(4, type_icons[_type]) # always give the "Address" field a monospace font even if it's # not strictly an address such as openalias... item.setFont(3, self.monospace_font) self.addTopLevelItem(item) if contact == current_contact or (contact == edited[0] and edited[1]): current_item = item # this key was the current item before and it hasn't gone away if contact in selected_contacts or (contact == edited[0] and edited[2]): selected_items.append( item ) # this key was selected before and it hasn't gone away if selected_items: # sometimes currentItem is set even if nothing actually selected. grr.. # restore current item & selections if current_item: # set the current item. this may also implicitly select it self.setCurrentItem(current_item) for item in selected_items: # restore the previous selection item.setSelected(True) self._edited_item_cur_sel = (None, ) * 3 run_hook('update_contacts_tab', self)
def __init__(self, tx, parent, desc, prompt_if_unsaved): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' # We want to be a top-level window QDialog.__init__(self, parent=None) # Take a copy; it might get updated in the main window by # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. self.tx = copy.deepcopy(tx) self.tx.deserialize() self.main_window = parent self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved self.saved = False self.desc = desc self.cashaddr_signal_slots = [] self._dl_pct = None self._closed = False self.tx_hash = self.tx.txid_fast() if self.tx.raw and self.tx.is_complete() else None self.tx_height = self.wallet.get_tx_height(self.tx_hash)[0] or None self.block_hash = None Weak.finalization_print_error(self) # track object lifecycle self.setMinimumWidth(750) self.setWindowTitle(_("Transaction")) vbox = QVBoxLayout() self.setLayout(vbox) self.tx_hash_e = ButtonsLineEdit() l = QLabel(_("&Transaction ID:")) l.setBuddy(self.tx_hash_e) vbox.addWidget(l) self.tx_hash_e.addCopyButton() weakSelfRef = Weak.ref(self) qr_show = lambda: weakSelfRef() and weakSelfRef().main_window.show_qrcode(str(weakSelfRef().tx_hash_e.text()), _("Transaction ID"), parent=weakSelfRef()) icon = ":icons/qrcode_white.svg" if ColorScheme.dark_scheme else ":icons/qrcode.svg" self.tx_hash_e.addButton(icon, qr_show, _("Show as QR code")) self.tx_hash_e.setReadOnly(True) vbox.addWidget(self.tx_hash_e) self.tx_desc = QLabel() vbox.addWidget(self.tx_desc) self.status_label = QLabel() vbox.addWidget(self.status_label) self.date_label = QLabel() vbox.addWidget(self.date_label) self.amount_label = QLabel() vbox.addWidget(self.amount_label) self.size_label = QLabel() vbox.addWidget(self.size_label) self.fee_label = QLabel() vbox.addWidget(self.fee_label) for l in (self.tx_desc, self.status_label, self.date_label, self.amount_label, self.size_label, self.fee_label): # make these labels selectable by mouse in case user wants to copy-paste things in tx dialog l.setTextInteractionFlags(l.textInteractionFlags() | Qt.TextSelectableByMouse) def open_be_url(link): if link: try: kind, thing = link.split(':') url = web.BE_URL(self.main_window.config, kind, thing) except: url = None if url: webopen( url ) else: self.show_error(_('Unable to open in block explorer. Please be sure your block explorer is configured correctly in preferences.')) self.status_label.linkActivated.connect(open_be_url) self.add_io(vbox) self.freeze_button = b = QPushButton(self._make_freeze_button_text()) b.setToolTip(_("Lock/unlock the coin(s) being spent in this transaction.\n\n" "Use this facility if you wish to broadcast this transaction later,\n" "in order to prevent its inputs from being accidentally spent.")) b.clicked.connect(self.do_freeze_unfreeze) self.sign_button = b = QPushButton(_("&Sign")) b.clicked.connect(self.sign) b.setToolTip(_("Sign the transaction")) self.broadcast_button = b = QPushButton(_("&Broadcast")) b.clicked.connect(self.do_broadcast) b.setToolTip(_("Submit the transaction to the blockchain")) self.last_broadcast_time = 0 self.save_button = b = QPushButton(_("S&ave")) b.setToolTip(_("Save the transaction to a file")) b.clicked.connect(self.save) self.cancel_button = b = CloseButton(self) self.qr_button = b = QPushButton() b.setToolTip(_("Show transaction QR code")) b.setIcon(QIcon(icon)) b.clicked.connect(self.show_qr) b.setShortcut(QKeySequence(Qt.ALT + Qt.Key_Q)) self.copy_button = b = CopyButton(lambda: str(weakSelfRef() and weakSelfRef().tx), callback=lambda: weakSelfRef() and weakSelfRef().show_message( _("Transaction raw hex copied to clipboard."))) b.setToolTip(_("Copy transaction raw hex to the clipboard")) # Action buttons self.buttons = [self.freeze_button, self.sign_button, self.broadcast_button, self.cancel_button] # Transaction sharing buttons self.sharing_buttons = [self.copy_button, self.qr_button, self.save_button] run_hook('transaction_dialog', self) hbox = QHBoxLayout() hbox.addLayout(Buttons(*self.sharing_buttons)) hbox.addStretch(1) hbox.addLayout(Buttons(*self.buttons)) vbox.addLayout(hbox) if self.tx_height: # this avoids downloading the block_height info if we already have it. self.tx.ephemeral['block_height'] = self.tx_height self.throttled_update_sig.connect(self.throttled_update, Qt.QueuedConnection) self.initiate_fetch_input_data(True) self.update() # connect slots so we update in realtime as blocks come in, etc parent.history_updated_signal.connect(self.update_tx_if_in_wallet) parent.labels_updated_signal.connect(self.update_tx_if_in_wallet) parent.network_signal.connect(self.got_verified_tx)
def update(self): if self._closed: # latent timer fire return desc = self.desc base_unit = self.main_window.base_unit() format_amount = self.main_window.format_amount delta2, info2 = self.wallet.get_tx_extended_info(self.tx) spends_coins_mine = delta2.spends_coins_mine tx_hash, status, label, can_broadcast, amount, fee, height, conf, timestamp, exp_n, status_enum = info2 self.tx_height = height or self.tx.ephemeral.get('block_height') or None self.tx_hash = tx_hash desc = label or desc size = self.tx.estimated_size() # Update freeze/unfreeze button depending on tx state StatusEnum = self.wallet.StatusEnum if spends_coins_mine: has_frozen = bool(self.wallet.is_frozen_coin(set(spends_coins_mine))) self.freeze_button._coins = spends_coins_mine self.freeze_button._op = op = self.FreezeOp.Freeze if not has_frozen else self.FreezeOp.Unfreeze # Set the proper text (plural / singular form) self.freeze_button.setText(self._make_freeze_button_text(op, len(spends_coins_mine))) # Freeze/Unfreeze enabled only for signed transactions or transactions with frozen coins self.freeze_button.setEnabled(has_frozen or status_enum in (StatusEnum.Signed, StatusEnum.PartiallySigned)) else: self.freeze_button.setEnabled(False) self.freeze_button.setText(self._make_freeze_button_text()) # We enable the broadcast button IFF both of the following hold: # 1. can_broadcast is true (tx has not been seen yet on the network # and is_complete). # 2. The last time user hit "Broadcast" (and it was successful) was # more than BROADCAST_COOLDOWN_SECS ago. This second condition # implements a broadcast cooldown timer which immediately disables # the "Broadcast" button for a time after a successful broadcast. # This prevents the user from being able to spam the broadcast # button. See #1483. self.broadcast_button.setEnabled(can_broadcast and time.time() - self.last_broadcast_time >= self.BROADCAST_COOLDOWN_SECS) can_sign = not self.tx.is_complete() and \ (self.wallet.can_sign(self.tx) or bool(self.main_window.tx_external_keypairs)) self.sign_button.setEnabled(can_sign) self.tx_hash_e.setText(tx_hash or _('Unknown')) if fee is None: fee = self.try_calculate_fee() if fee is None: # see if we can grab the fee from the wallet internal cache which # sometimes has fees for tx's not entirely 'is_mine' if self.wallet and self.tx_hash: fee = self.wallet.tx_fees.get(self.tx_hash) if desc is None: self.tx_desc.hide() else: self.tx_desc.setText(_("Description") + ': ' + desc) self.tx_desc.show() if self.tx_height is not None and self.tx_height > 0 and tx_hash: status_extra = ' ( ' + _("Mined in block") + f': <a href="tx:{tx_hash}">{self.tx_height}</a>' + ' )' else: status_extra = '' self.status_label.setText(_('Status:') + ' ' + status + status_extra) if timestamp: time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] self.date_label.setText(_("Date: {}").format(time_str)) self.date_label.show() elif exp_n: text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)') self.date_label.setText(_('Expected confirmation time') + ': ' + text) self.date_label.show() else: self.date_label.hide() if amount is None: amount_str = _("Transaction unrelated to your wallet") elif amount > 0: amount_str = _("Amount received:") + ' %s'% format_amount(amount) + ' ' + base_unit else: amount_str = _("Amount sent:") + ' %s'% format_amount(-amount) + ' ' + base_unit size_str = _("Size: {size} bytes").format(size=size) fee_str = _("Fee") + ": " if fee is not None: fee_str = _("Fee: {fee_amount} {fee_unit} ( {fee_rate} )") fee_str = fee_str.format(fee_amount=format_amount(fee), fee_unit=base_unit, fee_rate=self.main_window.format_fee_rate(fee/size*1000)) dusty_fee = self.tx.ephemeral.get('dust_to_fee', 0) if dusty_fee: fee_str += ' <font color=#999999>' + (_("( %s in dust was added to fee )") % format_amount(dusty_fee)) + '</font>' elif self._dl_pct is not None: fee_str = _('Downloading input data, please wait...') + ' {:.0f}%'.format(self._dl_pct) else: fee_str += _("unknown") self.amount_label.setText(amount_str) self.fee_label.setText(fee_str) self.size_label.setText(size_str) self.update_io() run_hook('transaction_dialog_update', self)
def doSend_(self, preview : bool) -> None: #if run_hook('abort_send', self): # return r = read_send_form(self) if not r: return outputs, fee, tx_desc, coins = r try: tx = wallet().make_unsigned_transaction(coins, outputs, config(), fee, sign_schnorr=parent().prefs_use_schnorr) except NotEnoughFunds: parent().show_error(_("Insufficient funds")) return except ExcessiveFee: parent().show_error(_("Your fee is too high. Max is 50 sat/byte.")) return except BaseException as e: traceback.print_exc(file=sys.stdout) parent().show_error(str(e)) return amount = tx.output_value() if self.isMax else sum(map(lambda x:x[2], outputs)) fee = tx.get_fee() #if fee < self.wallet.relayfee() * tx.estimated_size() / 1000 and tx.requires_fee(self.wallet): #parent().show_error(_("This transaction requires a higher fee, or it will not be propagated by the network")) #return if preview: self.showTransaction_desc_(tx.serialize(), tx_desc) return # confirmation dialog msg = [ _("Amount to be sent") + ": " + parent().format_amount_and_units(amount), _("Mining fee") + ": " + parent().format_amount_and_units(fee), ] x_fee = run_hook('get_tx_extra_fee', wallet(), tx) if x_fee: x_fee_address, x_fee_amount = x_fee msg.append( _("Additional fees") + ": " + parent().format_amount_and_units(x_fee_amount) ) confirm_rate = 2 * config().max_fee_rate() def DoSign(password : str) -> None: def sign_done(success) -> None: if success: if not tx.is_complete(): self.showTransaction_desc_(tx.serialize(), tx_desc) self.clear() else: parent().broadcast_transaction(tx, tx_desc) #else: # parent().show_error(_("An Unknown Error Occurred")) parent().sign_tx_with_password(tx, sign_done, password) # IN THE FUTURE IF WE WANT TO APPEND SOMETHING IN THE MSG ABOUT THE FEE, CODE IS COMMENTED OUT: #if fee > confirm_rate * tx.estimated_size() / 1000: # msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")) password = None if wallet().has_password(): parent().prompt_password_if_needed_asynch(callBack = DoSign, prompt = '\n'.join(msg), vc = self) else: msg.append(_('Proceed?')) parent().question(message = '\n'.join(msg), title = _("Confirm Send"), onOk = lambda: DoSign(None), vc = self)
def create_menu(self, position): menu = QMenu() selected = self.selectedItems() i2c = self._i2c ca_unverified = self._get_ca_unverified(include_temp=False) if selected: names = [item.text(1) for item in selected] keys = [i2c(item) for item in selected] payable_keys = [k for k in keys if k.type != 'cashacct_T'] deletable_keys = [k for k in keys if k.type in contact_types] needs_verif_keys = [k for k in keys if k in ca_unverified] column = self.currentColumn() column_title = self.headerItem().text(column) column_data = '\n'.join([item.text(column) for item in selected]) item = self.currentItem() typ = i2c(item).type if item else 'unknown' ca_info = None if item and typ in ('cashacct', 'cashacct_W'): ca_info = self.wallet.cashacct.get_verified(i2c(item).name) if column == 1 and len(selected) == 1: # hack .. for Cash Accounts just say "Copy Cash Account" column_title = _('Cash Account') if ca_info: column_data = self.wallet.cashacct.fmt_info(ca_info, emoji=True) if len(selected) > 1: column_title += f" ({len(selected)})" menu.addAction( _("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) if item and column in self.editable_columns and self.on_permit_edit( item, column): key = item.data(0, self.DataRoles.Contact) # this key & find_item business is so we don't hold a reference # to the ephemeral item, which may be deleted while the # context menu is up. Accessing the item after on_update runs # means the item is deleted and you get a C++ object deleted # runtime error. menu.addAction( _("Edit {}").format(column_title), lambda: self._on_edit_item(key, column)) a = menu.addAction( _("Pay to"), lambda: self.parent.payto_contacts(payable_keys)) if needs_verif_keys or not payable_keys: a.setDisabled(True) a = menu.addAction( _("Delete"), lambda: self.parent.delete_contacts(deletable_keys)) if not deletable_keys: a.setDisabled(True) # Add sign/verify and encrypt/decrypt menu - but only if just 1 thing selected if len(keys) == 1 and Address.is_valid(keys[0].address): signAddr = Address.from_string(keys[0].address) a = menu.addAction( _("Sign/verify message") + "...", lambda: self.parent.sign_verify_message(signAddr)) if signAddr.kind != Address.ADDR_P2PKH: a.setDisabled( True ) # We only allow this for P2PKH since it makes no sense for P2SH (ambiguous public key) URLs = [ web.BE_URL(self.config, 'addr', Address.from_string(key.address)) for key in keys if Address.is_valid(key.address) ] a = menu.addAction(_("View on block explorer"), lambda: [URL and webopen(URL) for URL in URLs]) if not any(URLs): a.setDisabled(True) if ca_info: menu.addAction( _("View registration tx..."), lambda: self.parent.do_process_from_txid( txid=ca_info.txid, tx_desc=self.wallet.get_label(ca_info.txid))) if typ in ('cashacct_W', 'cashacct'): _contact_d = i2c(item) menu.addAction( _("Details..."), lambda: cashacctqt.cash_account_detail_dialog( self.parent, _contact_d.name)) menu.addSeparator() menu.addAction(self.icon_cashacct, _("Add Contact") + " - " + _("Cash Account"), self.new_cash_account_contact_dialog) menu.addAction(self.icon_contacts, _("Add Contact") + " - " + _("Address"), self.parent.new_contact_dialog) menu.addSeparator() menu.addAction(self.icon_cashacct, _("Register Cash Account..."), self.parent.register_new_cash_account) menu.addSeparator() menu.addAction( QIcon(":icons/import.svg" if not ColorScheme.dark_scheme else ":icons/import_dark_theme.svg"), _("Import file"), self.import_contacts) if not self.parent.contacts.empty: menu.addAction( QIcon(":icons/save.svg" if not ColorScheme.dark_scheme else ":icons/save_dark_theme.svg"), _("Export file"), self.export_contacts) menu.addSeparator() a = menu.addAction(_("Show My Cash Accounts"), self.toggle_show_my_cashaccts) a.setCheckable(True) a.setChecked(self.show_my_cashaccts) if ca_unverified: def kick_off_verify(): bnums = set() for contact in ca_unverified: tup = self.wallet.cashacct.parse_string(contact.name) if not tup: continue bnums.add(tup[1]) # number ret = cashacctqt.verify_multiple_blocks( bnums, self.parent, self.wallet) if ret is None: # user cancel return verified = ca_unverified - self._get_ca_unverified() if not verified: self.parent.show_error( _("Cash Account verification failure")) menu.addSeparator() num = len(ca_unverified) a = menu.addAction( self.icon_unverif, ngettext("Verify {count} Cash Account", "Verify {count} Cash Accounts", num).format(count=num), kick_off_verify) if not self.wallet.network: a.setDisabled(True) run_hook('create_contact_menu', menu, selected) menu.exec_(self.viewport().mapToGlobal(position))
def on_update(self): self.wallet = self.parent.wallet h = self.wallet.get_history(self.get_domain(), reverse=True) sels = self.selectedItems() current_tx = sels[0].data(0, Qt.UserRole) if sels else None del sels # make sure not to hold stale ref to C++ list of items which will be deleted in clear() call below self.clear() self.has_unknown_balances = False fx = self.parent.fx if fx: fx.history_used_spot = False for h_item in h: tx_hash, height, conf, timestamp, value, balance = h_item label = self.wallet.get_label(tx_hash) should_skip = run_hook( "history_list_filter", self, h_item, label, multi=True) or [] if any(should_skip): # For implementation of fast plugin filters (such as CashShuffle # shuffle tx filtering), we short-circuit return. This is # faster than using the MyTreeWidget filter definted in .util continue if value is None or balance is None: # Workaround to the fact that sometimes the wallet doesn't # know the actual balance for history items while it's # downloading history, and we want to flag that situation # and redraw the GUI sometime later when it finishes updating. # This flag is checked in main_window.py, TxUpadteMgr class. self.has_unknown_balances = True status, status_str = self.wallet.get_tx_status( tx_hash, height, conf, timestamp) has_invoice = self.wallet.invoices.paid.get(tx_hash) icon = self._get_icon_for_status(status) v_str = self.parent.format_amount(value, True, whitespaces=True) balance_str = self.parent.format_amount(balance, whitespaces=True) entry = ['', tx_hash, 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 [value, balance]: text = fx.historical_value_str(amount, date) entry.append(text) item = SortableTreeWidgetItem(entry) if icon: item.setIcon(0, icon) item.setToolTip( 0, str(conf) + " confirmation" + ("s" if conf != 1 else "")) item.setData(0, SortableTreeWidgetItem.DataRole, (status, conf)) if has_invoice: item.setIcon(3, self.invoiceIcon) for i in range(len(entry)): if i > 3: item.setTextAlignment(i, Qt.AlignRight | Qt.AlignVCenter) if i != 2: item.setFont(i, self.monospaceFont) if value and value < 0: item.setForeground(3, self.withdrawalBrush) item.setForeground(4, self.withdrawalBrush) item.setData(0, Qt.UserRole, tx_hash) self.addTopLevelItem(item, tx_hash) if current_tx == tx_hash: # Note that it's faster to setSelected once the item is in # the tree. Also note that doing setSelected() on the item # itself is much faster than doing setCurrentItem() # which must do a linear search in the tree (wastefully) item.setSelected(True)
def create_menu(self, position): menu = QMenu() selected = self.get_selected() def create_menu_inner(): if not selected: return coins = filter(lambda x: self.get_name(x) in selected, self.utxos) if not coins: return spendable_coins = list( filter(lambda x: not selected.get(self.get_name(x), ''), coins)) # Unconditionally add the "Spend" option but leave it disabled if there are no spendable_coins spend_action = menu.addAction( _("Spend"), lambda: self.parent.spend_coins(spendable_coins)) spend_action.setEnabled(bool(spendable_coins)) if len(selected) == 1: # "Copy ..." item = self.itemAt(position) if not item: return col = self.currentColumn() column_title = self.headerItem().text(col) alt_column_title, alt_copy_text = None, None slp_token = item.data(0, self.DataRoles.slp_token) ca_info = None if col == self.Col.output_point: copy_text = item.data(0, self.DataRoles.name) elif col == self.Col.address: addr = item.data(0, self.DataRoles.address) # Determine the "alt copy text" "Legacy Address" or "Cash Address" copy_text = addr.to_full_ui_string() if Address.FMT_UI == Address.FMT_LEGACY: alt_copy_text, alt_column_title = addr.to_full_string( Address.FMT_CASHADDR), _('Cash Address') else: alt_copy_text, alt_column_title = addr.to_full_string( Address.FMT_LEGACY), _('Legacy Address') ca_info = item.data( 0, self.DataRoles.cash_account) # may be None del addr else: copy_text = item.text(col) if copy_text: copy_text = copy_text.strip( ) # make sure formatted amount is not whitespaced menu.addAction( _("Copy {}").format(column_title), lambda: QApplication. instance().clipboard().setText(copy_text)) if alt_copy_text and alt_column_title: menu.addAction( _("Copy {}").format(alt_column_title), lambda: QApplication.instance().clipboard().setText( alt_copy_text)) if ca_info: self.wallet.cashacct.fmt_info( ca_info ) # paranoia: pre-cache minimal chash (may go out to network) menu.addAction( _("Copy Cash Account"), lambda: self.wallet and QApplication.instance().clipboard().setText( self.wallet.cashacct.fmt_info(ca_info, emoji=True) )) # single selection, offer them the "Details" option and also coin/address "freeze" status, if any txid = list(selected.keys())[0].split(':')[0] frozen_flags = list(selected.values())[0] tx = self.wallet.transactions.get(txid) if tx: label = self.wallet.get_label(txid) or None menu.addAction( _("Details"), lambda: self.parent.show_transaction(tx, label)) act = None needsep = True if 'c' in frozen_flags: menu.addSeparator() menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False) menu.addAction( _("Unfreeze Coin"), lambda: self.set_frozen_coins( list(selected.keys()), False)) menu.addSeparator() needsep = False else: menu.addAction( _("Freeze Coin"), lambda: self.set_frozen_coins( list(selected.keys()), True)) if 'a' in frozen_flags: if needsep: menu.addSeparator() menu.addAction(_("Address is frozen"), lambda: None).setEnabled(False) menu.addAction( _("Unfreeze Address"), lambda: self.set_frozen_addresses_for_coins( list(selected.keys()), False)) else: menu.addAction( _("Freeze Address"), lambda: self.set_frozen_addresses_for_coins( list(selected.keys()), True)) if not spend_action.isEnabled(): if slp_token: spend_action.setText(_("SLP Token: Spend Locked")) elif 'i' in frozen_flags: # immature coinbase spend_action.setText( _("Immature Coinbase: Spend Locked")) else: # multi-selection menu.addSeparator() if any(['c' not in flags for flags in selected.values()]): # they have some coin-level non-frozen in the selection, so add the menu action "Freeze coins" menu.addAction( _("Freeze Coins"), lambda: self.set_frozen_coins( list(selected.keys()), True)) if any(['c' in flags for flags in selected.values()]): # they have some coin-level frozen in the selection, so add the menu action "Unfreeze coins" menu.addAction( _("Unfreeze Coins"), lambda: self.set_frozen_coins( list(selected.keys()), False)) if any(['a' not in flags for flags in selected.values()]): # they have some address-level non-frozen in the selection, so add the menu action "Freeze addresses" menu.addAction( _("Freeze Addresses"), lambda: self.set_frozen_addresses_for_coins( list(selected.keys()), True)) if any(['a' in flags for flags in selected.values()]): # they have some address-level frozen in the selection, so add the menu action "Unfreeze addresses" menu.addAction( _("Unfreeze Addresses"), lambda: self.set_frozen_addresses_for_coins( list(selected.keys()), False)) create_menu_inner() run_hook('utxo_list_context_menu_setup', self, menu, selected) # add optional toggle actions menu.addSeparator() def toggle(): self.show_cash_accounts = not self.show_cash_accounts a = menu.addAction(_("Show Cash Accounts"), toggle) a.setCheckable(True) a.setChecked(self.show_cash_accounts) menu.exec_(self.viewport().mapToGlobal(position))
def on_update(self): local_maturity_height = (self.wallet.get_local_height() + 1) - COINBASE_MATURITY prev_selection = self.get_selected( ) # cache previous selection, if any self.clear() ca_by_addr = defaultdict(list) if self.show_cash_accounts: addr_set = set() self.utxos = self.wallet.get_utxos(addr_set_out=addr_set, exclude_slp=False) # grab all cash accounts so that we may add the emoji char for info in self.wallet.cashacct.get_cashaccounts(addr_set): ca_by_addr[info.address].append(info) del info for ca_list in ca_by_addr.values(): ca_list.sort( key=lambda info: ( (info.number or 0), str(info.collision_hash)) ) # sort the ca_lists by number, required by cashacct.get_address_default del ca_list # reference still exists inside ca_by_addr dict, this is just deleted here because we re-use this name below. del addr_set # clean-up. We don't want the below code to ever depend on the existence of this cell. else: self.utxos = self.wallet.get_utxos(exclude_slp=False) for x in self.utxos: address = x['address'] address_text = address.to_ui_string() ca_info = None ca_list = ca_by_addr.get(address) tool_tip0 = None if ca_list: ca_info = self.wallet.cashacct.get_address_default(ca_list) address_text = f'{ca_info.emoji} {address_text}' # prepend the address emoji char tool_tip0 = self.wallet.cashacct.fmt_info(ca_info, emoji=True) height = x['height'] is_immature = x['coinbase'] and height > local_maturity_height name = self.get_name(x) name_short = self.get_name_short(x) label = self.wallet.get_label(x['prevout_hash']) amount = self.parent.format_amount(x['value'], is_diff=False, whitespaces=True) utxo_item = SortableTreeWidgetItem( [address_text, label, amount, str(height), name_short]) if label: utxo_item.setToolTip( 1, label ) # just in case it doesn't fit horizontally, we also provide it as a tool tip where hopefully it won't be elided if tool_tip0: utxo_item.setToolTip(0, tool_tip0) utxo_item.setToolTip( 4, name) # just in case they like to see lots of hex digits :) utxo_item.DataRole = Qt.UserRole + 100 # set this here to avoid sorting based on Qt.UserRole+1 utxo_item.setFont(0, self.monospaceFont) utxo_item.setFont(2, self.monospaceFont) utxo_item.setFont(4, self.monospaceFont) utxo_item.setData(0, self.DataRoles.name, name) a_frozen = self.wallet.is_frozen(address) c_frozen = x['is_frozen_coin'] toolTipMisc = '' slp_token = x['slp_token'] if is_immature: for colNum in range(self.columnCount()): if colNum == self.Col.label: continue # don't color the label column utxo_item.setForeground(colNum, self.immatureColor) toolTipMisc = _('Coin is not yet mature') elif slp_token: utxo_item.setBackground(0, self.slpBG) toolTipMisc = _('Coin contains an SLP token') elif a_frozen and not c_frozen: # address is frozen, coin is not frozen # emulate the "Look" off the address_list .py's frozen entry utxo_item.setBackground(0, self.lightBlue) toolTipMisc = _("Address is frozen") elif c_frozen and not a_frozen: # coin is frozen, address is not frozen utxo_item.setBackground(0, self.blue) toolTipMisc = _("Coin is frozen") elif c_frozen and a_frozen: # both coin and address are frozen so color-code it to indicate that. utxo_item.setBackground(0, self.lightBlue) utxo_item.setForeground(0, self.cyanBlue) toolTipMisc = _("Coin & Address are frozen") # save the address-level-frozen and coin-level-frozen flags to the data item for retrieval later in create_menu() below. utxo_item.setData( 0, self.DataRoles.frozen_flags, "{}{}{}{}".format( ("a" if a_frozen else ""), ("c" if c_frozen else ""), ("s" if slp_token else ""), ("i" if is_immature else ""))) # store the address utxo_item.setData(0, self.DataRoles.address, address) # store the ca_info for this address -- if any if ca_info: utxo_item.setData(0, self.DataRoles.cash_account, ca_info) # store the slp_token utxo_item.setData(0, self.DataRoles.slp_token, slp_token) if toolTipMisc: utxo_item.setToolTip(0, toolTipMisc) run_hook("utxo_list_item_setup", self, utxo_item, x, name) self.addChild(utxo_item) if name 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 self._update_utxo_count_display(len(self.utxos))
def create_menu(self, position): if self.picker: # picker mode has no menu return from oregano.wallet import Multisig_Wallet is_multisig = isinstance(self.wallet, Multisig_Wallet) can_delete = self.wallet.can_delete_address() selected = self.selectedItems() multi_select = len(selected) > 1 addrs = [item.data(0, self.DataRoles.address) for item in selected] if not addrs: return addrs = [addr for addr in addrs if isinstance(addr, Address)] menu = QMenu() where_to_insert_dupe_copy_cash_account = None def doCopy(txt): txt = txt.strip() self.parent.copy_to_clipboard(txt) col = self.currentColumn() column_title = self.headerItem().text(col) if not multi_select: item = self.itemAt(position) if not item: return if not addrs: item.setExpanded(not item.isExpanded()) return addr = addrs[0] alt_copy_text, alt_column_title = None, None if col == 0: copy_text = addr.to_full_ui_string() if Address.FMT_UI == Address.FMT_LEGACY: alt_copy_text, alt_column_title = addr.to_full_string( Address.FMT_CASHADDR), _('Cash Address') else: alt_copy_text, alt_column_title = addr.to_full_string( Address.FMT_LEGACY), _('Legacy Address') else: copy_text = item.text(col) menu.addAction( _("Copy {}").format(column_title), lambda: doCopy(copy_text)) if alt_copy_text and alt_column_title: # Add 'Copy Legacy Address' and 'Copy Cash Address' alternates if right-click is on column 0 menu.addAction( _("Copy {}").format(alt_column_title), lambda: doCopy(alt_copy_text)) a = menu.addAction( _('Details') + "...", lambda: self.parent.show_address(addr)) if col == 0: where_to_insert_dupe_copy_cash_account = a if col in self.editable_columns: menu.addAction( _("Edit {}").format(column_title), lambda: self.editItem( self.itemAt( position ), # NB: C++ item may go away if this widget is refreshed while menu is up -- so need to re-grab and not store in lamba. See #953 col)) a = menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) if self.wallet.get_num_tx(addr) or self.wallet.has_payment_request( addr): # This address cannot be used for a payment request because # the receive tab will refuse to display it and will instead # create a request with a new address, if we were to call # self.parent.receive_at(addr). This is because the recieve tab # now strongly enforces no-address-reuse. See #1552. a.setDisabled(True) if self.wallet.can_export(): menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) if not is_multisig and not self.wallet.is_watching_only(): menu.addAction( _("Sign/verify message") + "...", lambda: self.parent.sign_verify_message(addr)) menu.addAction( _("Encrypt/decrypt message") + "...", lambda: self.parent.encrypt_message(addr)) if can_delete: menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr)) addr_URL = web.BE_URL(self.config, 'addr', addr) if addr_URL: menu.addAction(_("View on block explorer"), lambda: webopen(addr_URL)) else: # multi-select if col > -1: texts, alt_copy, alt_copy_text = None, None, None if col == 0: # address column texts = [a.to_ui_string() for a in addrs] # Add additional copy option: "Address, Balance (n)" alt_copy = _("Copy {}").format( _("Address") + ", " + _("Balance")) + f" ({len(addrs)})" alt_copy_text = "\n".join([ a.to_ui_string() + ", " + self.parent.format_amount( sum(self.wallet.get_addr_balance(a))) for a in addrs ]) else: texts = [i.text(col).strip() for i in selected] texts = [t for t in texts if t] # omit empty items if texts: copy_text = '\n'.join(texts) menu.addAction( _("Copy {}").format(column_title) + f" ({len(texts)})", lambda: doCopy(copy_text)) if alt_copy and alt_copy_text: menu.addAction(alt_copy, lambda: doCopy(alt_copy_text)) freeze = self.parent.set_frozen_state if any(self.wallet.is_frozen(addr) for addr in addrs): menu.addAction(_("Unfreeze"), partial(freeze, addrs, False)) if not all(self.wallet.is_frozen(addr) for addr in addrs): menu.addAction(_("Freeze"), partial(freeze, addrs, True)) coins = self.wallet.get_spendable_coins(domain=addrs, config=self.config) if coins: menu.addAction(_("Spend from"), partial(self.parent.spend_coins, coins)) run_hook('address_list_context_menu_setup', self, menu, addrs) # Add Cash Accounts section at the end, if relevant if not multi_select: ca_list = item.data(0, self.DataRoles.cash_accounts) menu.addSeparator() a1 = menu.addAction(_("Cash Accounts"), lambda: None) a1.setDisabled(True) if ca_list: ca_default = self._ca_get_default(ca_list) for ca_info in ca_list: ca_text = self.wallet.cashacct.fmt_info( ca_info, ca_info.minimal_chash) ca_text_em = self.wallet.cashacct.fmt_info( ca_info, ca_info.minimal_chash, emoji=True) m = menu.addMenu(ca_info.emoji + " " + ca_text) a_ca_copy = m.addAction( _("Copy Cash Account"), lambda x=None, text=ca_text_em: doCopy(text)) a = m.addAction( _("Details") + "...", lambda x=None, ca_text=ca_text: cashacctqt. cash_account_detail_dialog(self.parent, ca_text)) a = m.addAction(_("View registration tx") + "...", lambda x=None, ca=ca_info: self.parent. do_process_from_txid(txid=ca.txid)) a = a_def = m.addAction(_("Make default for address"), lambda x=None, ca=ca_info: self. _ca_set_default(ca, True)) if ca_info == ca_default: if where_to_insert_dupe_copy_cash_account and a_ca_copy: # insert a dupe of "Copy Cash Account" for the default cash account for this address in the top-level menu menu.insertAction( where_to_insert_dupe_copy_cash_account, a_ca_copy) m.setTitle(m.title() + " " + "★") a_def.setDisabled(True) a_def.setCheckable(True) a_def.setChecked(True) a_def.setText(_("Is default for address")) else: a1.setText(_("No Cash Accounts")) a_new = menu.addAction(_("Register new..."), lambda x=None, addr=addr: self.parent. register_new_cash_account(addr)) a_new.setIcon(__class__._cashacct_icon) run_hook('receive_menu', menu, addrs, self.wallet) menu.exec_(self.viewport().mapToGlobal(position))