def invoke(firstarg='0.0.0.0', sport='8787', upnp_str=None, addr_str=None): bindhost, *extrahosts = firstarg.split(',') if len(extrahosts) > 1: raise Exception("too many hosts") elif len(extrahosts) == 1: [ announcehost, ] = extrahosts else: announcehost = None port = int(sport) pnp = get_upnp() if upnp_str == 'upnp' else None if not pnp and not addr_str: # third arg may be addr_str, so swap the args addr_str = upnp_str upnp_str = None addr = None if addr_str: assert Address.is_valid( addr_str), "Invalid donation address specified" addr = Address.from_string(addr_str) return self.start_fusion_server(network, bindhost, port, upnp=pnp, announcehost=announcehost, donation_address=addr)
def doVerify(self) -> None: addrtf = self.tf address_str = str(addrtf.text).strip() message = str(self.topTvDel.text) signature = str(self.botTvDel.text).strip() if not signature: parent().show_message( _("Please provide both a signature and a message to verify")) return try: address = Address.from_string(address_str) except: parent().show_error(_('Invalid Ergon address.')) return message = message.encode('utf-8') try: # This can raise on invalid base64 sig = base64.b64decode(signature) verified = bitcoin.verify_message( address, sig, message) # this raises too on failure except: verified = False if verified: parent().show_message(_("Signature verified"), title=_("Success")) else: parent().show_error(_("Wrong signature"))
def on_edited(self, item, column, prior_value): contact = item.data(0, self.DataRoles.Contact) if column == 2: # Label label_key = contact.address try: label_key = Address.from_string(label_key).to_storage_string() except: pass self.wallet.set_label(label_key, item.text(2)) self.update( ) # force refresh in case 2 contacts use the same address return # else.. Name typ = contact.type was_cur, was_sel = bool(self.currentItem()), item.isSelected() name, value = item.text(1), item.text(3) del item # paranoia # On success, parent.set_contact returns the new key (address text) # if 'cashacct'.. or always the same key for all other types. key = self.parent.set_contact(name, value, typ=typ, replace=contact) if key: # Due to deferred updates, on_update will actually be called later. # So, we have to save the edited item's "current" and "selected" # status here. 'on_update' will look at this tuple and clear it # after updating. self._edited_item_cur_sel = (key, was_cur, was_sel)
def doConversion_(self, text) -> bool: self.cash.text = "" self.legacy.text = "" self.cpyCashBut.enabled = False self.cpyLegBut.enabled = False self.qrButShowCash.enabled = False self.qrButShowLegacy.enabled = False text = text.strip() addy = None try: addy = Address.from_string(text) except: pass if addy: self.cash.text = addy.to_full_string(Address.FMT_CASHADDR) self.legacy.text = addy.to_full_string(Address.FMT_LEGACY) self.cpyCashBut.enabled = True self.cpyLegBut.enabled = True self.qrButShowCash.enabled = True self.qrButShowLegacy.enabled = True return True return False
def make_unsigned_transaction(self, amount, fee, all_inputs, outputs, changes): ''' make unsigned transaction ''' dust = self.dust_threshold( ) # always 546 for now, but this call is here in case something more sophisticated happens in the future coins = {} tx_inputs = [] amounts = {} try: for player in all_inputs: inputs_coins = self.get_coins(all_inputs[player]) # if there are no coins on input it terminates the process if inputs_coins: coins[player] = inputs_coins else: return None except BaseException as e: self.print_error('make_unsigned_transaction:', repr(e)) return None for player, pubkey_utxos in coins.items(): amounts[player] = 0 for pubkey, utxos in pubkey_utxos.items(): for utxo in utxos: utxo['type'] = 'p2pkh' utxo['address'] = Address.from_pubkey(pubkey) utxo['pubkeys'] = [pubkey] utxo['x_pubkeys'] = [pubkey] utxo['prevout_hash'] = utxo['tx_hash'] utxo['prevout_n'] = utxo['tx_pos'] utxo['signatures'] = [None] utxo['num_sig'] = 1 tx_inputs.append(utxo) amounts[player] += utxo['value'] tx_inputs.sort(key=lambda x: x['prevout_hash'] + str(x["tx_pos"])) tx_outputs = [(TYPE_ADDRESS, Address.from_string(output), int(amount)) for output in outputs] transaction = Transaction.from_io(tx_inputs, tx_outputs, sign_schnorr=False) tx_changes = [ (TYPE_ADDRESS, Address.from_string(changes[player]), int(amounts[player] - amount - fee)) for player in sorted(changes) if Address.is_valid(changes[player]) and int(amounts[player] - amount - fee) >= dust ] transaction.add_outputs(tx_changes) return transaction
def view_addr_link_activated(addr): if isinstance(parent, ElectrumWindow): try: address = Address.from_string(addr) parent.show_address(address, parent=parent.top_level_window()) except Exception as e: parent.print_error(repr(e))
def donation_address(self, window) -> Optional[Tuple[str, Address]]: ''' Plugin API: Returns a tuple of (description, Address) or None. This is the donation address that we as a client got from the remote server (as opposed to the donation address we announce if we are a server). ''' if self.remote_donation_address and Address.is_valid( self.remote_donation_address): return (self.fullname() + " " + _("Server") + ": " + self.get_server()[0], Address.from_string(self.remote_donation_address))
def delete_history(self, address: Address) -> None: if isinstance(address, str) and Address.is_valid(address): address = Address.from_string(address) storage = self.parent.wallet.storage if self.parent.wallet else None if storage: if True: #with self.lock: addrstr = address.to_storage_string() k = 'contact_history_%s' % (addrstr) storage.put(k, None) self.print_error("Deleted %s" % addrstr)
def decodeAddress(addr: str) -> Address: ret = None if addr: try: # re-encode addr in case they went to/from cashaddr ret = Address.from_string(addr) except BaseException as e: utils.NSLog("Caught exception decoding address %s: %s", addr, str(e)) return ret
def getaddressunspent(self, address): """Returns the UTXO list of any address. Note: This is a walletless server query, results are not checked by SPV. """ if not isinstance(address, Address): try: address = Address.from_string(address) except AddressError: return None sh = address.to_scripthash_hex() return self.network.synchronous_get( ('blockchain.scripthash.listunspent', [sh]))
def get_history(self, address: Address) -> list: ret = list() if isinstance(address, str) and Address.is_valid(address): address = Address.from_string(address) storage = self.parent.wallet.storage if self.parent.wallet else None if storage: addrstr = address.to_storage_string() k = 'contact_history_%s' % (addrstr) hdict = storage.get(k) if hdict: ret = list(hdict.values()) ret.sort(key=lambda x: x[1], reverse=True) return ret
def _open_internal_link(self, target): ''' accepts either a str txid, str address, or a QUrl which should be of the bare form "txid" and/or "address" -- used by the clickable links in the inputs/outputs QTextBrowsers''' if isinstance(target, QUrl): target = target.toString(QUrl.None_) assert target if Address.is_valid(target): # target was an address, open address dialog self.main_window.show_address(Address.from_string(target), parent=self) else: # target was a txid, open new tx dialog self.main_window.do_process_from_txid(txid=target, parent=self)
def doReloadForKey(self, key: Any) -> Any: t0 = time.time() hist = list() unk = False duped = '' if isinstance(key, (type(None), list)): # the common case, 'None' or [Address] hist = get_history(domain=key) # contacts entires elif isinstance(key, contacts.ContactsEntry): hist = get_contact_history(key.address) elif isinstance(key, Address): # support for list-less single Address.. call self again with the proper format hist = self.get([key]) duped = ' (duped) ' elif isinstance(key, str): # support for string addresses or tx_hashes.. detect which and act accordingly if Address.is_valid(key): hist = self.get( [Address.from_string(key)] ) # recursively call self with propery list data type, which will end up calling get_history (it's ok -- this is to cache results uniformly!) duped = ' (duped) ' elif gui.ElectrumGui.gui.wallet and gui.ElectrumGui.gui.wallet.transactions.get( key, None): fullHist = self.get( None ) # recursively call self to get a full history (will be cached so it's ok!) try: hentry = fullHist.get_by_txid(key) hist.append(hentry) except KeyError: pass else: unk = True else: unk = True dstr = str(key) if not isinstance( key, contacts.ContactsEntry ) else '[ContactsEntry: ' + key.address_str + ']' if unk: utils.NSLog( "HistoryMgr: failed to retrieve any data for unknown domain=%s, returning empty list", dstr[:80]) else: time_taken = time.time() - t0 utils.NSLog( "HistoryMgr: refresh %d entries for domain=%s in %f ms%s (hist result type=%s)", len(hist), dstr[:80], time_taken * 1e3, duped, ''.join(list(str(type(hist)))[-19:-2])) gui.ElectrumGui.gui.refresh_cost('history', time_taken) return hist
def load_shuffle_change_shared_with_others(wallet): ''' Modifies wallet instance and adds _shuffle_change_shared_with_others retrieving it from storage. _shuffle_patched_ need not be set. ''' wallet._shuffle_change_shared_with_others = set() tmpadrs = wallet.storage.get( ConfKeys.PerWallet.CHANGE_SHARED_WITH_OTHERS, []) if isinstance(tmpadrs, (list, tuple, set)): for a in tmpadrs: try: a = Address.from_string(a) if not wallet.get_address_history( a ): # no need to re-add to set if it has a history since it won't be shared anyway with the network if it's been used. This set is used only to not cross over shuffled out addresses with change addresses for unused addrs when shuffling wallet._shuffle_change_shared_with_others.add(a) except (AddressError, TypeError): pass
def _make_wallet_cashacct_pseudo_contacts(self, exclude_contacts=[] ) -> List[Contact]: ''' Returns a list of 'fake' contacts that come from the wallet's own registered Cash Accounts. These contacts do not exist in the wallet.contacts object but are created on-the-fly from the wallet.cashacct list of registered & verified Cash Accounts. The creation of this list is relatively cheap and scales as the lookups are O(logN) in the cashaccts caches. This is a convenience so that the Contacts tab shows "my" cash accounts after registration as well as external Cash Accounts. Note that the "mine" entries won't be shown if the user explicitly added his own as "external"... ''' try: excl_chk = set((c.name, Address.from_string(c.address)) for c in exclude_contacts if c.type == 'cashacct') except: # Hmm.. invalid address? excl_chk = set() wallet_cashaccts = [] v_txids = set() # Add the [Mine] pseudo-contacts for ca_info in self.wallet.cashacct.get_wallet_cashaccounts(): v_txids.add(ca_info.txid) name = self.wallet.cashacct.fmt_info(ca_info, emoji=False) if (name, ca_info.address) in excl_chk: continue wallet_cashaccts.append( Contact(name=name, address=ca_info.address.to_ui_string(), type='cashacct_W')) # Add the [Pend] pseudo-contacts for txid, tup in self._ca_pending_conf.copy().items(): if txid in v_txids or self.wallet.cashacct.is_verified(txid): self._ca_pending_conf.pop(txid, None) continue if tup in excl_chk: continue name, address = tup wallet_cashaccts.append( Contact(name=name, address=address.to_ui_string(), type='cashacct_T')) return wallet_cashaccts
def doReloadForKey(self, key: Any) -> Any: if key is None: a = AddressData(gui.ElectrumGui.gui) a.refresh() utils.NSLog("AddressMgr refresh (full)") return a elif key and isinstance(key, (str, Address)): if isinstance(key, str): key = Address.from_string(key) a = self.get( None ) # recursive call to self to get cached 'all' or rebuild 'all' if not cached if a: entries = a.master[0][0] for entry in entries: if entry.address == key: return entry return None
def doSign(self) -> None: addrtf = self.tf address = str(addrtf.text).strip() message = str(self.topTvDel.text) signatureTvDel = self.botTvDel try: print("address = ", address) addr = Address.from_string(address) except: parent().show_error(_('Invalid Ergon address.')) return if addr.kind != addr.ADDR_P2PKH: msg_sign = _("Signing with an address actually means signing with the corresponding " "private key, and verifying with the corresponding public key. The " "address you have entered does not have a unique public key, so these " "operations cannot be performed.") + '\n\n' + \ _('The operation is undefined. Not just in Oregano, but in general.') parent().show_message( _('Cannot sign messages with this type of address.') + '\n\n' + msg_sign) return if not parent().wallet: return if parent().wallet.is_watching_only(): parent().show_message(_('This is a watching-only wallet.')) return if not parent().wallet.is_mine(addr): parent().show_message(_('Address not in wallet.')) return def onPw(password: str) -> None: try: signed = parent().wallet.sign_message(addr, message, password) except: parent().show_error(str(sys.exc_info()[1])) return signatureTvDel.text = base64.b64encode(signed).decode('ascii') parent().show_message(_( "The signature for the provided message has been pasted into the signature text box." ), title=_("Success")) parent().prompt_password_if_needed_asynch(onPw, vc=self)
def on_update(self): self.chkVisible() # update the receive address if necessary current_address_string = self.parent.receive_address_e.text().strip() current_address = Address.from_string(current_address_string) if len( current_address_string) else None domain = self.wallet.get_receiving_addresses() addr = self.wallet.get_unused_address() if current_address not in domain and addr: self.parent.set_receive_address(addr) # clear the list and fill it again item = self.currentItem() prev_sel = item.data(0, Qt.UserRole) if item else None self.clear() for req in self.wallet.get_sorted_requests(self.config): address = req['address'] if address not in domain: continue timestamp = req.get('time', 0) amount = req.get('amount') expiration = req.get('exp', None) message = req.get('memo', '') date = format_time(timestamp) status = req.get('status') signature = req.get('sig') requestor = req.get('name', '') amount_str = self.parent.format_amount(amount) if amount else "" item = QTreeWidgetItem([ date, address.to_ui_string(), '', message, amount_str, _(pr_tooltips.get(status, '')) ]) item.setData(0, Qt.UserRole, address) if signature is not None: item.setIcon(2, QIcon(":icons/seal.svg")) item.setToolTip(2, 'signed by ' + requestor) if status is not PR_UNKNOWN: item.setIcon(6, QIcon(pr_icons.get(status))) self.addTopLevelItem(item) if prev_sel == address: self.setCurrentItem(item)
def getUnusedAddress(self) -> None: wallet = parent().wallet if not wallet: return # hide receive tab if no receive requests available b = len(wallet.receive_requests) > 0 #self.setVisible(b) #self.parent.receive_requests_label.setVisible(b) #if not b: # self.parent.expires_label.hide() # self.parent.expires_combo.show() domain = wallet.get_addresses() if self.addrStr: # update the receive address if necessary current_address = Address.from_string(self.addrStr) addr = wallet.get_unused_address() if not current_address in domain and addr: self.setReceiveAddress_(addr.to_ui_string()) current_address = addr.to_ui_string()
def open_link(link): if Address.is_valid(link): addr = Address.from_string(link) if wallet.is_mine(addr): parent.show_address(addr) else: addr_URL = web.BE_URL(parent.config, 'addr', addr) if addr_URL: webopen(addr_URL) return if link.startswith('http'): webopen(link) elif len(link) == 64: # 64 character txid tx = wallet.transactions.get(link) if tx: parent.show_transaction(tx, tx_desc=wallet.get_label(link)) else: parent.do_process_from_txid(txid=link, tx_desc=wallet.get_label(link)) return
def get_full_contacts(self, include_pseudo: bool = True) -> List[Contact]: ''' Returns all the contacts, with the "My CashAcct" pseudo-contacts clobbering dupes of the same type that were manually added. Client code should scan for type == 'cashacct' and type == 'cashacct_W' ''' if include_pseudo: # filter out cachaccts that are "Wallet", as they will be added # at the end as pseudo contacts if they also appear in real contacts real_contacts = [ contact for contact in self.parent.contacts.get_all(nocopy=True) if contact.type != 'cashacct' # accept anything that's not cashacct or not Address.is_valid( contact.address ) # or if it is, it can have invalid address as it's clearly 'not mine" or not self.wallet.is_mine( # or if it's not mine Address.from_string(contact.address)) ] return real_contacts + self._make_wallet_cashacct_pseudo_contacts() else: return self.parent.contacts.get_all(nocopy=True)
def viewWillDisappear_(self, animated: bool) -> None: send_super(__class__, self, 'viewWillDisappear:', animated, argtypes=[c_bool]) parent().cash_addr_sig.disconnect(self) data = utils.nspy_get_byname(self, 'data') if not data: data = DialogData(None, None) text = self.tf.text.strip() if self.mode == SignVerify: try: address = Address.from_string(text) data = utils.set_namedtuple_field(data, 'address', address) except: pass elif self.mode == EncryptDecrypt: data = utils.set_namedtuple_field(data, 'pubkey', text) utils.nspy_put_byname(self, data, 'data') if self.kbas: utils.unregister_keyboard_autoscroll(self.kbas) self.kbas = None
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 parse_address(self, line): r = line.strip() m = re.match(RE_ALIAS, r) address = m.group(2) if m else r return Address.from_string(address)
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)