Browse Source

Merge pull request #5152 from SomberNight/freeze_individual_utxos

Freeze individual UTXOs
regtest_lnd
ghost43 6 years ago
committed by GitHub
parent
commit
52af40685e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 42
      electrum/address_synchronizer.py
  2. 6
      electrum/commands.py
  3. 10
      electrum/gui/qt/address_list.py
  4. 15
      electrum/gui/qt/main_window.py
  5. 1
      electrum/gui/qt/util.py
  6. 76
      electrum/gui/qt/utxo_list.py
  7. 57
      electrum/wallet.py

42
electrum/address_synchronizer.py

@ -25,7 +25,7 @@ import threading
import asyncio
import itertools
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
from . import bitcoin
from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
@ -715,17 +715,23 @@ class AddressSynchronizer(PrintError):
return sum([v for height, v, is_cb in received.values()])
@with_local_height_cached
def get_addr_balance(self, address):
def get_addr_balance(self, address, *, excluded_coins: Set[str] = None):
"""Return the balance of a bitcoin address:
confirmed and matured, unconfirmed, unmatured
"""
cached_value = self._get_addr_balance_cache.get(address)
if cached_value:
return cached_value
if not excluded_coins: # cache is only used if there are no excluded_coins
cached_value = self._get_addr_balance_cache.get(address)
if cached_value:
return cached_value
if excluded_coins is None:
excluded_coins = set()
assert isinstance(excluded_coins, set), f"excluded_coins should be set, not {type(excluded_coins)}"
received, sent = self.get_addr_io(address)
c = u = x = 0
local_height = self.get_local_height()
for txo, (tx_height, v, is_cb) in received.items():
if txo in excluded_coins:
continue
if is_cb and tx_height + COINBASE_MATURITY > local_height:
x += v
elif tx_height > 0:
@ -739,19 +745,21 @@ class AddressSynchronizer(PrintError):
u -= v
result = c, u, x
# cache result.
# Cache needs to be invalidated if a transaction is added to/
# removed from history; or on new blocks (maturity...)
self._get_addr_balance_cache[address] = result
if not excluded_coins:
# Cache needs to be invalidated if a transaction is added to/
# removed from history; or on new blocks (maturity...)
self._get_addr_balance_cache[address] = result
return result
@with_local_height_cached
def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False, nonlocal_only=False):
def get_utxos(self, domain=None, *, excluded_addresses=None,
mature_only: bool = False, confirmed_only: bool = False, nonlocal_only: bool = False):
coins = []
if domain is None:
domain = self.get_addresses()
domain = set(domain)
if excluded:
domain = set(domain) - excluded
if excluded_addresses:
domain = set(domain) - set(excluded_addresses)
for addr in domain:
utxos = self.get_addr_utxo(addr)
for x in utxos.values():
@ -759,19 +767,23 @@ class AddressSynchronizer(PrintError):
continue
if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL:
continue
if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height():
if mature_only and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height():
continue
coins.append(x)
continue
return coins
def get_balance(self, domain=None):
def get_balance(self, domain=None, *, excluded_addresses: Set[str] = None,
excluded_coins: Set[str] = None) -> Tuple[int, int, int]:
if domain is None:
domain = self.get_addresses()
domain = set(domain)
if excluded_addresses is None:
excluded_addresses = set()
assert isinstance(excluded_addresses, set), f"excluded_addresses should be set, not {type(excluded_addresses)}"
domain = set(domain) - excluded_addresses
cc = uu = xx = 0
for addr in domain:
c, u, x = self.get_addr_balance(addr)
c, u, x = self.get_addr_balance(addr, excluded_coins=excluded_coins)
cc += c
uu += u
xx += x

6
electrum/commands.py

