taddress_list.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
taddress_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)