Skip to content

Commit

Permalink
bug fixes and UI improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
andreasgriffin committed Oct 16, 2024
1 parent 064ff05 commit 12a6a7c
Show file tree
Hide file tree
Showing 19 changed files with 170 additions and 70 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,6 @@ bitcoin_safe.dist-info
screenshots*/
.DS_Store

profile.html
profile.html

docs/org
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- 🇺🇸 English, 🇨🇳 Chinese - 简体中文, 🇪🇸 Spanish - español de España, 🇯🇵 Japanese - 日本語, 🇷🇺 Russian - русский, 🇵🇹 Portuguese - português europeu, 🇮🇳 Hindi - हिन्दी, Arabic - العربية, 🇮🇹 Italian - italiano, 🇫🇷 French - Français, (more upon request)
- **Simpler** address labels by using categories (e.g. "KYC", "Non-KYC", "Work", "Friends", ...)
- Automatic coin selection within categories
- Transaction flow diagrams, visualizing inputs and outputs
- Transaction flow diagrams, visualizing inputs and outputs, click on inputs and output to trace the money flow
- **Sending** for non-technical users
- 1-click fee selection via mempool-blocks
- Automatic merging of small utxos when fees are low
Expand All @@ -30,14 +30,19 @@

## Preview

##### Sending

![screenshot0](docs/send.gif)

##### Setup a multisig wallet

![screenshot1](docs/multisig-setup.gif)

##### Transaction exploring via the money flow diagram

![diagram](docs/explorer.gif)

##### Sending

![screenshot0](docs/send.gif)

##### PSBT sharing with trusted devices

![psbt-share.gif](docs/psbt-share.gif)
Expand Down
23 changes: 15 additions & 8 deletions bitcoin_safe/gui/qt/address_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
MyStandardItemModel,
MyTreeView,
TreeViewWithToolbar,
needs_frequent_flag,
)
from .taglist import AddressDragInfo
from .util import ColorScheme, Message, do_copy, read_QIcon, sort_id_to_icon, webopen
Expand Down Expand Up @@ -377,8 +378,15 @@ def update_with_filter(self, update_filter: UpdateFilter) -> None:
address = model.data(model.index(row, self.Columns.ADDRESS))
address_match = address in update_filter.addresses
category_match = model.data(model.index(row, self.Columns.CATEGORY)) in update_filter.categories
if address_match or (
not update_filter.addresses and category_match or len(update_filter.categories) > 1
if (
(
update_filter.reason == UpdateFilterReason.ChainHeightAdvanced
and model.data(
model.index(row, self.key_column), role=MyItemDataRole.ROLE_FREQUENT_UPDATEFLAG
)
)
or address_match
or (not update_filter.addresses and category_match or len(update_filter.categories) > 1)
):
log_info.append((row, address))
self.refresh_row(address, row)
Expand Down Expand Up @@ -491,12 +499,9 @@ def refresh_row(self, key: str, row: int) -> None:
fulltxdetails = [self.wallet.get_dict_fulltxdetail().get(txid) for txid in txids]
txs_involed = [fulltxdetail.tx for fulltxdetail in fulltxdetails if fulltxdetail]

sort_id = (
min([TxStatus.from_wallet(tx.txid, self.wallet).sort_id() for tx in txs_involed])
if txs_involed
else None
)
icon_path = sort_id_to_icon(sort_id) if sort_id is not None else None
statuses = [TxStatus.from_wallet(tx.txid, self.wallet) for tx in txs_involed]
min_status = sorted(statuses, key=lambda status: status.sort_id())[0] if statuses else None
icon_path = sort_id_to_icon(min_status.sort_id()) if min_status else None
num = len(txs_involed)

balance = self.wallet.get_addr_balance(address).total
Expand All @@ -506,6 +511,8 @@ def refresh_row(self, key: str, row: int) -> None:
fiat_balance_str = ""
_item = [self._source_model.item(row, col) for col in self.Columns]
item = [entry for entry in _item if entry]
if needs_frequent_flag(status=min_status):
item[self.key_column].setData(True, role=MyItemDataRole.ROLE_FREQUENT_UPDATEFLAG)
item[self.Columns.LABEL].setText(label)
item[self.Columns.LABEL].setData(label, MyItemDataRole.ROLE_CLIPBOARD_DATA)
item[self.Columns.CATEGORY].setText(category if category else "")
Expand Down
36 changes: 22 additions & 14 deletions bitcoin_safe/gui/qt/hist_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
MyStandardItemModel,
MyTreeView,
TreeViewWithToolbar,
needs_frequent_flag,
)
from .taglist import AddressDragInfo
from .util import Message, MessageType, read_QIcon, sort_id_to_icon, webopen
Expand Down Expand Up @@ -373,7 +374,12 @@ def tx_involves_address(txid) -> Set[str]:
for row in range(model.rowCount()):
txid = model.data(model.index(row, self.Columns.TXID))

