Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions electrum/gui/qt/address_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,51 @@ def update(self):
addr_list = self.wallet.get_change_addresses()
else:
addr_list = self.wallet.get_addresses()
should_show_fiat = self.should_show_fiat()
fiat_ccy = self.main_window.fx.get_currency() if should_show_fiat else None
fiat_rate = self.main_window.fx.exchange_rate() if should_show_fiat else None
self.addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit()
num_shown = 0
new_address_list_status = hash((
self.show_change,
self.show_used,
should_show_fiat,
fiat_ccy,
fiat_rate,
))
for address in addr_list:
c, u, x = self.wallet.get_addr_balance(address)
balance = c + u + x
is_used_and_empty = self.wallet.adb.is_used(address) and balance == 0
if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty):
continue
if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0:
continue
if self.show_used == AddressUsageStateFilter.USED_AND_EMPTY and not is_used_and_empty:
continue
if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty:
continue
num_shown += 1
new_address_list_status = hash((
new_address_list_status,
address,
c,
u,
x,
is_used_and_empty,
self.wallet.get_label_for_address(address),
self.wallet.adb.get_address_history_len(address),
self.wallet.is_change(address),
self.wallet.is_frozen_address(address),
address in self.addresses_beyond_gap_limit,
))
if (self._address_list_status == new_address_list_status
and self.std_model.rowCount() == num_shown):
return
self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change
self.std_model.clear()
self.refresh_headers()
set_address = None
num_shown = 0
new_address_list_status = 0
self.addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit()
for address in addr_list:
c, u, x = self.wallet.get_addr_balance(address)
balance = c + u + x
Expand All @@ -203,8 +241,6 @@ def update(self):
continue
if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty:
continue
num_shown += 1
new_address_list_status = hash((new_address_list_status, address, c, u, x, is_used_and_empty))
labels = [""] * len(self.Columns)
labels[self.Columns.ADDRESS] = address
address_item = [QStandardItem(e) for e in labels]
Expand Down
21 changes: 14 additions & 7 deletions electrum/gui/qt/history_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,17 @@ def refresh(self, reason: str):
onchain_domain=self.get_domain(),
include_lightning=self.should_include_lightning_payments(),
)
balance = 0
for tx_item in transactions.values():
balance += tx_item['value'].value
tx_item['balance'] = Satoshis(balance)
old_length = self._root.childCount()
if old_length == len(transactions) and transactions == self.transactions:
if self.view:
self.view.num_tx_label.setText(_("{} transactions").format(len(transactions)))
return
if old_length != 0:
self.beginRemoveRows(QModelIndex(), 0, old_length)
self.beginRemoveRows(QModelIndex(), 0, old_length - 1)
self.transactions.clear()
self._root = HistoryNode(self, None)
self.endRemoveRows()
Expand All @@ -318,10 +326,8 @@ def refresh(self, reason: str):
node.addChild(child_node)

# compute balance once all children have been added
balance = 0
for node in self._root._children:
balance += node._data['value'].value
node.set_balance(balance)
node.set_balance(node._data['balance'].value)

# update tx_status_cache (before endInsertRows() triggers get_data_for_role() calls)
self.tx_status_cache.clear()
Expand All @@ -331,11 +337,12 @@ def refresh(self, reason: str):
self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info)

new_length = self._root.childCount()
self.beginInsertRows(QModelIndex(), 0, new_length-1)
self.transactions = transactions
self.endInsertRows()
if new_length:
self.beginInsertRows(QModelIndex(), 0, new_length - 1)
self.endInsertRows()

if selected_row:
if selected_row is not None:
self.view.selectionModel().select(
self.createIndex(selected_row, 0),
QItemSelectionModel.SelectionFlag.Rows | QItemSelectionModel.SelectionFlag.SelectCurrent)
Expand Down
8 changes: 7 additions & 1 deletion electrum/gui/qt/utxo_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,16 @@ def cb():
@profiler(min_threshold=0.05)
def update(self):
# not calling maybe_defer_update() as it interferes with coincontrol status bar
self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change
utxos = self.wallet.get_utxos()
self._maybe_reset_coincontrol(utxos)
self._utxo_dict = dict([(utxo.prevout.to_str(), utxo) for utxo in utxos])
if self.maybe_defer_update():
self.update_coincontrol_bar()
self.num_coins_label.setText(
_('{} unspent transaction outputs').format(len(utxos))
)
return
self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change
self.std_model.clear()
self.update_headers(self.__class__.headers)
for idx, utxo in enumerate(utxos):
Expand Down
1 change: 1 addition & 0 deletions tests/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

123 changes: 123 additions & 0 deletions tests/gui/test_qt_address_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import importlib.util
import os
import subprocess
import sys
import textwrap
import unittest


class TestAddressList(unittest.TestCase):
def test_visible_update_skips_model_rebuild_when_rows_are_unchanged(self):
if importlib.util.find_spec("PyQt6") is None:
self.skipTest("PyQt6 not available")

