address_list.py (12334B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2015 Thomas Voegtlin 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 from enum import IntEnum 27 28 from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex 29 from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont 30 from PyQt5.QtWidgets import QAbstractItemView, QComboBox, QLabel, QMenu 31 32 from electrum.i18n import _ 33 from electrum.util import block_explorer_URL, profiler 34 from electrum.plugin import run_hook 35 from electrum.bitcoin import is_address 36 from electrum.wallet import InternalAddressCorruption 37 38 from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen, MySortModel 39 40 41 class AddressUsageStateFilter(IntEnum): 42 ALL = 0 43 UNUSED = 1 44 FUNDED = 2 45 USED_AND_EMPTY = 3 46 47 def ui_text(self) -> str: 48 return { 49 self.ALL: _('All'), 50 self.UNUSED: _('Unused'), 51 self.FUNDED: _('Funded'), 52 self.USED_AND_EMPTY: _('Used'), 53 }[self] 54 55 56 class AddressTypeFilter(IntEnum): 57 ALL = 0 58 RECEIVING = 1 59 CHANGE = 2 60 61 def ui_text(self) -> str: 62 return { 63 self.ALL: _('All'), 64 self.RECEIVING: _('Receiving'), 65 self.CHANGE: _('Change'), 66 }[self] 67 68 69 class AddressList(MyTreeView): 70 71 class Columns(IntEnum): 72 TYPE = 0 73 ADDRESS = 1 74 LABEL = 2 75 COIN_BALANCE = 3 76 FIAT_BALANCE = 4 77 NUM_TXS = 5 78 79 filter_columns = [Columns.TYPE, Columns.ADDRESS, Columns.LABEL, Columns.COIN_BALANCE] 80 81 ROLE_SORT_ORDER = Qt.UserRole + 1000 82 83 def __init__(self, parent): 84 super().__init__(parent, self.create_menu, stretch_column=self.Columns.LABEL) 85 self.wallet = self.parent.wallet 86 self.setSelectionMode(QAbstractItemView.ExtendedSelection) 87 self.setSortingEnabled(True) 88 self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter 89 self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter 90 self.change_button = QComboBox(self) 91 self.change_button.currentIndexChanged.connect(self.toggle_change) 92 for addr_type in AddressTypeFilter.__members__.values(): # type: AddressTypeFilter 93 self.change_button.addItem(addr_type.ui_text()) 94 self.used_button = QComboBox(self) 95 self.used_button.currentIndexChanged.connect(self.toggle_used) 96 for addr_usage_state in AddressUsageStateFilter.__members__.values(): # type: AddressUsageStateFilter 97 self.used_button.addItem(addr_usage_state.ui_text()) 98 self.std_model = QStandardItemModel(self) 99 self.proxy = MySortModel(self, sort_role=self.ROLE_SORT_ORDER) 100 self.proxy.setSourceModel(self.std_model) 101 self.setModel(self.proxy) 102 self.update() 103 self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder) 104 105 def get_toolbar_buttons(self): 106 return QLabel(_("Filter:")), self.change_button, self.used_button 107 108 def on_hide_toolbar(self): 109 self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter 110 self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter 111 self.update() 112 113 def save_toolbar_state(self, state, config): 114 config.set_key('show_toolbar_addresses', state) 115 116 def refresh_headers(self): 117 fx = self.parent.fx 118 if fx and fx.get_fiat_address_config(): 119 ccy = fx.get_currency() 120 else: 121 ccy = _('Fiat') 122 headers = { 123 self.Columns.TYPE: _('Type'), 124 self.Columns.ADDRESS: _('Address'), 125 self.Columns.LABEL: _('Label'), 126 self.Columns.COIN_BALANCE: _('Balance'), 127 self.Columns.FIAT_BALANCE: ccy + ' ' + _('Balance'), 128 self.Columns.NUM_TXS: _('Tx'), 129 } 130 self.update_headers(headers) 131 132 def toggle_change(self, state: int): 133 if state == self.show_change: 134 return 135 self.show_change = AddressTypeFilter(state) 136 self.update() 137 138 def toggle_used(self, state: int): 139 if state == self.show_used: 140 return 141 self.show_used = AddressUsageStateFilter(state) 142 self.update() 143 144 @profiler 145 def update(self): 146 if self.maybe_defer_update(): 147 return 148 current_address = self.current_item_user_role(col=self.Columns.LABEL) 149 if self.show_change == AddressTypeFilter.RECEIVING: 150 addr_list = self.wallet.get_receiving_addresses() 151 elif self.show_change == AddressTypeFilter.CHANGE: 152 addr_list = self.wallet.get_change_addresses() 153 else: 154 addr_list = self.wallet.get_addresses() 155 self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change 156 self.std_model.clear() 157 self.refresh_headers() 158 fx = self.parent.fx 159 set_address = None 160 addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit() 161 for address in addr_list: 162 num = self.wallet.get_address_history_len(address) 163 label = self.wallet.get_label(address) 164 c, u, x = self.wallet.get_addr_balance(address) 165 balance = c + u + x 166 is_used_and_empty = self.wallet.is_used(address) and balance == 0 167 if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty): 168 continue 169 if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0: 170 continue 171 if self.show_used == AddressUsageStateFilter.USED_AND_EMPTY and not is_used_and_empty: 172 continue 173 balance_text = self.parent.format_amount(balance, whitespaces=True) 174 # create item 175 if fx and fx.get_fiat_address_config(): 176 rate = fx.exchange_rate() 177 fiat_balance = fx.value_str(balance, rate) 178 else: 179 fiat_balance = '' 180 labels = ['', address, label, balance_text, fiat_balance, "%d"%num] 181 address_item = [QStandardItem(e) for e in labels] 182 # align text and set fonts 183 for i, item in enumerate(address_item): 184 item.setTextAlignment(Qt.AlignVCenter) 185 if i not in (self.Columns.TYPE, self.Columns.LABEL): 186 item.setFont(QFont(MONOSPACE_FONT)) 187 self.set_editability(address_item) 188 address_item[self.Columns.FIAT_BALANCE].setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) 189 # setup column 0 190 if self.wallet.is_change(address): 191 address_item[self.Columns.TYPE].setText(_('change')) 192 address_item[self.Columns.TYPE].setBackground(ColorScheme.YELLOW.as_color(True)) 193 else: 194 address_item[self.Columns.TYPE].setText(_('receiving')) 195 address_item[self.Columns.TYPE].setBackground(ColorScheme.GREEN.as_color(True)) 196 address_item[self.Columns.LABEL].setData(address, Qt.UserRole) 197 address_path = self.wallet.get_address_index(address) 198 address_item[self.Columns.TYPE].setData(address_path, self.ROLE_SORT_ORDER) 199 address_path_str = self.wallet.get_address_path_str(address) 200 if address_path_str is not None: 201 address_item[self.Columns.TYPE].setToolTip(address_path_str) 202 address_item[self.Columns.FIAT_BALANCE].setData(balance, self.ROLE_SORT_ORDER) 203 # setup column 1 204 if self.wallet.is_frozen_address(address): 205 address_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True)) 206 if address in addresses_beyond_gap_limit: 207 address_item[self.Columns.ADDRESS].setBackground(ColorScheme.RED.as_color(True)) 208 # add item 209 count = self.std_model.rowCount() 210 self.std_model.insertRow(count, address_item) 211 address_idx = self.std_model.index(count, self.Columns.LABEL) 212 if address == current_address: 213 set_address = QPersistentModelIndex(address_idx) 214 self.set_current_idx(set_address) 215 # show/hide columns 216 if fx and fx.get_fiat_address_config(): 217 self.showColumn(self.Columns.FIAT_BALANCE) 218 else: 219 self.hideColumn(self.Columns.FIAT_BALANCE) 220 self.filter() 221 self.proxy.setDynamicSortFilter(True) 222 223 def create_menu(self, position): 224 from electrum.wallet import Multisig_Wallet 225 is_multisig = isinstance(self.wallet, Multisig_Wallet) 226 can_delete = self.wallet.can_delete_address() 227 selected = self.selected_in_column(self.Columns.ADDRESS) 228 if not selected: 229 return 230 multi_select = len(selected) > 1 231 addrs = [self.item_from_index(item).text() for item in selected] 232 menu = QMenu() 233 if not multi_select: 234 idx = self.indexAt(position) 235 if not idx.isValid(): 236 return 237 item = self.item_from_index(idx) 238 if not item: 239 return 240 addr = addrs[0] 241 addr_column_title = self.std_model.horizontalHeaderItem(self.Columns.LABEL).text() 242 addr_idx = idx.sibling(idx.row(), self.Columns.LABEL) 243 self.add_copy_menu(menu, idx) 244 menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) 245 persistent = QPersistentModelIndex(addr_idx) 246 menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p))) 247 #menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) 248 if self.wallet.can_export(): 249 menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) 250 if not is_multisig and not self.wallet.is_watching_only(): 251 menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr)) 252 menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr)) 253 if can_delete: 254 menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr)) 255 addr_URL = block_explorer_URL(self.config, 'addr', addr) 256 if addr_URL: 257 menu.addAction(_("View on block explorer"), lambda: webopen(addr_URL)) 258 259 if not self.wallet.is_frozen_address(addr): 260 menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) 261 else: 262 menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], False)) 263 264 coins = self.wallet.get_spendable_coins(addrs) 265 if coins: 266 menu.addAction(_("Spend from"), lambda: self.parent.utxo_list.set_spend_list(coins)) 267 268 run_hook('receive_menu', menu, addrs, self.wallet) 269 menu.exec_(self.viewport().mapToGlobal(position)) 270 271 def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: 272 if is_address(text): 273 try: 274 self.wallet.check_address_for_corruption(text) 275 except InternalAddressCorruption as e: 276 self.parent.show_error(str(e)) 277 raise 278 super().place_text_on_clipboard(text, title=title)