diff --git a/.gitignore b/.gitignore index 4d93bd4..93ad1e2 100644 --- a/.gitignore +++ b/.gitignore @@ -202,4 +202,6 @@ bitcoin_safe.dist-info screenshots*/ .DS_Store -profile.html \ No newline at end of file +profile.html + +docs/org \ No newline at end of file diff --git a/README.md b/README.md index cf0ff96..6ffb341 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/bitcoin_safe/gui/qt/address_list.py b/bitcoin_safe/gui/qt/address_list.py index 8ad9251..a31ef9c 100644 --- a/bitcoin_safe/gui/qt/address_list.py +++ b/bitcoin_safe/gui/qt/address_list.py @@ -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 @@ -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) @@ -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 @@ -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 "") diff --git a/bitcoin_safe/gui/qt/hist_list.py b/bitcoin_safe/gui/qt/hist_list.py index c1c5ed8..080269f 100644 --- a/bitcoin_safe/gui/qt/hist_list.py +++ b/bitcoin_safe/gui/qt/hist_list.py @@ -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 @@ -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)) @@ -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) diff --git a/bitcoin_safe/gui/qt/main.py b/bitcoin_safe/gui/qt/main.py index 50f6ee4..8b96fe5 100644 --- a/bitcoin_safe/gui/qt/main.py +++ b/bitcoin_safe/gui/qt/main.py @@ -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) @@ -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( @@ -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( @@ -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) @@ -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() diff --git a/bitcoin_safe/gui/qt/my_treeview.py b/bitcoin_safe/gui/qt/my_treeview.py index b5048d3..2702635 100644 --- a/bitcoin_safe/gui/qt/my_treeview.py +++ b/bitcoin_safe/gui/qt/my_treeview.py @@ -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 @@ -128,6 +129,15 @@ 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 @@ -135,6 +145,7 @@ class MyItemDataRole(enum.IntEnum): 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): diff --git a/bitcoin_safe/gui/qt/qt_wallet.py b/bitcoin_safe/gui/qt/qt_wallet.py index 7bae456..cd84942 100644 --- a/bitcoin_safe/gui/qt/qt_wallet.py +++ b/bitcoin_safe/gui/qt/qt_wallet.py @@ -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(): @@ -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() @@ -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 @@ -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") @@ -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}") diff --git a/bitcoin_safe/gui/qt/sankey_bitcoin.py b/bitcoin_safe/gui/qt/sankey_bitcoin.py index 79d89ed..1544f10 100644 --- a/bitcoin_safe/gui/qt/sankey_bitcoin.py +++ b/bitcoin_safe/gui/qt/sankey_bitcoin.py @@ -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 @@ -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 @@ -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) @@ -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) @@ -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): @@ -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) diff --git a/bitcoin_safe/gui/qt/ui_tx.py b/bitcoin_safe/gui/qt/ui_tx.py index 4d9892b..6edaba6 100644 --- a/bitcoin_safe/gui/qt/ui_tx.py +++ b/bitcoin_safe/gui/qt/ui_tx.py @@ -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() @@ -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) diff --git a/bitcoin_safe/gui/qt/utxo_list.py b/bitcoin_safe/gui/qt/utxo_list.py index 59a3e12..1a47f0b 100644 --- a/bitcoin_safe/gui/qt/utxo_list.py +++ b/bitcoin_safe/gui/qt/utxo_list.py @@ -71,7 +71,7 @@ from PyQt6.QtWidgets import QAbstractItemView, QHeaderView, QWidget from ...i18n import translate -from ...signals import Signals, UpdateFilter +from ...signals import Signals, UpdateFilter, UpdateFilterReason from ...util import Satoshis, block_explorer_URL, clean_list from ...wallet import TxStatus, Wallet, get_wallets from .category_list import CategoryEditor @@ -82,6 +82,7 @@ MyTreeView, QItemSelectionModel, TreeViewWithToolbar, + needs_frequent_flag, ) from .util import ColorScheme, read_QIcon, sort_id_to_icon, webopen @@ -307,14 +308,34 @@ def update_with_filter(self, update_filter: UpdateFilter) -> None: should_update = False if should_update or update_filter.refresh_all: should_update = True - if should_update or update_filter.outpoints or update_filter.categories or update_filter.addresses: + if should_update or update_filter.categories or update_filter.addresses: should_update = True - if not should_update: - return + if should_update: + return self.update_content() logger.debug(f"{self.__class__.__name__} update_with_filter {update_filter}") - return self.update_content() + + self._before_update_content() + + log_info = [] + model = self._source_model + # Select rows with an ID in id_list + for row in range(model.rowCount()): + outpoint: OutPoint = model.data(model.index(row, self.key_column)) + + if ( + update_filter.reason == UpdateFilterReason.ChainHeightAdvanced + and model.data( + model.index(row, self.key_column), role=MyItemDataRole.ROLE_FREQUENT_UPDATEFLAG + ) + ) or any([outpoint in update_filter.outpoints]): + log_info.append((row, str(outpoint))) + self.refresh_row(outpoint, row) + + logger.debug(f"Updated {log_info}") + + self._after_update_content() def update_content(self): if self.maybe_defer_update(): @@ -384,11 +405,15 @@ def refresh_row(self, key: bdk.OutPoint, row: int): return txdetails = wallet.get_tx(outpoint.txid) if wallet else None - sort_id = TxStatus.from_wallet(txdetails.txid, wallet).sort_id() if txdetails and wallet else -1 + status = TxStatus.from_wallet(txdetails.txid, wallet) if txdetails and wallet else None + sort_id = status.sort_id() if status else -1 _items = [self._source_model.item(row, col) for col in self.Columns] items = [entry for entry in _items if entry] + if needs_frequent_flag(status=status): + # unconfirmed txos might be confirmed, and need to be updated more often + items[self.key_column].setData(True, role=MyItemDataRole.ROLE_FREQUENT_UPDATEFLAG) items[self.Columns.STATUS].setIcon( read_QIcon( icon_of_utxo(python_utxo.is_spent_by_txid, txdetails.confirmation_time, sort_id) diff --git a/bitcoin_safe/gui/qt/wallet_steps.py b/bitcoin_safe/gui/qt/wallet_steps.py index 3afb0ff..fc4b7da 100644 --- a/bitcoin_safe/gui/qt/wallet_steps.py +++ b/bitcoin_safe/gui/qt/wallet_steps.py @@ -262,9 +262,13 @@ def __init__( class BaseTab(QObject, ThreadingManager): def __init__(self, refs: TabInfo, threading_parent: ThreadingManager | None = None) -> None: - super().__init__(parent=refs.container, signals_min=refs.qt_wallet.signals_min if refs.qt_wallet else refs.qtwalletbase.signals_min, threading_parent=threading_parent) # type: ignore self.refs = refs - self.threading_parent = threading_parent + self.threading_parent = ( + threading_parent + if threading_parent + else (self.refs.qt_wallet if self.refs.qt_wallet else self.refs.qtwalletbase) + ) + super().__init__(parent=refs.container, signals_min=refs.qt_wallet.signals_min if refs.qt_wallet else refs.qtwalletbase.signals_min, threading_parent=self.threading_parent) # type: ignore self.buttonbox, self.buttonbox_buttons = create_button_box( self.refs.go_to_next_index, diff --git a/bitcoin_safe/signals.py b/bitcoin_safe/signals.py index cd33b3e..24fc69b 100644 --- a/bitcoin_safe/signals.py +++ b/bitcoin_safe/signals.py @@ -32,6 +32,8 @@ from collections import defaultdict from enum import Enum +from bitcoin_safe.pythonbdk_types import OutPoint + logger = logging.getLogger(__name__) import threading from typing import ( @@ -66,19 +68,22 @@ class UpdateFilterReason(Enum): RefreshCaches = enum.auto() CreatePSBT = enum.auto() TxCreator = enum.auto() + TransactionChange = enum.auto() + ForceRefresh = enum.auto() + ChainHeightAdvanced = enum.auto() class UpdateFilter: def __init__( self, - outpoints: Iterable[str] | None = None, + outpoints: Iterable[OutPoint] | None = None, addresses: Iterable[str] | None = None, categories: Iterable[Optional[str]] | None = None, txids: Iterable[str] | None = None, refresh_all=False, reason: UpdateFilterReason = UpdateFilterReason.Unknown, ) -> None: - self.outpoints: Set[str] = set(outpoints) if outpoints else set() + self.outpoints: Set[OutPoint] = set(outpoints) if outpoints else set() self.addresses: Set[str] = set(addresses) if addresses else set() self.categories = set(categories) if categories else set() self.txids = set(txids) if txids else set() diff --git a/bitcoin_safe/threading_manager.py b/bitcoin_safe/threading_manager.py index 3e108ec..e1ffbc0 100644 --- a/bitcoin_safe/threading_manager.py +++ b/bitcoin_safe/threading_manager.py @@ -196,6 +196,7 @@ def __init__( self.taskthreads: deque[TaskThread] = deque() self.threading_manager_children: deque[ThreadingManager] = deque() self.lock = Lock() + self.threading_parent = threading_parent if threading_parent: threading_parent.threading_manager_children.append(self) @@ -221,12 +222,18 @@ def _remove(self, thread: TaskThread): f"Removed thread {thread.thread_name}, Number of threads = {len(self.taskthreads)} {[thread.thread_name for thread in self.taskthreads]}" ) - def stop_and_wait_all(self, timeout=10): + def stop_and_wait_all(self): while self.threading_manager_children: child = self.threading_manager_children.pop() - child.stop_and_wait_all() + child.end_threading_manager() # Wait for all threads to finish while self.taskthreads: taskthread = self.taskthreads.pop() taskthread.stop() + + def end_threading_manager(self): + self.stop_and_wait_all() + + if self.threading_parent and self in self.threading_parent.threading_manager_children: + self.threading_parent.threading_manager_children.remove(self) diff --git a/bitcoin_safe/wallet.py b/bitcoin_safe/wallet.py index 2d9f86b..cf78f9b 100644 --- a/bitcoin_safe/wallet.py +++ b/bitcoin_safe/wallet.py @@ -957,6 +957,7 @@ def fill_commonly_used_caches(self) -> None: if i > 0: self.clear_cache() self.get_addresses() + self.get_height() advanced_tips = self.advance_tips_by_gap() logger.info(f"{self.id} tips were advanced by {advanced_tips}") diff --git a/docs/explorer.gif b/docs/explorer.gif new file mode 100644 index 0000000..569348b Binary files /dev/null and b/docs/explorer.gif differ diff --git a/docs/multisig-setup.gif b/docs/multisig-setup.gif index 6d56ac7..0cb3b90 100644 Binary files a/docs/multisig-setup.gif and b/docs/multisig-setup.gif differ diff --git a/docs/send.gif b/docs/send.gif index f3374e9..fe10885 100644 Binary files a/docs/send.gif and b/docs/send.gif differ diff --git a/poetry.lock b/poetry.lock index 5464120..cde3c30 100644 --- a/poetry.lock +++ b/poetry.lock @@ -110,13 +110,13 @@ requests = ">=2.31.0,<3.0.0" [[package]] name = "bitcoin-qr-tools" -version = "0.14.6" +version = "0.14.7" description = "Python bitcoin qr reader and generator" optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "bitcoin_qr_tools-0.14.6-py3-none-any.whl", hash = "sha256:4cb655cc9c6b795cd6c1b1c926bec6041de708d7ccb6eb215a4b0946b5bcc1ff"}, - {file = "bitcoin_qr_tools-0.14.6.tar.gz", hash = "sha256:aa81bdb83b3caf124b4039bbb8524b843416ce6f6dc4937a3efffd531f5dbb7f"}, + {file = "bitcoin_qr_tools-0.14.7-py3-none-any.whl", hash = "sha256:2f7fad74727d94519f8e3268300c30482881a755b2fa95c2aec6b90c03b0ce5b"}, + {file = "bitcoin_qr_tools-0.14.7.tar.gz", hash = "sha256:8aac9bef85fe110df82348f58250aafebbf65b169e660161291c71ac5d7c1110"}, ] [package.dependencies] @@ -2346,13 +2346,13 @@ files = [ [[package]] name = "translate-toolkit" -version = "3.13.5" +version = "3.14.1" description = "Tools and API for translation and localization engineering." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "translate_toolkit-3.13.5-py3-none-any.whl", hash = "sha256:f2a549973ca50ad56d299050401ee55bfd8265dd6f7c9307c77ece2f0e8e2ca8"}, - {file = "translate_toolkit-3.13.5.tar.gz", hash = "sha256:53c59c919e52a9787c0d1d7ccd34df9508e9f58bb84d1d52d27a9bda5203d768"}, + {file = "translate_toolkit-3.14.1-py3-none-any.whl", hash = "sha256:74dd963f770ec1d18e44895d8a9f86d47a0d73b270b22a69a5652f30ae2dca79"}, + {file = "translate_toolkit-3.14.1.tar.gz", hash = "sha256:2148c437c529d4eaf89c5a3bd5690376eabee97c3c39b7d4824001a7cf333e86"}, ] [package.dependencies] @@ -2360,15 +2360,15 @@ lxml = ">=4.6.3" wcwidth = ">=0.2.10" [package.extras] -all = ["BeautifulSoup4 (>=4.10.0)", "aeidon (==1.15)", "charset-normalizer (==3.3.2)", "cheroot (==10.0.1)", "fluent.syntax (==0.19.0)", "iniparse (==0.5)", "mistletoe (==1.4.0)", "phply (==1.2.6)", "pyenchant (==3.2.2)", "pyparsing (==3.1.4)", "python-Levenshtein (>=0.21.0)", "ruamel.yaml (==0.18.6)", "vobject (==0.9.8)"] -chardet = ["charset-normalizer (==3.3.2)"] +all = ["BeautifulSoup4 (>=4.10.0)", "aeidon (==1.15)", "charset-normalizer (==3.4.0)", "cheroot (==10.0.1)", "fluent.syntax (==0.19.0)", "iniparse (==0.5)", "mistletoe (==1.4.0)", "phply (==1.2.6)", "pyenchant (==3.2.2)", "pyparsing (==3.2.0)", "python-Levenshtein (>=0.21.0)", "ruamel.yaml (==0.18.6)", "vobject (==0.9.8)"] +chardet = ["charset-normalizer (==3.4.0)"] fluent = ["fluent.syntax (==0.19.0)"] ical = ["vobject (==0.9.8)"] ini = ["iniparse (==0.5)"] levenshtein = ["python-Levenshtein (>=0.21.0)"] markdown = ["mistletoe (==1.4.0)"] php = ["phply (==1.2.6)"] -rc = ["pyparsing (==3.1.4)"] +rc = ["pyparsing (==3.2.0)"] spellcheck = ["pyenchant (==3.2.2)"] subtitles = ["aeidon (==1.15)"] tmserver = ["cheroot (==10.0.1)"] @@ -2481,4 +2481,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "6230851e56531063fb041b948c29c763e1dca319daf21b30744ae7bacfd6b088" +content-hash = "4afd3fd31767ab297309d324d50c0a694a889721f46013a827def6c25fa1947a" diff --git a/pyproject.toml b/pyproject.toml index 7661350..66ac987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ python-gnupg = "^0.5.2" # bitcoin-qr-tools = {path="../bitcoin-qr-tools"} # "^0.10.15" # bitcoin-nostr-chat = {path="../bitcoin-nostr-chat"} # "^0.2.6" # bitcoin-usb = {path="../bitcoin-usb"} # "^0.3.3" -bitcoin-qr-tools = "^0.14.6" +bitcoin-qr-tools = "^0.14.7" bitcoin-nostr-chat = "^0.3.5" bitcoin-usb = "^0.5.3" numpy = "^2.0.1"