electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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)