@ -309,12 +309,12 @@ class Commands:
@command('w')
def freeze(self, address):
"""Freeze address. Freeze the funds at one of your wallet\'s addresses"""
return self.wallet.set_frozen_state([address], True)
return self.wallet.set_frozen_state_of_addresses([address], True)
@command('w')
def unfreeze(self, address):
"""Unfreeze address. Unfreeze the funds at one of your wallet\'s address"""
return self.wallet.set_frozen_state([address], False)
return self.wallet.set_frozen_state_of_addresses([address], False)
@command('wp')
def getprivatekeys(self, address, password=None):
@ -547,7 +547,7 @@ class Commands:
"""List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
out = []
for addr in self.wallet.get_addresses():
if frozen and not self.wallet.is_frozen(addr):
if frozen and not self.wallet.is_frozen_address(addr):
continue
if receiving and self.wallet.is_change(addr):
continue

10
electrum/gui/qt/address_list.py

@ -157,7 +157,7 @@ class AddressList(MyTreeView):
address_item[self.Columns.TYPE].setBackground(ColorScheme.GREEN.as_color(True))
address_item[self.Columns.LABEL].setData(address, Qt.UserRole)
# setup column 1
if self.wallet.is_frozen(address):
if self.wallet.is_frozen_address(address):
address_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
if self.wallet.is_beyond_limit(address):
address_item[self.Columns.ADDRESS].setBackground(ColorScheme.RED.as_color(True))
@ -213,12 +213,12 @@ class AddressList(MyTreeView):
if addr_URL:
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(addr_URL))
if not self.wallet.is_frozen(addr):
menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state([addr], True))
if not self.wallet.is_frozen_address(addr):
menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], True))
else:
menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state([addr], False))
menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], False))
coins = self.wallet.get_utxos(addrs)
coins = self.wallet.get_spendable_coins(addrs, config=self.config)
if coins:
menu.addAction(_("Spend from"), lambda: self.parent.spend_coins(coins))

15
electrum/gui/qt/main_window.py

@ -1314,10 +1314,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.not_enough_funds:
amt_color, fee_color = ColorScheme.RED, ColorScheme.RED
feerate_color = ColorScheme.RED
text = _( "Not enough funds" )
text = _("Not enough funds")
c, u, x = self.wallet.get_frozen_balance()
if c+u+x:
text += ' (' + self.format_amount(c+u+x).strip() + ' ' + self.base_unit() + ' ' +_("are frozen") + ')'
text += " ({} {} {})".format(
self.format_amount(c + u + x).strip(), self.base_unit(), _("are frozen")
)
# blue color denotes auto-filled values
elif self.fee_e.isModified():
@ -1850,12 +1852,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.update_status()
run_hook('do_clear', self)
def set_frozen_state(self, addrs, freeze):
self.wallet.set_frozen_state(addrs, freeze)
def set_frozen_state_of_addresses(self, addrs, freeze: bool):
self.wallet.set_frozen_state_of_addresses(addrs, freeze)
self.address_list.update()
self.utxo_list.update()
self.update_fee()
def set_frozen_state_of_coins(self, utxos, freeze: bool):
self.wallet.set_frozen_state_of_coins(utxos, freeze)
self.utxo_list.update()
self.update_fee()
def create_list_tab(self, l, toolbar=None):
w = QWidget()
w.searchable_list = l

1
electrum/gui/qt/util.py

@ -715,6 +715,7 @@ class ColorScheme:
YELLOW = ColorSchemeItem("#897b2a", "#ffff00")
RED = ColorSchemeItem("#7c1111", "#f18c8c")
BLUE = ColorSchemeItem("#123b7c", "#8cb3f2")
PURPLE = ColorSchemeItem("#8A2BE2", "#8A2BE2")
DEFAULT = ColorSchemeItem("black", "white")
@staticmethod

76
electrum/gui/qt/utxo_list.py

@ -37,11 +37,11 @@ from .util import MyTreeView, ColorScheme, MONOSPACE_FONT
class UTXOList(MyTreeView):
class Columns(IntEnum):
ADDRESS = 0
LABEL = 1
AMOUNT = 2
HEIGHT = 3
OUTPOINT = 4
OUTPOINT = 0
ADDRESS = 1
LABEL = 2
AMOUNT = 3
HEIGHT = 4
headers = {
Columns.ADDRESS: _('Address'),
@ -71,26 +71,31 @@ class UTXOList(MyTreeView):
self.insert_utxo(idx, x)
def insert_utxo(self, idx, x):
address = x.get('address')
address = x['address']
height = x.get('height')
name = x.get('prevout_hash') + ":%d"%x.get('prevout_n')
name_short = x.get('prevout_hash')[:10] + '...' + ":%d"%x.get('prevout_n')
name_short = x.get('prevout_hash')[:16] + '...' + ":%d"%x.get('prevout_n')
self.utxo_dict[name] = x
label = self.wallet.get_label(x.get('prevout_hash'))
amount = self.parent.format_amount(x['value'], whitespaces=True)
labels = [address, label, amount, '%d'%height, name_short]
labels = [name_short, address, label, amount, '%d'%height]
utxo_item = [QStandardItem(x) for x in labels]
self.set_editability(utxo_item)
utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT))
utxo_item[self.Columns.ADDRESS].setData(name, Qt.UserRole)
utxo_item[self.Columns.OUTPOINT].setToolTip(name)
if self.wallet.is_frozen(address):
if self.wallet.is_frozen_address(address):
utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))
if self.wallet.is_frozen_coin(x):
utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
else:
utxo_item[self.Columns.OUTPOINT].setToolTip(name)
self.model().insertRow(idx, utxo_item)
def selected_column_0_user_roles(self) -> Optional[List[str]]:
def get_selected_outpoints(self) -> Optional[List[str]]:
if not self.model():
return None
items = self.selected_in_column(self.Columns.ADDRESS)
@ -99,17 +104,58 @@ class UTXOList(MyTreeView):
return [x.data(Qt.UserRole) for x in items]
def create_menu(self, position):
selected = self.selected_column_0_user_roles()
selected = self.get_selected_outpoints()
if not selected:
return
menu = QMenu()
coins = (self.utxo_dict[name] for name in selected)
menu.setSeparatorsCollapsible(True) # consecutive separators are merged together
coins = [self.utxo_dict[name] for name in selected]
menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
if len(selected) == 1:
txid = selected[0].split(':')[0]
assert len(coins) >= 1, len(coins)
if len(coins) == 1:
utxo_dict = coins[0]
addr = utxo_dict['address']
txid = utxo_dict['prevout_hash']
# "Details"
tx = self.wallet.db.get_transaction(txid)
if tx:
label = self.wallet.get_label(txid) or None # Prefer None if empty (None hides the Description: field in the window)
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label))
# "Copy ..."
idx = self.indexAt(position)
col = idx.column()
column_title = self.model().horizontalHeaderItem(col).text()
copy_text = self.model().itemFromIndex(idx).text() if col != self.Columns.OUTPOINT else selected[0]
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(copy_text))
# "Freeze coin"
if not self.wallet.is_frozen_coin(utxo_dict):
menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], True))
else:
menu.addSeparator()
menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False)
menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], False))
menu.addSeparator()
# "Freeze address"
if not self.wallet.is_frozen_address(addr):
menu.addAction(_("Freeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], True))
else:
menu.addSeparator()
menu.addAction(_("Address is frozen"), lambda: None).setEnabled(False)
menu.addAction(_("Unfreeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], False))
menu.addSeparator()
else:
# multiple items selected
menu.addSeparator()
addrs = [utxo_dict['address'] for utxo_dict in coins]
is_coin_frozen = [self.wallet.is_frozen_coin(utxo_dict) for utxo_dict in coins]
is_addr_frozen = [self.wallet.is_frozen_address(utxo_dict['address']) for utxo_dict in coins]
if not all(is_coin_frozen):
menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True))
if any(is_coin_frozen):
menu.addAction(_("Unfreeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, False))
if not all(is_addr_frozen):
menu.addAction(_("Freeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, True))
if any(is_addr_frozen):
menu.addAction(_("Unfreeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, False))
menu.exec_(self.viewport().mapToGlobal(position))

57
electrum/wallet.py

@ -38,7 +38,7 @@ import traceback
from functools import partial
from numbers import Number
from decimal import Decimal
from typing import TYPE_CHECKING, List, Optional, Tuple
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from .i18n import _
from .util import (NotEnoughFunds, PrintError, UserCancelled, profiler,
@ -204,7 +204,8 @@ class Abstract_Wallet(AddressSynchronizer):
self.use_change = storage.get('use_change', True)
self.multiple_change = storage.get('multiple_change', False)
self.labels = storage.get('labels', {})
self.frozen_addresses = set(storage.get('frozen_addresses',[]))
self.frozen_addresses = set(storage.get('frozen_addresses', []))
self.frozen_coins = set(storage.get('frozen_coins', [])) # set of txid:vout strings
self.fiat_value = storage.get('fiat_value', {})
self.receive_requests = storage.get('payment_requests', {})
@ -395,17 +396,24 @@ class Abstract_Wallet(AddressSynchronizer):
def get_spendable_coins(self, domain, config, *, nonlocal_only=False):
confirmed_only = config.get('confirmed_only', False)
return self.get_utxos(domain,
excluded=self.frozen_addresses,
mature=True,
confirmed_only=confirmed_only,
nonlocal_only=nonlocal_only)
utxos = self.get_utxos(domain,
excluded_addresses=self.frozen_addresses,
mature_only=True,
confirmed_only=confirmed_only,
nonlocal_only=nonlocal_only)
utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
return utxos
def dummy_address(self):
return self.get_receiving_addresses()[0]
def get_frozen_balance(self):
return self.get_balance(self.frozen_addresses)
if not self.frozen_coins: # shortcut
return self.get_balance(self.frozen_addresses)
c1, u1, x1 = self.get_balance()
c2, u2, x2 = self.get_balance(excluded_addresses=self.frozen_addresses,
excluded_coins=self.frozen_coins)
return c1-c2, u1-u2, x1-x2
def balance_at_timestamp(self, domain, target_timestamp):
h = self.get_history(domain)
@ -737,12 +745,18 @@ class Abstract_Wallet(AddressSynchronizer):
self.sign_transaction(tx, password)
return tx
def is_frozen(self, addr):
def is_frozen_address(self, addr: str) -> bool:
return addr in self.frozen_addresses
def set_frozen_state(self, addrs, freeze):
'''Set frozen state of the addresses to FREEZE, True or False'''
def is_frozen_coin(self, utxo) -> bool:
# utxo is either a txid:vout str, or a dict
utxo = self._utxo_str_from_utxo(utxo)
return utxo in self.frozen_coins
def set_frozen_state_of_addresses(self, addrs, freeze: bool):
"""Set frozen state of the addresses to FREEZE, True or False"""
if all(self.is_mine(addr) for addr in addrs):
# FIXME take lock?
if freeze:
self.frozen_addresses |= set(addrs)
else:
@ -751,6 +765,25 @@ class Abstract_Wallet(AddressSynchronizer):
return True
return False
def set_frozen_state_of_coins(self, utxos, freeze: bool):
"""Set frozen state of the utxos to FREEZE, True or False"""
utxos = {self._utxo_str_from_utxo(utxo) for utxo in utxos}
# FIXME take lock?
if freeze:
self.frozen_coins |= set(utxos)
else:
self.frozen_coins -= set(utxos)
self.storage.put('frozen_coins', list(self.frozen_coins))
@staticmethod
def _utxo_str_from_utxo(utxo: Union[dict, str]) -> str:
"""Return a txid:vout str"""
if isinstance(utxo, dict):
return "{}:{}".format(utxo['prevout_hash'], utxo['prevout_n'])
assert isinstance(utxo, str), f"utxo should be a str, not {type(utxo)}"
# just assume it is already of the correct format
return utxo
def wait_until_synchronized(self, callback=None):
def wait_for_wallet():
self.set_up_to_date(False)
@ -1401,7 +1434,7 @@ class Imported_Wallet(Simple_Wallet):
self.db.remove_transaction(tx_hash)
self.set_label(address, None)
self.remove_payment_request(address, {})
self.set_frozen_state([address], False)
self.set_frozen_state_of_addresses([address], False)
pubkey = self.get_public_key(address)
self.db.remove_imported_address(address)
if pubkey:

Loading…
Cancel
Save