|
|
|
|
@ -1,7 +1,6 @@
|
|
|
|
|
import itertools |
|
|
|
|
from typing import TYPE_CHECKING |
|
|
|
|
from typing import TYPE_CHECKING, List |
|
|
|
|
|
|
|
|
|
from PyQt5.QtCore import pyqtSlot |
|
|
|
|
from PyQt5.QtCore import pyqtSlot, QSortFilterProxyModel, pyqtSignal, pyqtProperty |
|
|
|
|
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex |
|
|
|
|
|
|
|
|
|
from electrum.logging import get_logger |
|
|
|
|
@ -11,38 +10,126 @@ from .qetypes import QEAmount
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING: |
|
|
|
|
from electrum.wallet import Abstract_Wallet |
|
|
|
|
from electrum.transaction import PartialTxInput |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QEAddressListModel(QAbstractListModel): |
|
|
|
|
class QEAddressCoinFilterProxyModel(QSortFilterProxyModel): |
|
|
|
|
_logger = get_logger(__name__) |
|
|
|
|
|
|
|
|
|
def __init__(self, parent_model, parent=None): |
|
|
|
|
super().__init__(parent) |
|
|
|
|
self._filter_text = None |
|
|
|
|
self._show_coins = True |
|
|
|
|
self._show_addresses = True |
|
|
|
|
self._show_used = False |
|
|
|
|
self._parent_model = parent_model |
|
|
|
|
self.setSourceModel(parent_model) |
|
|
|
|
|
|
|
|
|
countChanged = pyqtSignal() |
|
|
|
|
@pyqtProperty(int, notify=countChanged) |
|
|
|
|
def count(self): |
|
|
|
|
return self.rowCount(QModelIndex()) |
|
|
|
|
|
|
|
|
|
def filterAcceptsRow(self, s_row, s_parent): |
|
|
|
|
parent_model = self.sourceModel() |
|
|
|
|
addridx = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['addridx']) |
|
|
|
|
if addridx is None: # coin |
|
|
|
|
if not self._show_coins: |
|
|
|
|
return False |
|
|
|
|
else: |
|
|
|
|
if not self._show_addresses: |
|
|
|
|
return False |
|
|
|
|
balance = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['balance']) |
|
|
|
|
numtx = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['numtx']) |
|
|
|
|
if balance.isEmpty and numtx and not self._show_used: |
|
|
|
|
return False |
|
|
|
|
if self._filter_text: |
|
|
|
|
label = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['label']) |
|
|
|
|
address = parent_model.data(parent_model.index(s_row, 0, s_parent), parent_model._ROLE_RMAP['address']) |
|
|
|
|
for item in [label, address]: |
|
|
|
|
if self._filter_text in str(item): |
|
|
|
|
return True |
|
|
|
|
return False |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
showAddressesCoinsChanged = pyqtSignal() |
|
|
|
|
@pyqtProperty(int, notify=showAddressesCoinsChanged) |
|
|
|
|
def showAddressesCoins(self) -> int: |
|
|
|
|
result = 0 |
|
|
|
|
if self._show_addresses: |
|
|
|
|
result += 1 |
|
|
|
|
if self._show_coins: |
|
|
|
|
result += 2 |
|
|
|
|
return result |
|
|
|
|
|
|
|
|
|
@showAddressesCoins.setter |
|
|
|
|
def showAddressesCoins(self, show_addresses_coins: int): |
|
|
|
|
show_addresses = show_addresses_coins in [1, 3] |
|
|
|
|
show_coins = show_addresses_coins in [2, 3] |
|
|
|
|
|
|
|
|
|
if self._show_addresses != show_addresses or self._show_coins != show_coins: |
|
|
|
|
self._show_addresses = show_addresses |
|
|
|
|
self._show_coins = show_coins |
|
|
|
|
self.invalidateFilter() |
|
|
|
|
self.showAddressesCoinsChanged.emit() |
|
|
|
|
|
|
|
|
|
showUsedChanged = pyqtSignal() |
|
|
|
|
@pyqtProperty(bool, notify=showUsedChanged) |
|
|
|
|
def showUsed(self) -> bool: |
|
|
|
|
return self._show_used |
|
|
|
|
|
|
|
|
|
@showUsed.setter |
|
|
|
|
def showUsed(self, show_used: bool): |
|
|
|
|
if self._show_used != show_used: |
|
|
|
|
self._show_used = show_used |
|
|
|
|
self.invalidateFilter() |
|
|
|
|
self.showUsedChanged.emit() |
|
|
|
|
|
|
|
|
|
filterTextChanged = pyqtSignal() |
|
|
|
|
@pyqtProperty(str, notify=filterTextChanged) |
|
|
|
|
def filterText(self) -> str: |
|
|
|
|
return self._filter_text |
|
|
|
|
|
|
|
|
|
@filterText.setter |
|
|
|
|
def filterText(self, filter_text: str): |
|
|
|
|
if self._filter_text != filter_text: |
|
|
|
|
self._filter_text = filter_text |
|
|
|
|
self.invalidateFilter() |
|
|
|
|
self.filterTextChanged.emit() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QEAddressCoinListModel(QAbstractListModel): |
|
|
|
|
_logger = get_logger(__name__) |
|
|
|
|
|
|
|
|
|
# define listmodel rolemap |
|
|
|
|
_ROLE_NAMES=('type', 'iaddr', 'address', 'label', 'balance', 'numtx', 'held') |
|
|
|
|
_ROLE_NAMES=('type', 'addridx', 'address', 'label', 'balance', 'numtx', 'held', 'height', 'amount', 'outpoint', |
|
|
|
|
'short_outpoint', 'short_id', 'txid') |
|
|
|
|
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) |
|
|
|
|
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) |
|
|
|
|
_ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS)) |
|
|
|
|
|
|
|
|
|
def __init__(self, wallet: 'Abstract_Wallet', parent=None): |
|
|
|
|
super().__init__(parent) |
|
|
|
|
self.wallet = wallet |
|
|
|
|
self._receive_addresses = [] |
|
|
|
|
self._change_addresses = [] |
|
|
|
|
self._items = [] |
|
|
|
|
self._filterModel = None |
|
|
|
|
|
|
|
|
|
self._dirty = True |
|
|
|
|
self.initModel() |
|
|
|
|
|
|
|
|
|
def rowCount(self, index): |
|
|
|
|
return len(self._receive_addresses) + len(self._change_addresses) |
|
|
|
|
return len(self._items) |
|
|
|
|
|
|
|
|
|
def roleNames(self): |
|
|
|
|
return self._ROLE_MAP |
|
|
|
|
|
|
|
|
|
def data(self, index, role): |
|
|
|
|
if index.row() > len(self._receive_addresses) - 1: |
|
|
|
|
address = self._change_addresses[index.row() - len(self._receive_addresses)] |
|
|
|
|
else: |
|
|
|
|
address = self._receive_addresses[index.row()] |
|
|
|
|
address = self._items[index.row()] |
|
|
|
|
role_index = role - Qt.UserRole |
|
|
|
|
value = address[self._ROLE_NAMES[role_index]] |
|
|
|
|
try: |
|
|
|
|
value = address[self._ROLE_NAMES[role_index]] |
|
|
|
|
except KeyError: |
|
|
|
|
return None |
|
|
|
|
if isinstance(value, (bool, list, int, str, QEAmount)) or value is None: |
|
|
|
|
return value |
|
|
|
|
if isinstance(value, Satoshis): |
|
|
|
|
@ -51,13 +138,14 @@ class QEAddressListModel(QAbstractListModel):
|
|
|
|
|
|
|
|
|
|
def clear(self): |
|
|
|
|
self.beginResetModel() |
|
|
|
|
self._receive_addresses = [] |
|
|
|
|
self._change_addresses = [] |
|
|
|
|
self._items = [] |
|
|
|
|
self.endResetModel() |
|
|
|
|
|
|
|
|
|
def addr_to_model(self, address): |
|
|
|
|
def addr_to_model(self, addrtype: str, addridx: int, address: str): |
|
|
|
|
c, u, x = self.wallet.get_addr_balance(address) |
|
|
|
|
item = { |
|
|
|
|
'type': addrtype, |
|
|
|
|
'addridx': addridx, |
|
|
|
|
'address': address, |
|
|
|
|
'numtx': self.wallet.adb.get_address_history_len(address), |
|
|
|
|
'label': self.wallet.get_label_for_address(address), |
|
|
|
|
@ -66,6 +154,27 @@ class QEAddressListModel(QAbstractListModel):
|
|
|
|
|
} |
|
|
|
|
return item |
|
|
|
|
|
|
|
|
|
def coin_to_model(self, addrtype: str, coin: 'PartialTxInput'): |
|
|
|
|
txid = coin.prevout.txid.hex() |
|
|
|
|
short_id = '' |
|
|
|
|
# check below duplicated from TxInput as we cannot get short_id unambiguously |
|
|
|
|
if coin.block_txpos is not None and coin.block_txpos >= 0: |
|
|
|
|
short_id = str(coin.short_id) |
|
|
|
|
item = { |
|
|
|
|
'type': addrtype, |
|
|
|
|
'amount': QEAmount(amount_sat=coin.value_sats()), |
|
|
|
|
'address': coin.address, |
|
|
|
|
'height': coin.block_height, |
|
|
|
|
'outpoint': coin.prevout.to_str(), |
|
|
|
|
'short_outpoint': coin.prevout.short_name(), |
|
|
|
|
'short_id': short_id, |
|
|
|
|
'txid': txid, |
|
|
|
|
'label': self.wallet.get_label_for_txid(txid) or '', |
|
|
|
|
'held': self.wallet.is_frozen_coin(coin), |
|
|
|
|
'coin': coin |
|
|
|
|
} |
|
|
|
|
return item |
|
|
|
|
|
|
|
|
|
@pyqtSlot() |
|
|
|
|
def setDirty(self): |
|
|
|
|
self._dirty = True |
|
|
|
|
@ -80,36 +189,70 @@ class QEAddressListModel(QAbstractListModel):
|
|
|
|
|
c_addresses = self.wallet.get_change_addresses() if self.wallet.wallet_type != 'imported' else [] |
|
|
|
|
n_addresses = len(r_addresses) + len(c_addresses) |
|
|
|
|
|
|
|
|
|
def insert_row(atype, alist, address, iaddr): |
|
|
|
|
item = self.addr_to_model(address) |
|
|
|
|
item['type'] = atype |
|
|
|
|
item['iaddr'] = iaddr |
|
|
|
|
alist.append(item) |
|
|
|
|
def insert_address(atype, address, addridx): |
|
|
|
|
item = self.addr_to_model(atype, addridx, address) |
|
|
|
|
self._items.append(item) |
|
|
|
|
|
|
|
|
|
utxos = self.wallet.get_utxos([address]) |
|
|
|
|
utxos.sort(key=lambda x: x.block_height) |
|
|
|
|
for i, coin in enumerate(utxos): |
|
|
|
|
self._items.append(self.coin_to_model(atype, coin)) |
|
|
|
|
|
|
|
|
|
self.clear() |
|
|
|
|
self.beginInsertRows(QModelIndex(), 0, n_addresses - 1) |
|
|
|
|
if self.wallet.wallet_type != 'imported': |
|
|
|
|
for i, address in enumerate(r_addresses): |
|
|
|
|
insert_row('receive', self._receive_addresses, address, i) |
|
|
|
|
insert_address('receive', address, i) |
|
|
|
|
for i, address in enumerate(c_addresses): |
|
|
|
|
insert_row('change', self._change_addresses, address, i) |
|
|
|
|
insert_address('change', address, i) |
|
|
|
|
else: |
|
|
|
|
for i, address in enumerate(r_addresses): |
|
|
|
|
insert_row('imported', self._receive_addresses, address, i) |
|
|
|
|
insert_address('imported', address, i) |
|
|
|
|
self.endInsertRows() |
|
|
|
|
|
|
|
|
|
self._dirty = False |
|
|
|
|
|
|
|
|
|
@pyqtSlot(str) |
|
|
|
|
def updateAddress(self, address): |
|
|
|
|
for i, a in enumerate(itertools.chain(self._receive_addresses, self._change_addresses)): |
|
|
|
|
for i, a in enumerate(self._items): |
|
|
|
|
if a['address'] == address: |
|
|
|
|
self.do_update(i, a) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
def updateCoin(self, outpoint): |
|
|
|
|
for i, a in enumerate(self._items): |
|
|
|
|
if a.get('outpoint') == outpoint: |
|
|
|
|
self.do_update(i, a) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
def do_update(self, modelindex, modelitem): |
|
|
|
|
mi = self.createIndex(modelindex, 0) |
|
|
|
|
self._logger.debug(repr(modelitem)) |
|
|
|
|
modelitem.update(self.addr_to_model(modelitem['address'])) |
|
|
|
|
if modelitem.get('outpoint'): |
|
|
|
|
modelitem.update(self.coin_to_model(modelitem['type'], modelitem['coin'])) |
|
|
|
|
else: |
|
|
|
|
modelitem.update(self.addr_to_model(modelitem['type'], modelitem['addridx'], modelitem['address'])) |
|
|
|
|
self._logger.debug(repr(modelitem)) |
|
|
|
|
self.dataChanged.emit(mi, mi, self._ROLE_KEYS) |
|
|
|
|
|
|
|
|
|
filterModelChanged = pyqtSignal() |
|
|
|
|
@pyqtProperty(QEAddressCoinFilterProxyModel, notify=filterModelChanged) |
|
|
|
|
def filterModel(self): |
|
|
|
|
if self._filterModel is None: |
|
|
|
|
self._filterModel = QEAddressCoinFilterProxyModel(self) |
|
|
|
|
return self._filterModel |
|
|
|
|
|
|
|
|
|
@pyqtSlot(bool, list) |
|
|
|
|
def setFrozenForItems(self, freeze: bool, items: List[str]): |
|
|
|
|
self._logger.debug(f'set frozen to {freeze} for {items!r}') |
|
|
|
|
coins = list(filter(lambda x: ':' in x, items)) |
|
|
|
|
if len(coins): |
|
|
|
|
self.wallet.set_frozen_state_of_coins(coins, freeze) |
|
|
|
|
for coin in coins: |
|
|
|
|
self.updateCoin(coin) |
|
|
|
|
addresses = list(filter(lambda x: ':' not in x, items)) |
|
|
|
|
if len(addresses): |
|
|
|
|
self.wallet.set_frozen_state_of_addresses(addresses, freeze) |
|
|
|
|
for address in addresses: |
|
|
|
|
self.updateAddress(address) |
|
|
|
|
|
|
|
|
|
|