if any(
if (
update_filter.reason == UpdateFilterReason.ChainHeightAdvanced
and model.data(
model.index(row, self.key_column), role=MyItemDataRole.ROLE_FREQUENT_UPDATEFLAG
)
) or any(
[txid in update_filter.txids, categories_intersect(model, row), tx_involves_address(txid)]
):
log_info.append((row, txid))
Expand Down Expand Up @@ -511,26 +517,28 @@ def refresh_row(self, key: str, row: int) -> None:
)
status_text = (
datetime.datetime.fromtimestamp(tx.confirmation_time.timestamp).strftime("%Y-%m-%d %H:%M")
if status.confirmations()
if tx.confirmation_time
else estimated_duration_str
)
status_data = (
datetime.datetime.fromtimestamp(tx.confirmation_time.height).strftime("%Y-%m-%d %H:%M")
if tx.confirmation_time
else TxConfirmationStatus.to_str(status.confirmation_status)
)
status_tooltip = (
self.tr("{number} Confirmations").format(number=status.confirmations())
if 1 <= status.confirmations() <= 6
else status_text
)

_item = [self._source_model.item(row, col) for col in self.Columns]
item = [entry for entry in _item if entry]
if needs_frequent_flag(status=status):
item[self.key_column].setData(True, role=MyItemDataRole.ROLE_FREQUENT_UPDATEFLAG)
item[self.Columns.STATUS].setText(status_text)
item[self.Columns.STATUS].setData(
(
tx.confirmation_time.height
if status.confirmations()
else (TxConfirmationStatus.to_str(status.confirmation_status))
),
MyItemDataRole.ROLE_CLIPBOARD_DATA,
)
item[self.Columns.STATUS].setData(status_data, MyItemDataRole.ROLE_CLIPBOARD_DATA)
item[self.Columns.STATUS].setIcon(read_QIcon(sort_id_to_icon(status.sort_id())))

