tutxo_list.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tutxo_list.py (10211B)
---
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 typing import Optional, List, Dict, Sequence, Set
27 from enum import IntEnum
28 import copy
29
30 from PyQt5.QtCore import Qt
31 from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
32 from PyQt5.QtWidgets import QAbstractItemView, QMenu, QLabel, QHBoxLayout
33
34 from electrum.i18n import _
35 from electrum.transaction import PartialTxInput
36
37 from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton
38
39
40 class UTXOList(MyTreeView):
41 _spend_set: Optional[Set[str]] # coins selected by the user to spend from
42 _utxo_dict: Dict[str, PartialTxInput] # coin name -> coin
43
44 class Columns(IntEnum):
45 OUTPOINT = 0
46 ADDRESS = 1
47 LABEL = 2
48 AMOUNT = 3
49 HEIGHT = 4
50
51 headers = {
52 Columns.ADDRESS: _('Address'),
53 Columns.LABEL: _('Label'),
54 Columns.AMOUNT: _('Amount'),
55 Columns.HEIGHT: _('Height'),
56 Columns.OUTPOINT: _('Output point'),
57 }
58 filter_columns = [Columns.ADDRESS, Columns.LABEL, Columns.OUTPOINT]
59 stretch_column = Columns.LABEL
60
61 def __init__(self, parent):
62 super().__init__(parent, self.create_menu,
63 stretch_column=self.stretch_column,
64 editable_columns=[])
65 self._spend_set = None
66 self._utxo_dict = {}
67 self.wallet = self.parent.wallet
68
69 self.setModel(QStandardItemModel(self))
70 self.setSelectionMode(QAbstractItemView.ExtendedSelection)
71 self.setSortingEnabled(True)
72 self.update()
73
74 def update(self):
75 # not calling maybe_defer_update() as it interferes with coincontrol status bar
76 utxos = self.wallet.get_utxos()
77 self._maybe_reset_spend_list(utxos)
78 self._utxo_dict = {}
79 self.model().clear()
80 self.update_headers(self.__class__.headers)
81 for idx, utxo in enumerate(utxos):
82 self.insert_utxo(idx, utxo)
83 self.filter()
84 # update coincontrol status bar
85 if self._spend_set is not None:
86 coins = [self._utxo_dict[x] for x in self._spend_set]
87 coins = self._filter_frozen_coins(coins)
88 amount = sum(x.value_sats() for x in coins)
89 amount_str = self.parent.format_amount_and_units(amount)
90 num_outputs_str = _("{} outputs available ({} total)").format(len(coins), len(utxos))
91 self.parent.set_coincontrol_msg(_("Coin control active") + f': {num_outputs_str}, {amount_str}')
92 else:
93 self.parent.set_coincontrol_msg(None)
94
95 def insert_utxo(self, idx, utxo: PartialTxInput):
96 address = utxo.address
97 height = utxo.block_height
98 name = utxo.prevout.to_str()
99 name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx
100 self._utxo_dict[name] = utxo
101 label = self.wallet.get_label_for_txid(utxo.prevout.txid.hex()) or self.wallet.get_label(address)
102 amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True)
103 labels = [name_short, address, label, amount, '%d'%height]
104 utxo_item = [QStandardItem(x) for x in labels]
105 self.set_editability(utxo_item)
106 utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA)
107 utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT))
108 utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT))
109 utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT))
110 utxo_item[self.Columns.ADDRESS].setData(name, Qt.UserRole)
111 SELECTED_TO_SPEND_TOOLTIP = _('Coin selected to be spent')
112 if name in (self._spend_set or set()):
113 for col in utxo_item:
114 col.setBackground(ColorScheme.GREEN.as_color(True))
115 if col != self.Columns.OUTPOINT:
116 col.setToolTip(SELECTED_TO_SPEND_TOOLTIP)
117 if self.wallet.is_frozen_address(address):
118 utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
119 utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))
120 if self.wallet.is_frozen_coin(utxo):
121 utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True))
122 utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
123 else:
124 tooltip = ("\n" + SELECTED_TO_SPEND_TOOLTIP) if name in (self._spend_set or set()) else ""
125 utxo_item[self.Columns.OUTPOINT].setToolTip(name + tooltip)
126 self.model().insertRow(idx, utxo_item)
127
128 def get_selected_outpoints(self) -> Optional[List[str]]:
129 if not self.model():
130 return None
131 items = self.selected_in_column(self.Columns.ADDRESS)
132 return [x.data(Qt.UserRole) for x in items]
133
134 def _filter_frozen_coins(self, coins: List[PartialTxInput]) -> List[PartialTxInput]:
135 coins = [utxo for utxo in coins
136 if (not self.wallet.is_frozen_address(utxo.address) and
137 not self.wallet.is_frozen_coin(utxo))]
138 return coins
139
140 def set_spend_list(self, coins: Optional[List[PartialTxInput]]):
141 if coins is not None:
142 coins = self._filter_frozen_coins(coins)
143 self._spend_set = {utxo.prevout.to_str() for utxo in coins}
144 else:
145 self._spend_set = None
146 self.update()
147
148 def get_spend_list(self) -> Optional[Sequence[PartialTxInput]]:
149 if self._spend_set is None:
150 return None
151 utxos = [self._utxo_dict[x] for x in self._spend_set]
152 return copy.deepcopy(utxos) # copy so that side-effects don't affect utxo_dict
153
154 def _maybe_reset_spend_list(self, current_wallet_utxos: Sequence[PartialTxInput]) -> None:
155 if self._spend_set is None:
156 return
157 # if we spent one of the selected UTXOs, just reset selection
158 utxo_set = {utxo.prevout.to_str() for utxo in current_wallet_utxos}
159 if not all([prevout_str in utxo_set for prevout_str in self._spend_set]):
160 self._spend_set = None
161
162 def create_menu(self, position):
163 selected = self.get_selected_outpoints()
164 if selected is None:
165 return
166 menu = QMenu()
167 menu.setSeparatorsCollapsible(True) # consecutive separators are merged together
168 coins = [self._utxo_dict[name] for name in selected]
169 if len(coins) == 0:
170 menu.addAction(_("Spend (select none)"), lambda: self.set_spend_list(coins))
171 else:
172 menu.addAction(_("Spend"), lambda: self.set_spend_list(coins))
173
174 if len(coins) == 1:
175 utxo = coins[0]
176 addr = utxo.address
177 txid = utxo.prevout.txid.hex()
178 # "Details"
179 tx = self.wallet.db.get_transaction(txid)
180 if tx:
181 label = self.wallet.get_label_for_txid(txid)
182 menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label))
183 # "Copy ..."
184 idx = self.indexAt(position)
185 if not idx.isValid():
186 return
187 self.add_copy_menu(menu, idx)
188 # "Freeze coin"
189 if not self.wallet.is_frozen_coin(utxo):
190 menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True))
191 else:
192 menu.addSeparator()
193 menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False)
194 menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False))
195 menu.addSeparator()
196 # "Freeze address"
197 if not self.wallet.is_frozen_address(addr):
198 menu.addAction(_("Freeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], True))
199 else:
200 menu.addSeparator()
201 menu.addAction(_("Address is frozen"), lambda: None).setEnabled(False)
202 menu.addAction(_("Unfreeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], False))
203 menu.addSeparator()
204 elif len(coins) > 1: # multiple items selected
205 menu.addSeparator()
206 addrs = [utxo.address for utxo in coins]
207 is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins]
208 is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins]
209 if not all(is_coin_frozen):
210 menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True))
211 if any(is_coin_frozen):
212 menu.addAction(_("Unfreeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, False))
213 if not all(is_addr_frozen):
214 menu.addAction(_("Freeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, True))
215 if any(is_addr_frozen):
216 menu.addAction(_("Unfreeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, False))
217
218 menu.exec_(self.viewport().mapToGlobal(position))