1#!/usr/local/bin/python3.8 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 26from enum import IntEnum 27 28from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex 29from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont 30from PyQt5.QtWidgets import QAbstractItemView, QComboBox, QLabel, QMenu 31 32from electrum.i18n import _ 33from electrum.util import block_explorer_URL, profiler 34from electrum.plugin import run_hook 35from electrum.bitcoin import is_address 36from electrum.wallet import InternalAddressCorruption 37 38from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen, MySortModel 39 40 41class AddressUsageStateFilter(IntEnum): 42 ALL = 0 43 UNUSED = 1 44 FUNDED = 2 45 USED_AND_EMPTY = 3 46 FUNDED_OR_UNUSED = 4 47 48 def ui_text(self) -> str: 49 return { 50 self.ALL: _('All'), 51 self.UNUSED: _('Unused'), 52 self.FUNDED: _('Funded'), 53 self.USED_AND_EMPTY: _('Used'), 54 self.FUNDED_OR_UNUSED: _('Funded or Unused'), 55 }[self] 56 57 58class AddressTypeFilter(IntEnum): 59 ALL = 0 60 RECEIVING = 1 61 CHANGE = 2 62 63 def ui_text(self) -> str: 64 return { 65 self.ALL: _('All'), 66 self.RECEIVING: _('Receiving'), 67 self.CHANGE: _('Change'), 68 }[self] 69 70 71class AddressList(MyTreeView): 72 73 class Columns(IntEnum): 74 TYPE = 0 75 ADDRESS = 1 76 LABEL = 2 77 COIN_BALANCE = 3 78 FIAT_BALANCE = 4 79 NUM_TXS = 5 80 81 filter_columns = [Columns.TYPE, Columns.ADDRESS, Columns.LABEL, Columns.COIN_BALANCE] 82 83 ROLE_SORT_ORDER = Qt.UserRole + 1000 84 ROLE_ADDRESS_STR = Qt.UserRole + 1001 85 86 def __init__(self, parent): 87 super().__init__(parent, self.create_menu, 88 stretch_column=self.Columns.LABEL, 89 editable_columns=[self.Columns.LABEL]) 90 self.wallet = self.parent.wallet 91 self.setSelectionMode(QAbstractItemView.ExtendedSelection) 92 self.setSortingEnabled(True) 93 self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter 94 self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter 95 self.change_button = QComboBox(self) 96 self.change_button.currentIndexChanged.connect(self.toggle_change) 97 for addr_type in AddressTypeFilter.__members__.values(): # type: AddressTypeFilter 98 self.change_button.addItem(addr_type.ui_text()) 99 self.used_button = QComboBox(self) 100 self.used_button.currentIndexChanged.connect(self.toggle_used) 101 for addr_usage_state in AddressUsageStateFilter.__members__.values(): # type: AddressUsageStateFilter 102 self.used_button.addItem(addr_usage_state.ui_text()) 103 self.std_model = QStandardItemModel(self) 104 self.proxy = MySortModel(self, sort_role=self.ROLE_SORT_ORDER) 105 self.proxy.setSourceModel(self.std_model) 106 self.setModel(self.proxy) 107 self.update() 108 self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder) 109 110 def get_toolbar_buttons(self): 111 return QLabel(_("Filter:")), self.change_button, self.used_button 112 113 def on_hide_toolbar(self): 114 self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter 115 self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter 116 self.update() 117 118 def save_toolbar_state(self, state, config): 119 config.set_key('show_toolbar_addresses', state) 120 121 def refresh_headers(self): 122 fx = self.parent.fx 123 if fx and fx.get_fiat_address_config(): 124 ccy = fx.get_currency() 125 else: 126 ccy = _('Fiat') 127 headers = { 128 self.Columns.TYPE: _('Type'), 129 self.Columns.ADDRESS: _('Address'), 130 self.Columns.LABEL: _('Label'), 131 self.Columns.COIN_BALANCE: _('Balance'), 132 self.Columns.FIAT_BALANCE: ccy + ' ' + _('Balance'), 133 self.Columns.NUM_TXS: _('Tx'), 134 } 135 self.update_headers(headers) 136 137 def toggle_change(self, state: int): 138 if state == self.show_change: 139 return 140 self.show_change = AddressTypeFilter(state) 141 self.update() 142 143 def toggle_used(self, state: int): 144 if state == self.show_used: 145 return 146 self.show_used = AddressUsageStateFilter(state) 147 self.update() 148 149 @profiler 150 def update(self): 151 if self.maybe_defer_update(): 152 return 153 current_address = self.get_role_data_for_current_item(col=self.Columns.LABEL, role=self.ROLE_ADDRESS_STR) 154 if self.show_change == AddressTypeFilter.RECEIVING: 155 addr_list = self.wallet.get_receiving_addresses() 156 elif self.show_change == AddressTypeFilter.CHANGE: 157 addr_list = self.wallet.get_change_addresses() 158 else: 159 addr_list = self.wallet.get_addresses() 160 self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change 161 self.std_model.clear() 162 self.refresh_headers() 163 fx = self.parent.fx 164 set_address = None 165 addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit() 166 for address in addr_list: 167 num = self.wallet.get_address_history_len(address) 168 label = self.wallet.get_label(address) 169 c, u, x = self.wallet.get_addr_balance(address) 170 balance = c + u + x 171 is_used_and_empty = self.wallet.is_used(address) and balance == 0 172 if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty): 173 continue 174 if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0: 175 continue 176 if self.show_used == AddressUsageStateFilter.USED_AND_EMPTY and not is_used_and_empty: 177 continue 178 if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty: 179 continue 180 balance_text = self.parent.format_amount(balance, whitespaces=True) 181 # create item 182 if fx and fx.get_fiat_address_config(): 183 rate = fx.exchange_rate() 184 fiat_balance = fx.value_str(balance, rate) 185 else: 186 fiat_balance = '' 187 labels = ['', address, label, balance_text, fiat_balance, "%d"%num] 188 address_item = [QStandardItem(e) for e in labels] 189 # align text and set fonts 190 for i, item in enumerate(address_item): 191 item.setTextAlignment(Qt.AlignVCenter) 192 if i not in (self.Columns.TYPE, self.Columns.LABEL): 193 item.setFont(QFont(MONOSPACE_FONT)) 194 self.set_editability(address_item) 195 address_item[self.Columns.FIAT_BALANCE].setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) 196 # setup column 0 197 if self.wallet.is_change(address): 198 address_item[self.Columns.TYPE].setText(_('change')) 199 address_item[self.Columns.TYPE].setBackground(ColorScheme.YELLOW.as_color(True)) 200 else: 201 address_item[self.Columns.TYPE].setText(_('receiving')) 202 address_item[self.Columns.TYPE].setBackground(ColorScheme.GREEN.as_color(True)) 203 address_item[self.Columns.LABEL].setData(address, self.ROLE_ADDRESS_STR) 204 address_path = self.wallet.get_address_index(address) 205 address_item[self.Columns.TYPE].setData(address_path, self.ROLE_SORT_ORDER) 206 address_path_str = self.wallet.get_address_path_str(address) 207 if address_path_str is not None: 208 address_item[self.Columns.TYPE].setToolTip(address_path_str) 209 address_item[self.Columns.FIAT_BALANCE].setData(balance, self.ROLE_SORT_ORDER) 210 # setup column 1 211 if self.wallet.is_frozen_address(address): 212 address_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True)) 213 if address in addresses_beyond_gap_limit: 214 address_item[self.Columns.ADDRESS].setBackground(ColorScheme.RED.as_color(True)) 215 # add item 216 count = self.std_model.rowCount() 217 self.std_model.insertRow(count, address_item) 218 address_idx = self.std_model.index(count, self.Columns.LABEL) 219 if address == current_address: 220 set_address = QPersistentModelIndex(address_idx) 221 self.set_current_idx(set_address) 222 # show/hide columns 223 if fx and fx.get_fiat_address_config(): 224 self.showColumn(self.Columns.FIAT_BALANCE) 225 else: 226 self.hideColumn(self.Columns.FIAT_BALANCE) 227 self.filter() 228 self.proxy.setDynamicSortFilter(True) 229 230 def create_menu(self, position): 231 from electrum.wallet import Multisig_Wallet 232 is_multisig = isinstance(self.wallet, Multisig_Wallet) 233 can_delete = self.wallet.can_delete_address() 234 selected = self.selected_in_column(self.Columns.ADDRESS) 235 if not selected: 236 return 237 multi_select = len(selected) > 1 238 addrs = [self.item_from_index(item).text() for item in selected] 239 menu = QMenu() 240 if not multi_select: 241 idx = self.indexAt(position) 242 if not idx.isValid(): 243 return 244 item = self.item_from_index(idx) 245 if not item: 246 return 247 addr = addrs[0] 248 addr_column_title = self.std_model.horizontalHeaderItem(self.Columns.LABEL).text() 249 addr_idx = idx.sibling(idx.row(), self.Columns.LABEL) 250 self.add_copy_menu(menu, idx) 251 menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) 252 persistent = QPersistentModelIndex(addr_idx) 253 menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p))) 254 #menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) 255 if self.wallet.can_export(): 256 menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) 257 if not is_multisig and not self.wallet.is_watching_only(): 258 menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr)) 259 menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr)) 260 if can_delete: 261 menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr)) 262 addr_URL = block_explorer_URL(self.config, 'addr', addr) 263 if addr_URL: 264 menu.addAction(_("View on block explorer"), lambda: webopen(addr_URL)) 265 266 if not self.wallet.is_frozen_address(addr): 267 menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) 268 else: 269 menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], False)) 270 271 coins = self.wallet.get_spendable_coins(addrs) 272 if coins: 273 menu.addAction(_("Spend from"), lambda: self.parent.utxo_list.set_spend_list(coins)) 274 275 run_hook('receive_menu', menu, addrs, self.wallet) 276 menu.exec_(self.viewport().mapToGlobal(position)) 277 278 def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: 279 if is_address(text): 280 try: 281 self.wallet.check_address_for_corruption(text) 282 except InternalAddressCorruption as e: 283 self.parent.show_error(str(e)) 284 raise 285 super().place_text_on_clipboard(text, title=title) 286 287 def get_edit_key_from_coordinate(self, row, col): 288 if col != self.Columns.LABEL: 289 return None 290 return self.get_role_data_from_coordinate(row, col, role=self.ROLE_ADDRESS_STR) 291 292 def on_edited(self, idx, edit_key, *, text): 293 self.parent.wallet.set_label(edit_key, text) 294 self.parent.history_model.refresh('address label edited') 295 self.parent.utxo_list.update() 296 self.parent.update_completions() 297