env = os.environ.copy()
env.setdefault("QT_QPA_PLATFORM", "offscreen")
script = textwrap.dedent(
r"""
from PyQt6.QtWidgets import QApplication, QLabel, QWidget

from electrum.simple_config import SimpleConfig
from electrum.gui.qt.address_list import AddressList


class _AddressDB:
def __init__(self, wallet):
self.wallet = wallet

def is_used(self, address):
return self.wallet._used.get(address, False)

def get_address_history_len(self, address):
return self.wallet._history_len.get(address, 0)


class _Wallet:
def __init__(self, address_count):
self._addresses = [f"addr{n}" for n in range(address_count)]
self._balances = {address: (n, 0, 0)
for n, address in enumerate(self._addresses)}
self._labels = {address: "" for address in self._addresses}
self._used = {address: False for address in self._addresses}
self._history_len = {address: 0 for address in self._addresses}
self.adb = _AddressDB(self)

def get_receiving_addresses(self):
return self._addresses

def get_change_addresses(self):
return []

def get_addresses(self):
return self._addresses

def get_all_known_addresses_beyond_gap_limit(self):
return set()

def get_addr_balance(self, address):
return self._balances[address]

def get_label_for_address(self, address):
return self._labels[address]

def is_change(self, address):
return False

def get_address_index(self, address):
return (self._addresses.index(address),)

def get_address_path_str(self, address):
return None

def is_frozen_address(self, address):
return False


class _MainWindow(QWidget):
def __init__(self, address_count):
super().__init__()
self.config = SimpleConfig({"electrum_path": "/tmp/electrum-test"})
self.wallet = _Wallet(address_count)
self.fx = None

def format_amount(self, amount, **kwargs):
return str(amount)


app = QApplication([])
window = _MainWindow(100)
address_list = AddressList(window)
address_list.num_addr_label = QLabel()

address_list._forced_update = True
address_list.update()
first_item = address_list.std_model.item(0, AddressList.Columns.ADDRESS)
assert address_list.std_model.rowCount() == 100
assert address_list.num_addr_label.text() == "100 addresses"

address_list.update()
assert first_item is address_list.std_model.item(0, AddressList.Columns.ADDRESS)

window.wallet._labels["addr0"] = "updated"
address_list.update()
updated_item = address_list.std_model.item(0, AddressList.Columns.ADDRESS)
assert first_item is not updated_item
assert address_list.std_model.item(0, AddressList.Columns.LABEL).text() == "updated"

address_list._forced_update = False
app.quit()
"""
)

result = subprocess.run(
[sys.executable, "-c", script],
env=env,
text=True,
capture_output=True,
timeout=30,
)
self.assertEqual(
0,
result.returncode,
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}",
)
106 changes: 106 additions & 0 deletions tests/gui/test_qt_history_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import importlib.util
import os
import subprocess
import sys
import textwrap
import unittest


class TestHistoryList(unittest.TestCase):
def test_visible_refresh_skips_model_rebuild_when_history_is_unchanged(self):
if importlib.util.find_spec("PyQt6") is None:
self.skipTest("PyQt6 not available")

env = os.environ.copy()
env.setdefault("QT_QPA_PLATFORM", "offscreen")
script = textwrap.dedent(
r"""
import datetime
import threading

from PyQt6.QtCore import QModelIndex
from PyQt6.QtWidgets import QApplication, QLabel, QWidget

from electrum.simple_config import SimpleConfig
from electrum.util import OrderedDictWithIndex, Satoshis
from electrum.gui.qt.history_list import HistoryList, HistoryModel


class _Wallet:
def __init__(self, tx_count):
self._labels = {f"txid{n}": f"label {n}" for n in range(tx_count)}
self.status_calls = 0

def get_full_history(self, **kwargs):
history = OrderedDictWithIndex()
for n, txid in enumerate(self._labels):
history[txid] = {
"txid": txid,
"height": n + 1,
"confirmations": 10,
"timestamp": 1_700_000_000 + n,
"txpos_in_block": n,
"date": datetime.datetime(2024, 1, 1),
"label": self._labels[txid],
"value": Satoshis(1000 + n),
"bc_value": Satoshis(1000 + n),
}
return history

def get_tx_status(self, txid, tx_mined_info):
self.status_calls += 1
return 9, "confirmed"


class _MainWindow(QWidget):
def __init__(self, tx_count):
super().__init__()
self.config = SimpleConfig({"electrum_path": "/tmp/electrum-test"})
self.wallet = _Wallet(tx_count)
self.fx = None
self.gui_thread = threading.current_thread()

def format_amount(self, amount, **kwargs):
return str(amount)


app = QApplication([])
window = _MainWindow(100)
history_model = HistoryModel(window)
history_list = HistoryList(window, history_model)
history_model.set_view(history_list)
history_list.num_tx_label = QLabel()

history_list._forced_update = True
history_list.update()
first_node = history_model._root.child(0)
first_status_calls = window.wallet.status_calls
assert history_model.rowCount(QModelIndex()) == 100
assert history_list.num_tx_label.text() == "100 transactions"

history_list.update()
assert first_node is history_model._root.child(0)
assert window.wallet.status_calls == first_status_calls

window.wallet._labels["txid0"] = "updated"
history_list.update()
assert first_node is not history_model._root.child(0)
assert history_model._root.child(0).get_data()["label"] == "updated"

history_list._forced_update = False
app.quit()
"""
)

result = subprocess.run(
[sys.executable, "-c", script],
env=env,
text=True,
capture_output=True,
timeout=30,
)
self.assertEqual(
0,
result.returncode,
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}",
)
Loading