item[self.Columns.STATUS].setToolTip(
f"{status.confirmations()} Confirmations" if status.confirmations() else status_text
)
item[self.Columns.STATUS].setToolTip(status_tooltip)
item[self.Columns.LABEL].setText(label)
item[self.Columns.LABEL].setData(label, MyItemDataRole.ROLE_CLIPBOARD_DATA)
item[self.Columns.CATEGORIES].setText(category)
Expand Down
8 changes: 5 additions & 3 deletions bitcoin_safe/gui/qt/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def __init__(
self.qt_wallets: Dict[str, QTWallet] = {}
self.threading_manager = ThreadingManager(signals_min=self.signals)

self.fx = FX(signals_min=self.signals)
self.fx = FX(signals_min=self.signals, threading_parent=self.threading_manager)
self.language_chooser = LanguageChooser(self, self.config, [self.signals.language_switch])
if not config_present:
self.config.language_code = self.language_chooser.dialog_choose_language(self)
Expand Down Expand Up @@ -799,6 +799,7 @@ def open_tx_in_tab(
blockchain=self.get_blockchain_of_any_wallet(),
data=data,
parent=self,
threading_parent=self.threading_manager,
)

self.tab_wallets.add_tab(
Expand Down Expand Up @@ -894,6 +895,7 @@ def open_psbt_in_tab(
blockchain=self.get_blockchain_of_any_wallet(),
data=data,
parent=self,
threading_parent=self.threading_manager,
)

self.tab_wallets.add_tab(
Expand Down Expand Up @@ -1327,7 +1329,7 @@ def close_tab(self, index: int) -> None:
if isinstance(tab_data, ThreadingManager):
# this is necessary to ensure the closeevent
# and with it the thread cleanup is called
tab_data.stop_and_wait_all()
tab_data.end_threading_manager()

if qt_wallet:
self.remove_qt_wallet(qt_wallet)
Expand All @@ -1341,7 +1343,7 @@ def sync(self) -> None:
qt_wallet.sync()

def closeEvent(self, event: Optional[QCloseEvent]) -> None:
self.threading_manager.stop_and_wait_all()
self.threading_manager.end_threading_manager()

self.config.last_wallet_files[str(self.config.network)] = [
qt_wallet.file_path for qt_wallet in self.qt_wallets.values()
Expand Down
11 changes: 11 additions & 0 deletions bitcoin_safe/gui/qt/my_treeview.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
from bitcoin_safe.gui.qt.wrappers import Menu
from bitcoin_safe.signals import Signals
from bitcoin_safe.util import str_to_qbytearray
from bitcoin_safe.wallet import TxStatus

from ...config import UserConfig
from ...i18n import translate
Expand Down Expand Up @@ -128,13 +129,23 @@
from .util import do_copy, read_QIcon


def needs_frequent_flag(status: TxStatus | None) -> bool:
if not status:
return True

if status.confirmations() < 6:
return True
return False


class MyItemDataRole(enum.IntEnum):
ROLE_CLIPBOARD_DATA = Qt.ItemDataRole.UserRole + 100
ROLE_CUSTOM_PAINT = Qt.ItemDataRole.UserRole + 101
ROLE_EDIT_KEY = Qt.ItemDataRole.UserRole + 102
ROLE_FILTER_DATA = Qt.ItemDataRole.UserRole + 103
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
ROLE_KEY = Qt.ItemDataRole.UserRole + 1001
ROLE_FREQUENT_UPDATEFLAG = Qt.ItemDataRole.UserRole + 1002


class MyMenu(Menu):
Expand Down
22 changes: 18 additions & 4 deletions bitcoin_safe/gui/qt/qt_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def close(self) -> None:
self.sync_tab.unsubscribe_all()
self.sync_tab.nostr_sync.stop()
self.stop_sync_timer()
self.stop_and_wait_all()
self.end_threading_manager()

def _start_sync_regularly_timer(self, delay_retry_sync=60) -> None:
if self.timer_sync_regularly.isActive():
Expand Down Expand Up @@ -618,7 +618,9 @@ def handle_delta_txs(self, delta_txs: DeltaCacheListTransactions) -> None:
self.hanlde_removed_txs(delta_txs.removed)
self.handle_appended_txs(delta_txs.appended)

def refresh_caches_and_ui_lists(self, enable_threading=ENABLE_THREADING, force_ui_refresh=True) -> None:
def refresh_caches_and_ui_lists(
self, enable_threading=ENABLE_THREADING, force_ui_refresh=True, chain_height_advanced=False
) -> None:
# before the wallet UI updates, we have to refresh the wallet caches to make the UI update faster
logger.debug("refresh_caches_and_ui_lists")
self.wallet.clear_cache()
Expand All @@ -637,8 +639,17 @@ def on_done(result) -> None:
logger.debug("start refresh ui")

self.wallet_signals.updated.emit(
UpdateFilter(refresh_all=True, reason=UpdateFilterReason.RefreshCaches)
UpdateFilter(
refresh_all=True,
reason=(
UpdateFilterReason.TransactionChange
if change_dict
else UpdateFilterReason.ForceRefresh
),
)
)
elif chain_height_advanced:
self.wallet_signals.updated.emit(UpdateFilter(reason=UpdateFilterReason.ChainHeightAdvanced))

def on_success(result) -> None:
# now do the UI
Expand Down Expand Up @@ -964,7 +975,9 @@ def on_success(result) -> None:

logger.info("start updating lists")
# self.wallet.clear_cache()
self.refresh_caches_and_ui_lists(force_ui_refresh=False)
self.refresh_caches_and_ui_lists(
force_ui_refresh=False, chain_height_advanced=self.wallet.get_height() != old_chain_height
)
# self.update_tabs()
logger.info("finished updating lists")

Expand All @@ -984,6 +997,7 @@ def on_success(result) -> None:
# by filling all the cache before the sync
# Additionally I do this in the main thread so all caches
# are filled before the syncing process
old_chain_height = self.wallet.get_height()
self.refresh_caches_and_ui_lists(enable_threading=False, force_ui_refresh=False)

logger.info(f"Start syncing wallet {self.wallet.id}")
Expand Down
31 changes: 20 additions & 11 deletions bitcoin_safe/gui/qt/sankey_bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@


import logging
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional

import bdkpython as bdk
from PyQt6.QtGui import QColor
Expand Down Expand Up @@ -86,17 +86,17 @@ def refresh(self, update_filter: UpdateFilter):
self.set_tx(self.tx)

@property
def outpoints(self) -> List[str]:
def outpoints(self) -> List[OutPoint]:
if not self.tx:
return []
txid = self.tx.txid()
return [f"{txid}:{i}" for i in range(len(self.tx.output()))]
return [OutPoint(txid=txid, vout=vout) for vout in range(len(self.tx.output()))]

@property
def input_outpoints(self) -> List[str]:
def input_outpoints(self) -> List[OutPoint]:
if not self.tx:
return []
return [str(OutPoint.from_bdk(inp.previous_output)) for inp in self.tx.input()]
return [OutPoint.from_bdk(inp.previous_output) for inp in self.tx.input()]

def set_tx(self, tx: bdk.Transaction, fee_info: FeeInfo | None = None) -> bool:
self.tx = tx
Expand All @@ -115,7 +115,8 @@ def set_tx(self, tx: bdk.Transaction, fee_info: FeeInfo | None = None) -> bool:
address = robust_address_str_from_script(txout.script_pubkey, network=self.network)
self.addresses.append(address)

label, color = self.get_address_info(address, wallets=wallets)
label = self.get_address_label(address, wallets=wallets)
color = self.get_address_color(address, wallets=wallets)
labels[flow_index] = label if label else address
tooltips[flow_index] = html_f(
((label + "\n" + address) if label else address)
Expand Down Expand Up @@ -149,7 +150,8 @@ def set_tx(self, tx: bdk.Transaction, fee_info: FeeInfo | None = None) -> bool:
self.addresses.append(txo.address)
flow_index = FlowIndex(flow_type=FlowType.InFlow, i=i)

label, color = self.get_address_info(txo.address, wallets=wallets)
label = self.get_address_label(txo.address, wallets=wallets)
color = self.get_address_color(txo.address, wallets=wallets)
labels[flow_index] = label if label else txo.address
tooltips[flow_index] = html_f(
((label + "\n" + txo.address) if label else txo.address)
Expand Down Expand Up @@ -193,7 +195,7 @@ def set_tx(self, tx: bdk.Transaction, fee_info: FeeInfo | None = None) -> bool:
)
return True

def get_address_info(self, address: str, wallets: List[Wallet]) -> Tuple[str | None, QColor | None]:
def get_address_color(self, address: str, wallets: List[Wallet]) -> QColor | None:
def get_wallet():
for wallet in wallets:
if wallet.is_my_address(address):
Expand All @@ -202,12 +204,19 @@ def get_wallet():

wallet = get_wallet()
if not wallet:
return None, None
return None
color = AddressEdit.color_address(address, wallet)
if not color:
logger.error("This should not happen, since wallet should only be found if the address is mine.")
return None, None
return wallet.labels.get_label(address), color
return None
return color

def get_address_label(self, address: str, wallets: List[Wallet]) -> str | None:
for wallet in wallets:
label = wallet.labels.get_label(address)
if label:
return label
return None

def get_python_txo(self, outpoint: str, wallets: List[Wallet] | None = None) -> Optional[PythonUtxo]:
wallets = wallets if wallets else get_wallets(self.signals)
Expand Down
2 changes: 1 addition & 1 deletion bitcoin_safe/gui/qt/ui_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,6 @@ def on_error(packed_error_info) -> None:
pass

def on_success(finalized_tx) -> None:
print("here")
if finalized_tx:
assert (
finalized_tx.txid() == self.data.data.txid()
Expand Down Expand Up @@ -818,6 +817,7 @@ def update_tx_progress(self) -> Optional[TxSigningSteps]:
psbt=self.data.data,
network=self.network,
signals=self.signals,
threading_parent=self,
)

self.tx_singning_steps_container_layout.addWidget(tx_singning_steps)
Expand Down
Loading

0 comments on commit 12a6a7c

Please sign in to comment.