From 062051b530a0953566699bbf16e28602e962c3b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 21 Jan 2023 12:23:26 +0100 Subject: [PATCH 0001/1143] lnworker: store onchain default labels in a cache --- electrum/lnworker.py | 14 ++++++++++---- electrum/wallet.py | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index f89c29370..90f428953 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -217,7 +217,7 @@ def __init__(self, xprv, features: LnFeatures): self.network = None # type: Optional[Network] self.config = None # type: Optional[SimpleConfig] self.stopping_soon = False # whether we are being shut down - + self._labels_cache = {} # txid -> str self.register_callbacks() @property @@ -876,6 +876,9 @@ def get_lightning_history(self): out[payment_hash] = item return out + def get_label_for_txid(self, txid: str) -> str: + return self._labels_cache.get(txid) + def get_onchain_history(self): current_height = self.wallet.adb.get_local_height() out = {} @@ -886,10 +889,11 @@ def get_onchain_history(self): continue funding_txid, funding_height, funding_timestamp = item tx_height = self.wallet.adb.get_tx_height(funding_txid) + self._labels_cache[funding_txid] = _('Open channel') + ' ' + chan.get_id_for_log() item = { 'channel_id': bh2u(chan.channel_id), 'type': 'channel_opening', - 'label': self.wallet.get_label_for_txid(funding_txid) or (_('Open channel') + ' ' + chan.get_id_for_log()), + 'label': self.get_label_for_txid(funding_txid), 'txid': funding_txid, 'amount_msat': chan.balance(LOCAL, ctn=0), 'direction': PaymentDirection.RECEIVED, @@ -906,10 +910,11 @@ def get_onchain_history(self): continue closing_txid, closing_height, closing_timestamp = item tx_height = self.wallet.adb.get_tx_height(closing_txid) + self._labels_cache[closing_txid] = _('Close channel') + ' ' + chan.get_id_for_log() item = { 'channel_id': bh2u(chan.channel_id), 'txid': closing_txid, - 'label': self.wallet.get_label_for_txid(closing_txid) or (_('Close channel') + ' ' + chan.get_id_for_log()), + 'label': self.get_label_for_txid(closing_txid), 'type': 'channel_closure', 'amount_msat': -chan.balance_minus_outgoing_htlcs(LOCAL), 'direction': PaymentDirection.SENT, @@ -942,13 +947,14 @@ def get_onchain_history(self): label += ' (%s)' % _('waiting for funding tx confirmation') if not swap.is_reverse and not swap.is_redeemed and swap.spending_txid is None and delta < 0: label += f' (refundable in {-delta} blocks)' # fixme: only if unspent + self._labels_cache[txid] = label out[txid] = { 'txid': txid, 'group_id': txid, 'amount_msat': 0, #'amount_msat': amount_msat, # must not be added 'type': 'swap', - 'label': self.wallet.get_label_for_txid(txid) or label, + 'label': self.get_label_for_txid(txid), } return out diff --git a/electrum/wallet.py b/electrum/wallet.py index 360f7a0d5..34258778f 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1372,6 +1372,8 @@ def get_label_for_txid(self, tx_hash: str) -> str: return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash) def _get_default_label_for_txid(self, tx_hash: str) -> str: + if self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)): + return label # note: we don't deserialize tx as the history calls us for every tx, and that would be slow if not self.db.get_txi_addresses(tx_hash): # no inputs are ismine -> likely incoming payment -> concat labels of output addresses From 768eb35c864f1f0b4220f24f62f7edae094923cf Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 24 Jan 2023 14:45:55 +0100 Subject: [PATCH 0002/1143] follow-up 5d9678a2690abbfbb22dc137f94e51fea28a95fd --- electrum/lnworker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 90f428953..78df09282 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -941,7 +941,7 @@ def get_onchain_history(self): amount_msat = 0 label = 'Reverse swap' if swap.is_reverse else 'Forward swap' delta = current_height - swap.locktime - if self.wallet.adb.is_mine(swap.funding_txid): + if self.wallet.adb.is_mine(swap.lockup_address): tx_height = self.wallet.adb.get_tx_height(swap.funding_txid) if swap.is_reverse and tx_height.height <= 0: label += ' (%s)' % _('waiting for funding tx confirmation') From ccc0b5daa24fdfae4dd86dbafa7103fc9dc88e16 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 24 Jan 2023 15:06:31 +0000 Subject: [PATCH 0003/1143] build: don't force-push git branches needed for historical builds closes https://github.com/spesmilo/electrum/issues/8162 --- contrib/android/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 064182b8c..97bb20cb0 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -167,7 +167,7 @@ RUN cd /opt \ && cd buildozer \ && git remote add sombernight https://github.com/SomberNight/buildozer \ && git fetch --all \ - # commit: from branch sombernight/electrum_20210421 + # commit: from branch sombernight/electrum_20210421 (note: careful with force-pushing! see #8162) && git checkout "6f03256e8312f8d1e5a6da3a2a1bf06e2738325e^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . @@ -178,7 +178,7 @@ RUN cd /opt \ && git remote add sombernight https://github.com/SomberNight/python-for-android \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ - # commit: from branch accumulator/electrum_20210421d + # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) && git checkout "d33e07ba4c7931da46122a32f3807709a73cb7f6^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . From 23adb53572a078dbb7f9fb5d51183130463fee03 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 25 Jan 2023 15:44:37 +0100 Subject: [PATCH 0004/1143] fix crash when trying to display channel backup details --- electrum/lnchannel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 3eefe48f5..2328e191a 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -509,6 +509,9 @@ def get_capacity(self): def is_backup(self): return True + def get_remote_alias(self) -> Optional[bytes]: + return None + def create_sweeptxs_for_their_ctx(self, ctx): return {} From 599ac065fb766b67d246ee2d33a6ee574608c505 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 25 Jan 2023 15:53:55 +0100 Subject: [PATCH 0005/1143] Qt: unify calls to get_transaction (follow-up 121d8732f1e1d97f70a0f72221b4acca1e818319) --- electrum/gui/qt/history_list.py | 7 ++----- electrum/gui/qt/utxo_list.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 914068ef6..073e58c82 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -672,7 +672,7 @@ def mouseDoubleClickEvent(self, event: QMouseEvent): self.parent.show_lightning_transaction(tx_item) return tx_hash = tx_item['txid'] - tx = self.wallet.db.get_transaction(tx_hash) + tx = self.wallet.adb.get_transaction(tx_hash) if not tx: return self.show_transaction(tx_item, tx) @@ -716,10 +716,7 @@ def create_menu(self, position: QPoint): menu.exec_(self.viewport().mapToGlobal(position)) return tx_hash = tx_item['txid'] - if tx_item.get('lightning'): - tx = self.wallet.adb.get_transaction(tx_hash) - else: - tx = self.wallet.db.get_transaction(tx_hash) + tx = self.wallet.adb.get_transaction(tx_hash) if not tx: return tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index b9415957c..727e38d46 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -206,7 +206,7 @@ def create_menu(self, position): addr = utxo.address txid = utxo.prevout.txid.hex() # "Details" - tx = self.wallet.db.get_transaction(txid) + tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label)) From cb11e1faed11fb966026ace287103980f3397630 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 25 Jan 2023 15:35:42 +0000 Subject: [PATCH 0006/1143] CLI: make "electrum stop" robust to dead daemon / lingering lockfile follow-up fbf79b148b29ecf99a704947d999190cdbb4e517 --- run_electrum | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/run_electrum b/run_electrum index 49e9a0f2a..fb27bf45a 100755 --- a/run_electrum +++ b/run_electrum @@ -392,7 +392,7 @@ def main(): if cmdname == 'daemon' and config.get("detach"): # detect lockfile. - # This is not as goog as get_file_descriptor, but that would require the asyncio loop + # This is not as good as get_file_descriptor, but that would require the asyncio loop lockfile = daemon.get_lockfile(config) if os.path.exists(lockfile): print_stderr("Daemon already running (lockfile detected).") @@ -474,6 +474,11 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): print_msg("Daemon not running; try 'electrum daemon -d'") if not cmd.requires_network: print_msg("To run this command without a daemon, use --offline") + if cmd.name == "stop": # remove lockfile if it exists, as daemon looks dead + lockfile = daemon.get_lockfile(config) + if os.path.exists(lockfile): + print_msg("Found lingering lockfile for daemon. Removing.") + daemon.remove_lockfile(lockfile) sys_exit(1) except Exception as e: print_stderr(str(e) or repr(e)) From 8b5aa5c43303adbaf3b7a02c05c2c7ea8928be5b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 25 Jan 2023 15:56:50 +0000 Subject: [PATCH 0007/1143] manually rerun freeze_packages, restricted to fix known CVEs only --- contrib/deterministic-build/requirements-build-wine.txt | 4 ++-- contrib/deterministic-build/requirements-hw.txt | 6 +++--- contrib/deterministic-build/requirements.txt | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/deterministic-build/requirements-build-wine.txt b/contrib/deterministic-build/requirements-build-wine.txt index 1bc537b33..96a7709c3 100644 --- a/contrib/deterministic-build/requirements-build-wine.txt +++ b/contrib/deterministic-build/requirements-build-wine.txt @@ -1,7 +1,7 @@ altgraph==0.17.3 \ --hash=sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd -future==0.18.2 \ - --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d +future==0.18.3 \ + --hash=sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307 pefile==2022.5.30 \ --hash=sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b pip==22.3.1 \ diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 1376e65e0..a911568f3 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -8,9 +8,9 @@ btchip-python==0.1.32 \ --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55 cbor==1.0.0 \ --hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 -certifi==2022.9.24 \ - --hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 \ - --hash=sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ + --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 cffi==1.15.1 \ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ diff --git a/contrib/deterministic-build/requirements.txt b/contrib/deterministic-build/requirements.txt index 8d6dba0ca..87068dd61 100644 --- a/contrib/deterministic-build/requirements.txt +++ b/contrib/deterministic-build/requirements.txt @@ -12,8 +12,8 @@ attrs==22.1.0 \ --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 bitstring==3.1.9 \ --hash=sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7 -certifi==2022.9.24 \ - --hash=sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14 +certifi==2022.12.7 \ + --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 dnspython==2.2.1 \ From d6febb5c1243f3f80d5a79af9aa39312c8166c91 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 19 Jan 2023 20:54:42 +0100 Subject: [PATCH 0008/1143] Display mined tx outputs as ShortIDs instead of full transaction outpoints. ShortIDs were originally designed for lightning channels, and are now understood by some block explorers. This allows to remove one column in the UTXO tab (height is redundant). In the transaction dialog, the space saving ensures that all inputs fit into one line (it was not the case previously with p2wsh addresses). For clarity and consistency, the ShortID is displayed for both inputs and outputs in the transaction dialog. --- electrum/address_synchronizer.py | 25 ++++++++---- electrum/gui/qt/transaction_dialog.py | 50 ++++++++++++----------- electrum/gui/qt/utxo_list.py | 8 +--- electrum/lnutil.py | 59 ++------------------------- electrum/transaction.py | 19 +++++++-- electrum/util.py | 59 +++++++++++++++++++++++++++ electrum/wallet.py | 2 +- 7 files changed, 124 insertions(+), 98 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index cc45c5826..9a8464c98 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -243,7 +243,14 @@ def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=Fa return conflicting_txns def get_transaction(self, txid: str) -> Transaction: - return self.db.get_transaction(txid) + tx = self.db.get_transaction(txid) + # add verified tx info + tx.deserialize() + for txin in tx._inputs: + tx_height, tx_pos = self.get_txpos(txin.prevout.txid.hex()) + txin.block_height = tx_height + txin.block_txpos = tx_pos + return tx def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool: """ @@ -768,9 +775,10 @@ def get_addr_io(self, address): received = {} sent = {} for tx_hash, height in h: + hh, pos = self.get_txpos(tx_hash) d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): - received[tx_hash + ':%d'%n] = (height, v, is_cb) + received[tx_hash + ':%d'%n] = (height, pos, v, is_cb) for tx_hash, height in h: l = self.db.get_txi_addr(tx_hash, address) for txi, v in l: @@ -778,17 +786,18 @@ def get_addr_io(self, address): return received, sent def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: - coins, spent = self.get_addr_io(address) + received, sent = self.get_addr_io(address) out = {} - for prevout_str, v in coins.items(): - tx_height, value, is_cb = v + for prevout_str, v in received.items(): + tx_height, tx_pos, value, is_cb = v prevout = TxOutpoint.from_str(prevout_str) utxo = PartialTxInput(prevout=prevout, is_coinbase_output=is_cb) utxo._trusted_address = address utxo._trusted_value_sats = value utxo.block_height = tx_height - if prevout_str in spent: - txid, height = spent[prevout_str] + utxo.block_txpos = tx_pos + if prevout_str in sent: + txid, height = sent[prevout_str] utxo.spent_txid = txid utxo.spent_height = height else: @@ -807,7 +816,7 @@ def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: # return the total amount ever received by an address def get_addr_received(self, address): received, sent = self.get_addr_io(address) - return sum([v for height, v, is_cb in received.values()]) + return sum([value for height, pos, value, is_cb in received.values()]) @with_local_height_cached def get_balance(self, domain, *, excluded_addresses: Set[str] = None, diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 0007b2212..0916af943 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -45,8 +45,9 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config -from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput +from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint from electrum.logging import get_logger +from electrum.util import ShortID from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog, @@ -593,8 +594,17 @@ def text_format(addr): return self.txo_color_2fa.text_char_format return ext - def format_amount(amt): - return self.main_window.format_amount(amt, whitespaces=True) + def insert_tx_io(cursor, is_coinbase, short_id, address, value): + if is_coinbase: + cursor.insertText('coinbase') + else: + address_str = address or '
' + value_str = self.main_window.format_amount(value, whitespaces=True) + cursor.insertText("%-15s\t"%str(short_id), ext) + cursor.insertText("%-62s"%address_str, text_format(address)) + cursor.insertText('\t', ext) + cursor.insertText(value_str, ext) + cursor.insertBlock() i_text = self.inputs_textedit i_text.clear() @@ -602,34 +612,26 @@ def format_amount(amt): i_text.setReadOnly(True) cursor = i_text.textCursor() for txin in self.tx.inputs(): - if txin.is_coinbase_input(): - cursor.insertText('coinbase') - else: - prevout_hash = txin.prevout.txid.hex() - prevout_n = txin.prevout.out_idx - cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext) - addr = self.wallet.adb.get_txin_address(txin) - if addr is None: - addr = '' - cursor.insertText(addr, text_format(addr)) - txin_value = self.wallet.adb.get_txin_value(txin) - if txin_value is not None: - cursor.insertText(format_amount(txin_value), ext) - cursor.insertBlock() + addr = self.wallet.adb.get_txin_address(txin) + txin_value = self.wallet.adb.get_txin_value(txin) + insert_tx_io(cursor, txin.is_coinbase_output(), txin.short_id, addr, txin_value) self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) o_text = self.outputs_textedit o_text.clear() o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) + tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) + tx_hash = bytes.fromhex(self.tx.txid()) cursor = o_text.textCursor() - for o in self.tx.outputs(): - addr, v = o.get_ui_address_str(), o.value - cursor.insertText(addr, text_format(addr)) - if v is not None: - cursor.insertText('\t', ext) - cursor.insertText(format_amount(v), ext) - cursor.insertBlock() + for index, o in enumerate(self.tx.outputs()): + if tx_pos is not None and tx_pos >= 0: + short_id = ShortID.from_components(tx_height, tx_pos, index) + else: + short_id = TxOutpoint(tx_hash, index).short_name() + + addr, value = o.get_ui_address_str(), o.value + insert_tx_io(cursor, False, short_id, addr, value) self.txo_color_recv.legend_label.setVisible(tf_used_recv) self.txo_color_change.legend_label.setVisible(tf_used_change) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 727e38d46..3c1d067e7 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -46,14 +46,12 @@ class Columns(IntEnum): ADDRESS = 1 LABEL = 2 AMOUNT = 3 - HEIGHT = 4 headers = { + Columns.OUTPOINT: _('Output point'), Columns.ADDRESS: _('Address'), Columns.LABEL: _('Label'), Columns.AMOUNT: _('Amount'), - Columns.HEIGHT: _('Height'), - Columns.OUTPOINT: _('Output point'), } filter_columns = [Columns.ADDRESS, Columns.LABEL, Columns.OUTPOINT] stretch_column = Columns.LABEL @@ -86,10 +84,8 @@ def update(self): name = utxo.prevout.to_str() self._utxo_dict[name] = utxo address = utxo.address - height = utxo.block_height - name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True) - labels = [name_short, address, '', amount, '%d'%height] + labels = [str(utxo.short_id), address, '', amount] utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 2b1f3a7ce..90c079aa9 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -13,6 +13,9 @@ from .util import bfh, bh2u, inv_dict, UserFacingException from .util import list_enabled_bits +from .util import ShortID as ShortChannelID +from .util import format_short_id as format_short_channel_id + from .crypto import sha256 from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, TxOutput) @@ -1487,63 +1490,7 @@ def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair: NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH -class ShortChannelID(bytes): - - def __repr__(self): - return f"" - - def __str__(self): - return format_short_channel_id(self) - - @classmethod - def from_components(cls, block_height: int, tx_pos_in_block: int, output_index: int) -> 'ShortChannelID': - bh = block_height.to_bytes(3, byteorder='big') - tpos = tx_pos_in_block.to_bytes(3, byteorder='big') - oi = output_index.to_bytes(2, byteorder='big') - return ShortChannelID(bh + tpos + oi) - - @classmethod - def from_str(cls, scid: str) -> 'ShortChannelID': - """Parses a formatted scid str, e.g. '643920x356x0'.""" - components = scid.split("x") - if len(components) != 3: - raise ValueError(f"failed to parse ShortChannelID: {scid!r}") - try: - components = [int(x) for x in components] - except ValueError: - raise ValueError(f"failed to parse ShortChannelID: {scid!r}") from None - return ShortChannelID.from_components(*components) - - @classmethod - def normalize(cls, data: Union[None, str, bytes, 'ShortChannelID']) -> Optional['ShortChannelID']: - if isinstance(data, ShortChannelID) or data is None: - return data - if isinstance(data, str): - assert len(data) == 16 - return ShortChannelID.fromhex(data) - if isinstance(data, (bytes, bytearray)): - assert len(data) == 8 - return ShortChannelID(data) - - @property - def block_height(self) -> int: - return int.from_bytes(self[:3], byteorder='big') - - @property - def txpos(self) -> int: - return int.from_bytes(self[3:6], byteorder='big') - - @property - def output_index(self) -> int: - return int.from_bytes(self[6:8], byteorder='big') - -def format_short_channel_id(short_channel_id: Optional[bytes]): - if not short_channel_id: - return _('Not yet available') - return str(int.from_bytes(short_channel_id[:3], 'big')) \ - + 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \ - + 'x' + str(int.from_bytes(short_channel_id[6:], 'big')) @attr.s(frozen=True) diff --git a/electrum/transaction.py b/electrum/transaction.py index 0fc7d713a..763d003d0 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -51,6 +51,7 @@ base_encode, construct_witness, construct_script) from .crypto import sha256d from .logging import get_logger +from .util import ShortID if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -212,6 +213,9 @@ def serialize_to_network(self) -> bytes: def is_coinbase(self) -> bool: return self.txid == bytes(32) + def short_name(self): + return f"{self.txid.hex()[0:10]}:{self.out_idx}" + class TxInput: prevout: TxOutpoint @@ -231,6 +235,18 @@ def __init__(self, *, self.nsequence = nsequence self.witness = witness self._is_coinbase_output = is_coinbase_output + # blockchain fields + self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown + self.block_txpos = None + self.spent_height = None # type: Optional[int] # height at which the TXO got spent + self.spent_txid = None # type: Optional[str] # txid of the spender + + @property + def short_id(self): + if self.block_txpos is not None and self.block_txpos >= 0: + return ShortID.from_components(self.block_height, self.block_txpos, self.prevout.out_idx) + else: + return self.prevout.short_name() def is_coinbase_input(self) -> bool: """Whether this is the input of a coinbase tx.""" @@ -1227,9 +1243,6 @@ def __init__(self, *args, **kwargs): self.pubkeys = [] # type: List[bytes] # note: order matters self._trusted_value_sats = None # type: Optional[int] self._trusted_address = None # type: Optional[str] - self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown - self.spent_height = None # type: Optional[int] # height at which the TXO got spent - self.spent_txid = None # type: Optional[str] # txid of the spender self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown self._is_native_segwit = None # type: Optional[bool] # None means unknown self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est diff --git a/electrum/util.py b/electrum/util.py index a4388fd28..ba3738e35 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1286,6 +1286,65 @@ class TxMinedInfo(NamedTuple): header_hash: Optional[str] = None # hash of block that mined tx +class ShortID(bytes): + + def __repr__(self): + return f"" + + def __str__(self): + return format_short_id(self) + + @classmethod + def from_components(cls, block_height: int, tx_pos_in_block: int, output_index: int) -> 'ShortID': + bh = block_height.to_bytes(3, byteorder='big') + tpos = tx_pos_in_block.to_bytes(3, byteorder='big') + oi = output_index.to_bytes(2, byteorder='big') + return ShortID(bh + tpos + oi) + + @classmethod + def from_str(cls, scid: str) -> 'ShortID': + """Parses a formatted scid str, e.g. '643920x356x0'.""" + components = scid.split("x") + if len(components) != 3: + raise ValueError(f"failed to parse ShortID: {scid!r}") + try: + components = [int(x) for x in components] + except ValueError: + raise ValueError(f"failed to parse ShortID: {scid!r}") from None + return ShortID.from_components(*components) + + @classmethod + def normalize(cls, data: Union[None, str, bytes, 'ShortChannelID']) -> Optional['ShortChannelID']: + if isinstance(data, ShortID) or data is None: + return data + if isinstance(data, str): + assert len(data) == 16 + return ShortID.fromhex(data) + if isinstance(data, (bytes, bytearray)): + assert len(data) == 8 + return ShortID(data) + + @property + def block_height(self) -> int: + return int.from_bytes(self[:3], byteorder='big') + + @property + def txpos(self) -> int: + return int.from_bytes(self[3:6], byteorder='big') + + @property + def output_index(self) -> int: + return int.from_bytes(self[6:8], byteorder='big') + + +def format_short_id(short_channel_id: Optional[bytes]): + if not short_channel_id: + return _('Not yet available') + return str(int.from_bytes(short_channel_id[:3], 'big')) \ + + 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \ + + 'x' + str(int.from_bytes(short_channel_id[6:], 'big')) + + def make_aiohttp_session(proxy: Optional[dict], headers=None, timeout=None): if headers is None: headers = {'User-Agent': 'Electrum'} diff --git a/electrum/wallet.py b/electrum/wallet.py index 34258778f..f6ef08580 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2116,7 +2116,7 @@ def _add_input_utxo_info( received, spent = self.adb.get_addr_io(address) item = received.get(txin.prevout.to_str()) if item: - txin_value = item[1] + txin_value = item[2] txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value) if txin.utxo is None: txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=ignore_network_issues) From 7625b4e63b3ea2e8f6283619212f9419efa63985 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 26 Jan 2023 11:13:35 +0100 Subject: [PATCH 0009/1143] follow-up d6febb5c1243f3f80d5a79af9aa39312c8166c91 --- electrum/address_synchronizer.py | 13 +++++++------ electrum/util.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 9a8464c98..efbe3552c 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -244,12 +244,13 @@ def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=Fa def get_transaction(self, txid: str) -> Transaction: tx = self.db.get_transaction(txid) - # add verified tx info - tx.deserialize() - for txin in tx._inputs: - tx_height, tx_pos = self.get_txpos(txin.prevout.txid.hex()) - txin.block_height = tx_height - txin.block_txpos = tx_pos + if tx: + # add verified tx info + tx.deserialize() + for txin in tx._inputs: + tx_height, tx_pos = self.get_txpos(txin.prevout.txid.hex()) + txin.block_height = tx_height + txin.block_txpos = tx_pos return tx def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool: diff --git a/electrum/util.py b/electrum/util.py index ba3738e35..622538074 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1289,7 +1289,7 @@ class TxMinedInfo(NamedTuple): class ShortID(bytes): def __repr__(self): - return f"" + return f"" def __str__(self): return format_short_id(self) @@ -1314,7 +1314,7 @@ def from_str(cls, scid: str) -> 'ShortID': return ShortID.from_components(*components) @classmethod - def normalize(cls, data: Union[None, str, bytes, 'ShortChannelID']) -> Optional['ShortChannelID']: + def normalize(cls, data: Union[None, str, bytes, 'ShortID']) -> Optional['ShortID']: if isinstance(data, ShortID) or data is None: return data if isinstance(data, str): From fd11b9189e1786d5433521b90cede03720f161d2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 26 Jan 2023 12:59:31 +0100 Subject: [PATCH 0010/1143] qml: really disambiguate text prefs item onchain fallback address --- electrum/gui/qml/components/Preferences.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index ffe8169f6..0471e0f2b 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -313,7 +313,7 @@ Pane { } Label { Layout.fillWidth: true - text: qsTr('Use onchain fallback address for Lightning payment requests') + text: qsTr('Create lightning invoices with on-chain fallback address') wrapMode: Text.Wrap } } From 697c700a1f42292df12c23651ea5e48033b33cc0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 26 Jan 2023 11:40:41 +0000 Subject: [PATCH 0011/1143] release process: split release.sh into two This allows the add_cosigner workflow to be done before the website links to new binaries. (so Emzy or other builders can try to reproduce builds and add signatures before new binaries are fully live) --- contrib/add_cosigner | 13 ++++++--- contrib/release.sh | 49 +++++++++++++++------------------ contrib/release_www.sh | 57 +++++++++++++++++++++++++++++++++++++++ contrib/trigger_deploy.sh | 35 ++++++++++++++++++++++++ contrib/upload.sh | 18 ++++++++----- 5 files changed, 135 insertions(+), 37 deletions(-) create mode 100755 contrib/release_www.sh create mode 100755 contrib/trigger_deploy.sh diff --git a/contrib/add_cosigner b/contrib/add_cosigner index 2e9b7d936..63f26f2e3 100755 --- a/contrib/add_cosigner +++ b/contrib/add_cosigner @@ -3,12 +3,15 @@ # This script is part of the workflow for BUILDERs to reproduce and sign the # release binaries. (for builders who do not have sftp access to "electrum-downloads-airlock") # +# env vars: +# - SSHUSER +# +# # - BUILDER builds all binaries and checks they match the official releases # (using release.sh, and perhaps some manual steps) # - BUILDER creates a PR against https://github.com/spesmilo/electrum-signatures/ # to add their sigs for a given release, which then gets merged -# - SFTPUSER runs `$ electrum/contrib/add_cosigner $BUILDER` -# - SFTPUSER runs `$ SSHUSER=$SFTPUSER electrum/contrib/upload.sh` +# - SFTPUSER runs `$ SSHUSER=$SFTPUSER electrum/contrib/add_cosigner $BUILDER` # - SFTPUSER runs `$ electrum/contrib/make_download $WWW_DIR` # - $ (cd $WWW_DIR; git commit -a -m "add_cosigner"; git push) # - SFTPUSER runs `$ electrum-web/publish.sh $SFTPUSER` @@ -18,6 +21,7 @@ import re import os import sys import importlib +import subprocess # cd to project root @@ -32,7 +36,7 @@ ELECTRUM_VERSION = version_module.ELECTRUM_VERSION APK_VERSION = version_module.APK_VERSION print("version", ELECTRUM_VERSION) -# GPG names of cosigner +# GPG name of cosigner cosigner = sys.argv[1] version = version_win = version_mac = ELECTRUM_VERSION @@ -63,3 +67,6 @@ for shortname, filename in files.items(): os.system(f"wget -nc {sig_url} -O {sig_path}") if os.system(f"gpg --verify {sig_path} {path}") != 0: raise Exception(sig_name) + +print("Calling upload.sh now... This might take some time.") +subprocess.check_output(["./contrib/upload.sh", ]) diff --git a/contrib/release.sh b/contrib/release.sh index e34b687a2..b08becb71 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -1,23 +1,19 @@ #!/bin/bash # -# This script, for the RELEASEMANAGER: -# - builds and uploads all binaries, +# This script is used for stage 1 of the release process. It operates exclusively on the airlock. +# This script, for the RELEASEMANAGER (RM): +# - builds and uploads all binaries to airlock, # - assumes all keys are available, and signs everything # This script, for other builders: # - builds all reproducible binaries, -# - downloads binaries built by the release manager, compares and signs them, +# - downloads binaries built by the release manager (from airlock), compares and signs them, # - and then uploads sigs # Note: the .dmg should be built separately beforehand and copied into dist/ # (as it is built on a separate machine) # +# # env vars: # - ELECBUILD_NOCACHE: if set, forces rebuild of docker images -# - WWW_DIR: path to "electrum-web" git clone -# -# additional env vars for the RELEASEMANAGER: -# - for signing the version announcement file: -# - ELECTRUM_SIGNING_ADDRESS (required) -# - ELECTRUM_SIGNING_WALLET (required) # # "uploadserver" is set in /etc/hosts # @@ -29,6 +25,20 @@ # - update RELEASE-NOTES and version.py # - $ git tag -s $VERSION -m $VERSION # +# ----- +# Then, typical release flow: +# - RM runs release.sh +# - Another SFTPUSER BUILDER runs `$ ./release.sh` +# - now airlock contains new binaries and two sigs for each +# - deploy.sh will verify sigs and move binaries across airlock +# - new binaries are now publicly available on uploadserver, but not linked from website yet +# - other BUILDERS can now also try to reproduce binaries and open PRs with sigs against spesmilo/electrum-signatures +# - these PRs can get merged as they come +# - run add_cosigner +# - after some time, RM can run release_www.sh to create and commit website-update +# - then run WWW_DIR/publish.sh to update website +# - at least two people need to run WWW_DIR/publish.sh +# set -e @@ -42,10 +52,6 @@ cd "$PROJECT_ROOT" # rm -rf dist/* # rm -f .buildozer -if [ -z "$WWW_DIR" ] ; then - WWW_DIR=/opt/electrum-web -fi - GPGUSER=$1 if [ -z "$GPGUSER" ]; then fail "usage: $0 gpg_username" @@ -247,13 +253,6 @@ else cd "$PROJECT_ROOT" - info "updating www repo" - ./contrib/make_download $WWW_DIR - info "signing the version announcement file" - sig=$(./run_electrum -o signmessage $ELECTRUM_SIGNING_ADDRESS $VERSION -w $ELECTRUM_SIGNING_WALLET) - echo "{ \"version\":\"$VERSION\", \"signatures\":{ \"$ELECTRUM_SIGNING_ADDRESS\":\"$sig\"}}" > $WWW_DIR/version - - if [ $REV != $VERSION ]; then fail "versions differ, not uploading" fi @@ -266,14 +265,10 @@ else touch dist/uploaded fi - # push changes to website repo - pushd $WWW_DIR - git diff - git commit -a -m "version $VERSION" - git push - popd fi info "release.sh finished successfully." -info "now you should run WWW_DIR/publish.sh to sign the website commit and upload signature" +info "After two people ran release.sh, the binaries will be publicly available on uploadserver." +info "Then, we wait for additional signers, and run add_cosigner for them." +info "Finally, release_www.sh needs to be run, for the website to be updated." diff --git a/contrib/release_www.sh b/contrib/release_www.sh new file mode 100755 index 000000000..b284a9517 --- /dev/null +++ b/contrib/release_www.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# +# env vars: +# - WWW_DIR: path to "electrum-web" git clone +# - for signing the version announcement file: +# - ELECTRUM_SIGNING_ADDRESS (required) +# - ELECTRUM_SIGNING_WALLET (required) +# + +set -e + +PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/.." +CONTRIB="$PROJECT_ROOT/contrib" + +cd "$PROJECT_ROOT" + +. "$CONTRIB"/build_tools_util.sh + + +echo -n "Remember to run add_cosigner to add any additional sigs. Continue (y/n)? " +read answer +if [ "$answer" != "y" ]; then + echo "exit" + exit 1 +fi + + +if [ -z "$WWW_DIR" ] ; then + WWW_DIR=/opt/electrum-web +fi + +if [ -z "$ELECTRUM_SIGNING_WALLET" ] || [ -z "$ELECTRUM_SIGNING_ADDRESS" ]; then + echo "You need to set env vars ELECTRUM_SIGNING_WALLET and ELECTRUM_SIGNING_ADDRESS!" + exit 1 +fi + +VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") +info "VERSION: $VERSION" + +set -x + +info "updating www repo" +./contrib/make_download "$WWW_DIR" +info "signing the version announcement file" +sig=$(./run_electrum -o signmessage $ELECTRUM_SIGNING_ADDRESS $VERSION -w $ELECTRUM_SIGNING_WALLET) +echo "{ \"version\":\"$VERSION\", \"signatures\":{ \"$ELECTRUM_SIGNING_ADDRESS\":\"$sig\"}}" > "$WWW_DIR"/version + +# push changes to website repo +pushd "$WWW_DIR" +git diff +git commit -a -m "version $VERSION" +git push +popd + + +info "release_www.sh finished successfully." +info "now you should run WWW_DIR/publish.sh to sign the website commit and upload signature" diff --git a/contrib/trigger_deploy.sh b/contrib/trigger_deploy.sh new file mode 100755 index 000000000..fc4a0df78 --- /dev/null +++ b/contrib/trigger_deploy.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Triggers deploy.sh to maybe update the website or move binaries. +# uploadserver needs to be defined in /etc/hosts + +SSHUSER=$1 +TRIGGERVERSION=$2 +if [ -z $SSHUSER ] || [ -z TRIGGERVERSION ]; then + echo "usage: $0 SSHUSER TRIGGERVERSION" + echo "e.g. $0 thomasv 3.0.0" + echo "e.g. $0 thomasv website" + exit 1 +fi +set -ex +cd "$(dirname "$0")" + +if [ "$TRIGGERVERSION" == "website" ]; then + rm -f trigger_website + touch trigger_website + echo "uploading file: trigger_website..." + sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" << ! + cd electrum-downloads-airlock + mput trigger_website + bye +! +else + rm -f trigger_binaries + printf "$TRIGGERVERSION" > trigger_binaries + echo "uploading file: trigger_binaries..." + sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" << ! + cd electrum-downloads-airlock + mput trigger_binaries + bye +! +fi + diff --git a/contrib/upload.sh b/contrib/upload.sh index 4d8d41e17..1e2675a67 100755 --- a/contrib/upload.sh +++ b/contrib/upload.sh @@ -5,9 +5,10 @@ # - ELECBUILD_UPLOADFROM # - SSHUSER -set -e +set -ex PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/.." +CONTRIB="$PROJECT_ROOT/contrib" if [ -z "$SSHUSER" ]; then SSHUSER=thomasv @@ -15,8 +16,8 @@ fi cd "$PROJECT_ROOT" -version=$(git describe --tags --abbrev=0) -echo $version +VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") +echo "$VERSION" if [ -z "$ELECBUILD_UPLOADFROM" ]; then cd "$PROJECT_ROOT/dist" @@ -30,9 +31,12 @@ fi sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" << ! cd electrum-downloads-airlock - -mkdir "$version" - -chmod 777 "$version" - cd "$version" - mput * + -mkdir "$VERSION" + -chmod 777 "$VERSION" + cd "$VERSION" + -mput * + -chmod 444 * # this prevents future re-uploads of same file bye ! + +"$CONTRIB/trigger_deploy.sh" "$SSHUSER" "$VERSION" From 5edd17724f5d840be6028a3c338aa11eeaa7828a Mon Sep 17 00:00:00 2001 From: ghost43 Date: Thu, 26 Jan 2023 14:47:39 +0000 Subject: [PATCH 0012/1143] CI: bump available memory for unit tests (1G->2G) (#8166) Tasks recently started spuriously getting killed with "Container errored with 'OOMKilled'". Not sure what changed, but this seems like the easiest fix. --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 9b471132a..0f93e7c94 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -2,7 +2,7 @@ task: container: image: $ELECTRUM_IMAGE cpu: 1 - memory: 1G + memory: 2G matrix: - name: Tox Python $ELECTRUM_PYTHON_VERSION env: From dec397af952035676d4a7d415391f0d00bfc0afa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 26 Jan 2023 15:51:48 +0100 Subject: [PATCH 0013/1143] update payserver submodule --- electrum/plugins/payserver/www | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/payserver/www b/electrum/plugins/payserver/www index 0b1e1664d..bde9d3b5f 160000 --- a/electrum/plugins/payserver/www +++ b/electrum/plugins/payserver/www @@ -1 +1 @@ -Subproject commit 0b1e1664d13fb35931cb4b1577a2a4303a10a767 +Subproject commit bde9d3b5fbf34623ca04c14eb6b0db6676c5ec52 From d7c723b97571b74b530f9d5b39b13b6ca6b87e65 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 26 Jan 2023 15:17:59 +0000 Subject: [PATCH 0014/1143] prepare release 4.3.4 --- RELEASE-NOTES | 15 +++++++++++++++ electrum/version.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 03f782d15..43cd0f729 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,18 @@ +# Release 4.3.4 - Copyright is Dubious (January 26, 2023) + * Lightning: + - make sending trampoline payments more reliable (5251e7f8) + - use different trampoline feature bits than eclair (#8141) + * invoice-handling: fix get_request_by_addr incorrectly mapping + addresses to request ids when an address was reused (#8113) + * fix a deadlock in wallet.py (52e2da3a) + * CLI: detect if daemon is already running (c7e2125f) + * add an AppStream metainfo.xml file for Linux packagers (#8149) + * payserver plugin: + -replaced vendored qrcode lib + -added tabs for on-chain and lightning invoices + -revamped html and javascript + + # Release 4.3.3 - (January 3, 2023) * Lightning: - fix handling failed HTLCs in gossip-based routing (#7995) diff --git a/electrum/version.py b/electrum/version.py index 622155f31..4f2b93b60 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.3.3' # version of the client package -APK_VERSION = '4.3.3.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.3.4' # version of the client package +APK_VERSION = '4.3.4.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 563180c94c9b5d07970effa523b4c12a0aaa64ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 26 Jan 2023 18:15:04 +0000 Subject: [PATCH 0015/1143] build: scripts to not require electrum to be installed --- contrib/build-wine/unsign.sh | 2 +- contrib/print_electrum_version.py | 22 ++++++++++++++++++++++ contrib/release.sh | 2 +- contrib/release_www.sh | 2 +- contrib/upload.sh | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100755 contrib/print_electrum_version.py diff --git a/contrib/build-wine/unsign.sh b/contrib/build-wine/unsign.sh index 3beb30642..8cfc4bb3d 100755 --- a/contrib/build-wine/unsign.sh +++ b/contrib/build-wine/unsign.sh @@ -14,7 +14,7 @@ set -e mkdir -p signed >/dev/null 2>&1 mkdir -p signed/stripped >/dev/null 2>&1 -version=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") +version=$("$CONTRIB"/print_electrum_version.py) echo "Found $(ls dist/*.exe | wc -w) files to verify." diff --git a/contrib/print_electrum_version.py b/contrib/print_electrum_version.py new file mode 100755 index 000000000..4e9aba79f --- /dev/null +++ b/contrib/print_electrum_version.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# For usage in shell, to get the version of electrum, without needing electrum installed. +# For example: +# $ VERSION=$("$CONTRIB"/print_electrum_version.py) +# instead of +# $ VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") + +import importlib.util +import os + + +if __name__ == '__main__': + project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + version_file_path = os.path.join(project_root, "electrum", "version.py") + + # load version.py; needlessly complicated alternative to "imp.load_source": + version_spec = importlib.util.spec_from_file_location('version', version_file_path) + version_module = version = importlib.util.module_from_spec(version_spec) + version_spec.loader.exec_module(version_module) + + print(version.ELECTRUM_VERSION) + diff --git a/contrib/release.sh b/contrib/release.sh index b08becb71..297fea60c 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -79,7 +79,7 @@ if [ ! -z "$RELEASEMANAGER" ] ; then fi -VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") +VERSION=$("$CONTRIB"/print_electrum_version.py) info "VERSION: $VERSION" REV=$(git describe --tags) info "REV: $REV" diff --git a/contrib/release_www.sh b/contrib/release_www.sh index b284a9517..8f8cf60fe 100755 --- a/contrib/release_www.sh +++ b/contrib/release_www.sh @@ -34,7 +34,7 @@ if [ -z "$ELECTRUM_SIGNING_WALLET" ] || [ -z "$ELECTRUM_SIGNING_ADDRESS" ]; then exit 1 fi -VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") +VERSION=$("$CONTRIB"/print_electrum_version.py) info "VERSION: $VERSION" set -x diff --git a/contrib/upload.sh b/contrib/upload.sh index 1e2675a67..b161bd21f 100755 --- a/contrib/upload.sh +++ b/contrib/upload.sh @@ -16,7 +16,7 @@ fi cd "$PROJECT_ROOT" -VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") +VERSION=$("$CONTRIB"/print_electrum_version.py) echo "$VERSION" if [ -z "$ELECBUILD_UPLOADFROM" ]; then From ee2e25569969d35301bd494450a4b3728f4784b6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 26 Jan 2023 18:30:11 +0000 Subject: [PATCH 0016/1143] contrib/build-wine/unsign.sh: small improvements/fixes - follow-up prev: CONTRIB was not defined - rm folder signed/stripped if already exists (otherwise script early-exited silently) - quote paths to guard against whitespace shenanigans --- contrib/build-wine/unsign.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contrib/build-wine/unsign.sh b/contrib/build-wine/unsign.sh index 8cfc4bb3d..ec7ca1094 100755 --- a/contrib/build-wine/unsign.sh +++ b/contrib/build-wine/unsign.sh @@ -1,4 +1,7 @@ #!/bin/bash + +PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." +CONTRIB="$PROJECT_ROOT/contrib" here=$(dirname "$0") test -n "$here" -a -d "$here" || exit cd $here @@ -11,6 +14,7 @@ fi # exit if command fails set -e +rm -rf signed/stripped mkdir -p signed >/dev/null 2>&1 mkdir -p signed/stripped >/dev/null 2>&1 @@ -20,18 +24,18 @@ echo "Found $(ls dist/*.exe | wc -w) files to verify." for mine in $(ls dist/*.exe); do echo "---------------" - f=$(basename $mine) - if test -f signed/$f; then + f="$(basename $mine)" + if test -f "signed/$f"; then echo "Found file at signed/$f" else echo "Downloading https://download.electrum.org/$version/$f" - wget -q https://download.electrum.org/$version/$f -O signed/$f + wget -q "https://download.electrum.org/$version/$f" -O "signed/$f" fi out="signed/stripped/$f" # Remove PE signature from signed binary - osslsigncode remove-signature -in signed/$f -out $out > /dev/null 2>&1 - chmod +x $out - if cmp -s $out $mine; then + osslsigncode remove-signature -in "signed/$f" -out "$out" > /dev/null 2>&1 + chmod +x "$out" + if cmp -s "$out" "$mine"; then echo "Success: $f" #gpg --sign --armor --detach signed/$f else From a8f1d1c32655552aa3a93a897bf621513d0f2974 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 27 Jan 2023 11:44:46 +0100 Subject: [PATCH 0017/1143] qml: remember user selected request type and pre-select on subsequent payment requests --- electrum/gui/qml/components/ReceiveDialog.qml | 10 +++++++++- electrum/gui/qml/qeconfig.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index de42cc39c..b03e57c14 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -125,6 +125,7 @@ ElDialog { else if (_bip21uri != '') rootLayout.state = 'bip21uri' } + Config.preferredRequestType = rootLayout.state } } } @@ -348,7 +349,14 @@ ElDialog { id: request wallet: Daemon.currentWallet onDetailsChanged: { - if (bolt11) { + var req_type = Config.preferredRequestType + if (bolt11 && req_type == 'bolt11') { + rootLayout.state = 'bolt11' + } else if (bip21 && req_type == 'bip21uri') { + rootLayout.state = 'bip21uri' + } else if (req_type == 'address') { + rootLayout.state = 'address' + } else if (bolt11) { rootLayout.state = 'bolt11' } else if (bip21) { rootLayout.state = 'bip21uri' diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 99fb74dee..ec0e354e5 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -167,6 +167,17 @@ def trustedcoinPrepay(self, num_prepay): self.config.set_key('trustedcoin_prepay', num_prepay) self.trustedcoinPrepayChanged.emit() + preferredRequestTypeChanged = pyqtSignal() + @pyqtProperty(str, notify=preferredRequestTypeChanged) + def preferredRequestType(self): + return self.config.get('preferred_request_type', 'bolt11') + + @preferredRequestType.setter + def preferredRequestType(self, preferred_request_type): + if preferred_request_type != self.config.get('preferred_request_type', 'bolt11'): + self.config.set_key('preferred_request_type', preferred_request_type) + self.preferredRequestTypeChanged.emit() + @pyqtSlot('qint64', result=str) @pyqtSlot('qint64', bool, result=str) @pyqtSlot(QEAmount, result=str) From 497267bd340aa2c97239f98d78c136f03f6c0f55 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 27 Jan 2023 11:03:31 +0000 Subject: [PATCH 0018/1143] release process: don't create "uploaded" marker file in release.sh No longer needed with the "chmod 444 *" trick in 697c700a1f42292df12c23651ea5e48033b33cc0. (it is now cheap to re-run upload.sh, it no longer redundantly re-uploads hundreds of MBs) --- contrib/release.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/contrib/release.sh b/contrib/release.sh index 297fea60c..cb325bdea 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -258,12 +258,7 @@ else fi # upload the files - if test -f dist/uploaded; then - info "files already uploaded" - else - ./contrib/upload.sh - touch dist/uploaded - fi + ./contrib/upload.sh fi From 49061f5420bc47eddda4b23987f4517accf6c9e8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 27 Jan 2023 11:05:32 +0000 Subject: [PATCH 0019/1143] release process: fix typo in trigger_deploy.sh --- contrib/trigger_deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/trigger_deploy.sh b/contrib/trigger_deploy.sh index fc4a0df78..63283612a 100755 --- a/contrib/trigger_deploy.sh +++ b/contrib/trigger_deploy.sh @@ -4,7 +4,7 @@ SSHUSER=$1 TRIGGERVERSION=$2 -if [ -z $SSHUSER ] || [ -z TRIGGERVERSION ]; then +if [ -z "$SSHUSER" ] || [ -z "$TRIGGERVERSION" ]; then echo "usage: $0 SSHUSER TRIGGERVERSION" echo "e.g. $0 thomasv 3.0.0" echo "e.g. $0 thomasv website" From d6faeb411ad866ab6f17d67b096399738bb15b94 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 27 Jan 2023 12:21:44 +0100 Subject: [PATCH 0020/1143] qml: BalanceSummary only show Lightning values when wallet is lightning --- .../qml/components/controls/BalanceSummary.qml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index f3c0b51d5..e3ef3afba 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -69,6 +69,7 @@ Item { text: Daemon.fx.fiatCurrency } RowLayout { + visible: Daemon.currentWallet.isLightning Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall @@ -81,35 +82,38 @@ Item { } } Label { + visible: Daemon.currentWallet.isLightning Layout.alignment: Qt.AlignRight text: formattedLightningCanReceive font.family: FixedFont } Label { + visible: Daemon.currentWallet.isLightning font.pixelSize: constants.fontSizeSmall color: Material.accentColor text: Config.baseUnit } Item { - visible: Daemon.fx.enabled + visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled Layout.preferredHeight: 1 Layout.preferredWidth: 1 } Label { Layout.alignment: Qt.AlignRight - visible: Daemon.fx.enabled + visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled font.pixelSize: constants.fontSizeSmall color: constants.mutedForeground text: formattedLightningCanReceiveFiat } Label { - visible: Daemon.fx.enabled + visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled font.pixelSize: constants.fontSizeSmall color: constants.mutedForeground text: Daemon.fx.fiatCurrency } RowLayout { + visible: Daemon.currentWallet.isLightning Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall @@ -122,29 +126,31 @@ Item { } } Label { + visible: Daemon.currentWallet.isLightning Layout.alignment: Qt.AlignRight text: formattedLightningCanSend font.family: FixedFont } Label { + visible: Daemon.currentWallet.isLightning font.pixelSize: constants.fontSizeSmall color: Material.accentColor text: Config.baseUnit } Item { - visible: Daemon.fx.enabled + visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled Layout.preferredHeight: 1 Layout.preferredWidth: 1 } Label { Layout.alignment: Qt.AlignRight - visible: Daemon.fx.enabled + visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled font.pixelSize: constants.fontSizeSmall color: constants.mutedForeground text: formattedLightningCanSendFiat } Label { - visible: Daemon.fx.enabled + visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled font.pixelSize: constants.fontSizeSmall color: constants.mutedForeground text: Daemon.fx.fiatCurrency From c747182122a61f2dc6ed5529dcf7a96dc24c60f8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 27 Jan 2023 12:38:14 +0100 Subject: [PATCH 0021/1143] qml: make sure ExceptionDialog is always on top --- electrum/gui/qml/components/main.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 523cb73cb..ad8bc55fb 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -254,7 +254,9 @@ ApplicationWindow Component { id: crashDialog - ExceptionDialog {} + ExceptionDialog { + z: 1000 + } } Component.onCompleted: { From 8ed9a22793def0e9fc59d362bd9b9df28059c4bc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 27 Jan 2023 13:25:36 +0100 Subject: [PATCH 0022/1143] qml: handle non-deterministic wallet address selection when generating payment request --- electrum/gui/qml/components/ReceiveDialog.qml | 28 +++++++++---- electrum/gui/qml/qewallet.py | 42 ++++++++++--------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index b03e57c14..22a0fcc03 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -20,6 +20,8 @@ ElDialog { property bool _render_qr: false // delay qr rendering until dialog is shown property bool _ispaid: false + property bool _ignore_gaplimit: false + property bool _reuse_address: false parent: Overlay.overlay modal: true @@ -310,20 +312,20 @@ ElDialog { } } - function createRequest(ignoreGaplimit = false) { + function createRequest() { var qamt = Config.unitsToSats(receiveDetailsDialog.amount) if (qamt.satsInt > Daemon.currentWallet.lightningCanReceive.satsInt) { console.log('Creating OnChain request') - Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, false, ignoreGaplimit) + Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, false, _ignore_gaplimit, _reuse_address) } else { console.log('Creating Lightning request') - Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, true) + Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, true, _ignore_gaplimit, _reuse_address) } } - function createDefaultRequest(ignoreGaplimit = false) { + function createDefaultRequest() { console.log('Creating default request') - Daemon.currentWallet.createDefaultRequest(ignoreGaplimit) + Daemon.currentWallet.createDefaultRequest(_ignore_gaplimit, _reuse_address) } Connections { @@ -333,13 +335,20 @@ ElDialog { } function onRequestCreateError(code, error) { if (code == 'gaplimit') { - var dialog = app.messageDialog.createObject(app, {'text': error, 'yesno': true}) + var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) dialog.yesClicked.connect(function() { - createDefaultRequest(true) + _ignore_gaplimit = true + createDefaultRequest() + }) + } else if (code == 'non-deterministic') { + var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) + dialog.yesClicked.connect(function() { + _reuse_address = true + createDefaultRequest() }) } else { console.log(error) - var dialog = app.messageDialog.createObject(app, {'text': error}) + var dialog = app.messageDialog.createObject(app, {text: error}) } dialog.open() } @@ -388,7 +397,8 @@ ElDialog { } Component.onCompleted: { - createDefaultRequest() + // callLater to make sure any popups are on top of the dialog stacking order + Qt.callLater(createDefaultRequest) } // hack. delay qr rendering until dialog is shown diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 65c4ace72..15871546a 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -2,7 +2,7 @@ import queue import threading import time -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple from functools import partial from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer @@ -571,21 +571,20 @@ def pay_thread(): threading.Thread(target=pay_thread).start() - def create_bitcoin_request(self, amount: int, message: str, expiration: int, ignore_gap: bool) -> Optional[str]: + def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, ignore_gap: bool = False, reuse_address: bool = False) -> Optional[Tuple]: addr = self.wallet.get_unused_address() if addr is None: if not self.wallet.is_deterministic(): # imported wallet - # TODO implement - return - #msg = [ - #_('No more addresses in your wallet.'), ' ', - #_('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ', - #_('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n', - #_('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'), - #] - #if not self.question(''.join(msg)): - #return - #addr = self.wallet.get_receiving_address() + if not reuse_address: + msg = [ + _('No more addresses in your wallet.'), ' ', + _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ', + _('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n', + _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'), + ] + self.requestCreateError.emit('non-deterministic',''.join(msg)) + return + addr = self.wallet.get_receiving_address() else: # deterministic wallet if not ignore_gap: self.requestCreateError.emit('gaplimit',_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")) @@ -593,6 +592,7 @@ def create_bitcoin_request(self, amount: int, message: str, expiration: int, ign addr = self.wallet.create_new_address(False) req_key = self.wallet.create_request(amount, message, expiration, addr) + self._logger.debug(f'created request with key {req_key}') #try: #self.wallet.add_payment_request(req) #except Exception as e: @@ -608,8 +608,8 @@ def create_bitcoin_request(self, amount: int, message: str, expiration: int, ign @pyqtSlot(QEAmount, str, int) @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) - def createRequest(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False): - # TODO: unify this method and create_bitcoin_request + @pyqtSlot(QEAmount, str, int, bool, bool, bool) + def createRequest(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False, reuse_address: bool = False): try: if is_lightning: if not self.wallet.lnworker.channels: @@ -620,10 +620,10 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, is_ligh addr = self.wallet.get_unused_address() key = self.wallet.create_request(amount.satsInt, message, expiration, addr) else: - key, addr = self.create_bitcoin_request(amount.satsInt, message, expiration, ignore_gap) + key, addr = self.create_bitcoin_request(amount.satsInt, message, expiration, ignore_gap=ignore_gap, reuse_address=reuse_address) if not key: return - self.addressModel.init_model() + self.addressModel.setDirty() except InvoiceError as e: self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e)) return @@ -634,7 +634,8 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, is_ligh @pyqtSlot() @pyqtSlot(bool) - def createDefaultRequest(self, ignore_gap: bool = False): + @pyqtSlot(bool, bool) + def createDefaultRequest(self, ignore_gap: bool = False, reuse_address: bool = False): try: default_expiry = self.wallet.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) if self.wallet.lnworker and self.wallet.lnworker.channels: @@ -645,9 +646,10 @@ def createDefaultRequest(self, ignore_gap: bool = False): pass key = self.wallet.create_request(None, None, default_expiry, addr) else: - key, addr = self.create_bitcoin_request(None, None, default_expiry, ignore_gap) - if not key: + req = self.create_bitcoin_request(None, None, default_expiry, ignore_gap=ignore_gap, reuse_address=reuse_address) + if not req: return + key, addr = req except InvoiceError as e: self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e)) return From 8b7743c4bfb7fdbf0d07c999444c74da15b0ebe3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 27 Jan 2023 14:54:45 +0100 Subject: [PATCH 0023/1143] qml: show menu also when no wallet loaded --- .../gui/qml/components/WalletMainView.qml | 89 +++++++++++-------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 6f21f2ca7..d78cb72bf 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -101,51 +101,57 @@ Item { } ColumnLayout { - anchors.centerIn: parent - width: parent.width - spacing: 2*constants.paddingXLarge - visible: !Daemon.currentWallet - - Label { - text: qsTr('No wallet loaded') - font.pixelSize: constants.fontSizeXXLarge - Layout.alignment: Qt.AlignHCenter + anchors.fill: parent + + History { + id: history + visible: Daemon.currentWallet + Layout.fillWidth: true + Layout.fillHeight: true } - Pane { + ColumnLayout { Layout.alignment: Qt.AlignHCenter - padding: 0 - background: Rectangle { - color: Material.dialogColor + Layout.fillHeight: true + spacing: 2*constants.paddingXLarge + visible: !Daemon.currentWallet + + Item { + Layout.fillHeight: true } - FlatButton { - text: qsTr('Open/Create Wallet') - icon.source: '../../icons/wallet.png' - onClicked: { - if (Daemon.availableWallets.rowCount() > 0) { - stack.push(Qt.resolvedUrl('Wallets.qml')) - } else { - var newww = app.newWalletWizard.createObject(app) - newww.walletCreated.connect(function() { - Daemon.availableWallets.reload() - // and load the new wallet - Daemon.load_wallet(newww.path, newww.wizard_data['password']) - }) - newww.open() + Label { + Layout.alignment: Qt.AlignHCenter + text: qsTr('No wallet loaded') + font.pixelSize: constants.fontSizeXXLarge + } + + Pane { + Layout.alignment: Qt.AlignHCenter + padding: 0 + background: Rectangle { + color: Material.dialogColor + } + FlatButton { + text: qsTr('Open/Create Wallet') + icon.source: '../../icons/wallet.png' + onClicked: { + if (Daemon.availableWallets.rowCount() > 0) { + stack.push(Qt.resolvedUrl('Wallets.qml')) + } else { + var newww = app.newWalletWizard.createObject(app) + newww.walletCreated.connect(function() { + Daemon.availableWallets.reload() + // and load the new wallet + Daemon.load_wallet(newww.path, newww.wizard_data['password']) + }) + newww.open() + } } } } - } - } - - ColumnLayout { - anchors.fill: parent - visible: Daemon.currentWallet - - History { - id: history - Layout.preferredWidth: parent.width - Layout.fillHeight: true + Item { + Layout.fillHeight: true + } } RowLayout { @@ -167,7 +173,12 @@ Item { Layout.alignment: Qt.AlignVCenter color: constants.darkerBackground } + Item { + visible: !Daemon.currentWallet + Layout.fillWidth: true + } FlatButton { + visible: Daemon.currentWallet Layout.fillWidth: true Layout.preferredWidth: 1 icon.source: '../../icons/tab_receive.png' @@ -178,6 +189,7 @@ Item { } } Rectangle { + visible: Daemon.currentWallet Layout.fillWidth: false Layout.preferredWidth: 2 Layout.preferredHeight: parent.height * 2/3 @@ -185,6 +197,7 @@ Item { color: constants.darkerBackground } FlatButton { + visible: Daemon.currentWallet Layout.fillWidth: true Layout.preferredWidth: 1 icon.source: '../../icons/tab_send.png' From 4f9469b7894d0e237beaabd62a2b8a6153f6dcdb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 28 Jan 2023 00:39:36 +0000 Subject: [PATCH 0024/1143] re-generate protobuf _pb2.py files and bump min required protobuf upper bound "<4" still needed due to keepkey... related https://github.com/spesmilo/electrum/issues/7922 --- contrib/requirements/requirements-hw.txt | 2 +- contrib/requirements/requirements.txt | 2 +- electrum/paymentrequest_pb2.py | 385 ++--------------------- 3 files changed, 23 insertions(+), 366 deletions(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index a8706d446..8b16d7839 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -29,7 +29,7 @@ pyserial>=3.5.0,<4.0.0 # prefer older protobuf (see #7922) # (pulled in via e.g. keepkey and bitbox02) -protobuf>=3.12,<4 +protobuf>=3.20,<4 # prefer older colorama to avoid needing hatchling # (pulled in via trezor -> click -> colorama) diff --git a/contrib/requirements/requirements.txt b/contrib/requirements/requirements.txt index 0f2dd7b5c..820e9cca0 100644 --- a/contrib/requirements/requirements.txt +++ b/contrib/requirements/requirements.txt @@ -1,5 +1,5 @@ qrcode -protobuf>=3.12,<4 +protobuf>=3.20,<4 qdarkstyle>=2.7 aiorpcx>=0.22.0,<0.23 aiohttp>=3.3.0,<4.0.0 diff --git a/electrum/paymentrequest_pb2.py b/electrum/paymentrequest_pb2.py index f5b2c3815..fe5c53b99 100644 --- a/electrum/paymentrequest_pb2.py +++ b/electrum/paymentrequest_pb2.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: paymentrequest.proto - +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pool as _descriptor_pool from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) @@ -13,367 +13,24 @@ -DESCRIPTOR = _descriptor.FileDescriptor( - name='paymentrequest.proto', - package='payments', - syntax='proto2', - serialized_options=b'\n\036org.bitcoin.protocols.paymentsB\006Protos', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x14paymentrequest.proto\x12\x08payments\"+\n\x06Output\x12\x11\n\x06\x61mount\x18\x01 \x01(\x04:\x01\x30\x12\x0e\n\x06script\x18\x02 \x02(\x0c\"\xa3\x01\n\x0ePaymentDetails\x12\x15\n\x07network\x18\x01 \x01(\t:\x04main\x12!\n\x07outputs\x18\x02 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04time\x18\x03 \x02(\x04\x12\x0f\n\x07\x65xpires\x18\x04 \x01(\x04\x12\x0c\n\x04memo\x18\x05 \x01(\t\x12\x13\n\x0bpayment_url\x18\x06 \x01(\t\x12\x15\n\rmerchant_data\x18\x07 \x01(\x0c\"\x95\x01\n\x0ePaymentRequest\x12\"\n\x17payment_details_version\x18\x01 \x01(\r:\x01\x31\x12\x16\n\x08pki_type\x18\x02 \x01(\t:\x04none\x12\x10\n\x08pki_data\x18\x03 \x01(\x0c\x12\"\n\x1aserialized_payment_details\x18\x04 \x02(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"\'\n\x10X509Certificates\x12\x13\n\x0b\x63\x65rtificate\x18\x01 \x03(\x0c\"i\n\x07Payment\x12\x15\n\rmerchant_data\x18\x01 \x01(\x0c\x12\x14\n\x0ctransactions\x18\x02 \x03(\x0c\x12#\n\trefund_to\x18\x03 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04memo\x18\x04 \x01(\t\">\n\nPaymentACK\x12\"\n\x07payment\x18\x01 \x02(\x0b\x32\x11.payments.Payment\x12\x0c\n\x04memo\x18\x02 \x01(\tB(\n\x1eorg.bitcoin.protocols.paymentsB\x06Protos' -) - - - - -_OUTPUT = _descriptor.Descriptor( - name='Output', - full_name='payments.Output', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='amount', full_name='payments.Output.amount', index=0, - number=1, type=4, cpp_type=4, label=1, - has_default_value=True, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='script', full_name='payments.Output.script', index=1, - number=2, type=12, cpp_type=9, label=2, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=34, - serialized_end=77, -) - - -_PAYMENTDETAILS = _descriptor.Descriptor( - name='PaymentDetails', - full_name='payments.PaymentDetails', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='network', full_name='payments.PaymentDetails.network', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=b"main".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='outputs', full_name='payments.PaymentDetails.outputs', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='time', full_name='payments.PaymentDetails.time', index=2, - number=3, type=4, cpp_type=4, label=2, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='expires', full_name='payments.PaymentDetails.expires', index=3, - number=4, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='memo', full_name='payments.PaymentDetails.memo', index=4, - number=5, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='payment_url', full_name='payments.PaymentDetails.payment_url', index=5, - number=6, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='merchant_data', full_name='payments.PaymentDetails.merchant_data', index=6, - number=7, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=80, - serialized_end=243, -) - - -_PAYMENTREQUEST = _descriptor.Descriptor( - name='PaymentRequest', - full_name='payments.PaymentRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='payment_details_version', full_name='payments.PaymentRequest.payment_details_version', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=True, default_value=1, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='pki_type', full_name='payments.PaymentRequest.pki_type', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=True, default_value=b"none".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='pki_data', full_name='payments.PaymentRequest.pki_data', index=2, - number=3, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='serialized_payment_details', full_name='payments.PaymentRequest.serialized_payment_details', index=3, - number=4, type=12, cpp_type=9, label=2, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='signature', full_name='payments.PaymentRequest.signature', index=4, - number=5, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=246, - serialized_end=395, -) - - -_X509CERTIFICATES = _descriptor.Descriptor( - name='X509Certificates', - full_name='payments.X509Certificates', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='certificate', full_name='payments.X509Certificates.certificate', index=0, - number=1, type=12, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=397, - serialized_end=436, -) - - -_PAYMENT = _descriptor.Descriptor( - name='Payment', - full_name='payments.Payment', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='merchant_data', full_name='payments.Payment.merchant_data', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=b"", - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='transactions', full_name='payments.Payment.transactions', index=1, - number=2, type=12, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='refund_to', full_name='payments.Payment.refund_to', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='memo', full_name='payments.Payment.memo', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=438, - serialized_end=543, -) - - -_PAYMENTACK = _descriptor.Descriptor( - name='PaymentACK', - full_name='payments.PaymentACK', - filename=None, - file=DESCRIPTOR, - containing_type=None, - create_key=_descriptor._internal_create_key, - fields=[ - _descriptor.FieldDescriptor( - name='payment', full_name='payments.PaymentACK.payment', index=0, - number=1, type=11, cpp_type=10, label=2, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - _descriptor.FieldDescriptor( - name='memo', full_name='payments.PaymentACK.memo', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - serialized_options=None, - is_extendable=False, - syntax='proto2', - extension_ranges=[], - oneofs=[ - ], - serialized_start=545, - serialized_end=607, -) - -_PAYMENTDETAILS.fields_by_name['outputs'].message_type = _OUTPUT -_PAYMENT.fields_by_name['refund_to'].message_type = _OUTPUT -_PAYMENTACK.fields_by_name['payment'].message_type = _PAYMENT -DESCRIPTOR.message_types_by_name['Output'] = _OUTPUT -DESCRIPTOR.message_types_by_name['PaymentDetails'] = _PAYMENTDETAILS -DESCRIPTOR.message_types_by_name['PaymentRequest'] = _PAYMENTREQUEST -DESCRIPTOR.message_types_by_name['X509Certificates'] = _X509CERTIFICATES -DESCRIPTOR.message_types_by_name['Payment'] = _PAYMENT -DESCRIPTOR.message_types_by_name['PaymentACK'] = _PAYMENTACK -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Output = _reflection.GeneratedProtocolMessageType('Output', (_message.Message,), { - 'DESCRIPTOR' : _OUTPUT, - '__module__' : 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.Output) - }) -_sym_db.RegisterMessage(Output) - -PaymentDetails = _reflection.GeneratedProtocolMessageType('PaymentDetails', (_message.Message,), { - 'DESCRIPTOR' : _PAYMENTDETAILS, - '__module__' : 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.PaymentDetails) - }) -_sym_db.RegisterMessage(PaymentDetails) - -PaymentRequest = _reflection.GeneratedProtocolMessageType('PaymentRequest', (_message.Message,), { - 'DESCRIPTOR' : _PAYMENTREQUEST, - '__module__' : 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.PaymentRequest) - }) -_sym_db.RegisterMessage(PaymentRequest) - -X509Certificates = _reflection.GeneratedProtocolMessageType('X509Certificates', (_message.Message,), { - 'DESCRIPTOR' : _X509CERTIFICATES, - '__module__' : 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.X509Certificates) - }) -_sym_db.RegisterMessage(X509Certificates) - -Payment = _reflection.GeneratedProtocolMessageType('Payment', (_message.Message,), { - 'DESCRIPTOR' : _PAYMENT, - '__module__' : 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.Payment) - }) -_sym_db.RegisterMessage(Payment) - -PaymentACK = _reflection.GeneratedProtocolMessageType('PaymentACK', (_message.Message,), { - 'DESCRIPTOR' : _PAYMENTACK, - '__module__' : 'paymentrequest_pb2' - # @@protoc_insertion_point(class_scope:payments.PaymentACK) - }) -_sym_db.RegisterMessage(PaymentACK) +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14paymentrequest.proto\x12\x08payments\"+\n\x06Output\x12\x11\n\x06\x61mount\x18\x01 \x01(\x04:\x01\x30\x12\x0e\n\x06script\x18\x02 \x02(\x0c\"\xa3\x01\n\x0ePaymentDetails\x12\x15\n\x07network\x18\x01 \x01(\t:\x04main\x12!\n\x07outputs\x18\x02 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04time\x18\x03 \x02(\x04\x12\x0f\n\x07\x65xpires\x18\x04 \x01(\x04\x12\x0c\n\x04memo\x18\x05 \x01(\t\x12\x13\n\x0bpayment_url\x18\x06 \x01(\t\x12\x15\n\rmerchant_data\x18\x07 \x01(\x0c\"\x95\x01\n\x0ePaymentRequest\x12\"\n\x17payment_details_version\x18\x01 \x01(\r:\x01\x31\x12\x16\n\x08pki_type\x18\x02 \x01(\t:\x04none\x12\x10\n\x08pki_data\x18\x03 \x01(\x0c\x12\"\n\x1aserialized_payment_details\x18\x04 \x02(\x0c\x12\x11\n\tsignature\x18\x05 \x01(\x0c\"\'\n\x10X509Certificates\x12\x13\n\x0b\x63\x65rtificate\x18\x01 \x03(\x0c\"i\n\x07Payment\x12\x15\n\rmerchant_data\x18\x01 \x01(\x0c\x12\x14\n\x0ctransactions\x18\x02 \x03(\x0c\x12#\n\trefund_to\x18\x03 \x03(\x0b\x32\x10.payments.Output\x12\x0c\n\x04memo\x18\x04 \x01(\t\">\n\nPaymentACK\x12\"\n\x07payment\x18\x01 \x02(\x0b\x32\x11.payments.Payment\x12\x0c\n\x04memo\x18\x02 \x01(\tB(\n\x1eorg.bitcoin.protocols.paymentsB\x06Protos') +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'paymentrequest_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: -DESCRIPTOR._options = None + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\036org.bitcoin.protocols.paymentsB\006Protos' + _OUTPUT._serialized_start=34 + _OUTPUT._serialized_end=77 + _PAYMENTDETAILS._serialized_start=80 + _PAYMENTDETAILS._serialized_end=243 + _PAYMENTREQUEST._serialized_start=246 + _PAYMENTREQUEST._serialized_end=395 + _X509CERTIFICATES._serialized_start=397 + _X509CERTIFICATES._serialized_end=436 + _PAYMENT._serialized_start=438 + _PAYMENT._serialized_end=543 + _PAYMENTACK._serialized_start=545 + _PAYMENTACK._serialized_end=607 # @@protoc_insertion_point(module_scope) From 4aa319e5c31543883346e28a5459fa3642601be6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 28 Jan 2023 00:51:12 +0000 Subject: [PATCH 0025/1143] CI: exclude generated protobuf files from flake8 --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 0f93e7c94..2064b93d4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -126,7 +126,7 @@ task: install_script: - pip install flake8 flake8_script: - - flake8 . --count --select=$ELECTRUM_LINTERS --show-source --statistics + - flake8 . --count --select=$ELECTRUM_LINTERS --show-source --statistics --exclude "*_pb2.py" env: ELECTRUM_IMAGE: python:3.8 ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt From 54ced268855edba167beedb05117bd7ab9762e22 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 30 Jan 2023 14:10:25 +0100 Subject: [PATCH 0026/1143] qml: remove leftover --- electrum/gui/qml/components/main.qml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index ad8bc55fb..48f1faba1 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -120,14 +120,6 @@ ApplicationWindow } } } - - Rectangle { - color: 'transparent' - Layout.preferredWidth: constants.paddingSmall - height: 1 - visible: !menuButton.visible - } - } WalletSummary { From 000a3de571ae2408025c15b512f482da15e55a80 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 27 Jan 2023 16:29:51 +0100 Subject: [PATCH 0027/1143] extract QML translation strings, convert to gettext and combine with rest of template --- contrib/pull_locale | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/contrib/pull_locale b/contrib/pull_locale index 4b187504a..9f2b8536b 100755 --- a/contrib/pull_locale +++ b/contrib/pull_locale @@ -25,10 +25,34 @@ print("Found {} files to translate".format(len(files.splitlines()))) # Generate fresh translation template if not os.path.exists('electrum/locale'): os.mkdir('electrum/locale') -cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot' +cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=messages.pot' print('Generate template') os.system(cmd) +# add QML translations +cmd = "find electrum/gui/qml -type f -name '*.qml'" + +files = subprocess.check_output(cmd, shell=True) + +with open("qml.lst", "wb") as f: + f.write(files) + +print("Found {} QML files to translate".format(len(files.splitlines()))) + +cmd = "lupdate @qml.lst -ts qml.ts" +print('Collecting strings') +os.system(cmd) + +cmd = "lconvert -of po -o qml.pot qml.ts" +print('Convert to gettext') +os.system(cmd) + +cmd = "msgcat -u -o electrum/locale/messages.pot messages.pot qml.pot" +print('Generate template') +os.system(cmd) + + + os.chdir('electrum') crowdin_identifier = 'electrum' From 2849c021b660212999c63a062a53c4890a67e6b6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 30 Jan 2023 12:52:50 +0100 Subject: [PATCH 0028/1143] qml: add gettext-wrapping QTranslator --- electrum/gui/qml/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 11c6354cc..fdbca3980 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -15,11 +15,11 @@ except Exception: sys.exit("Error: Could not import PyQt5.QtQml on Linux systems, you may try 'sudo apt-get install python3-pyqt5.qtquick'") -from PyQt5.QtCore import (Qt, QCoreApplication, QObject, QLocale, QTimer, pyqtSignal, +from PyQt5.QtCore import (Qt, QCoreApplication, QObject, QLocale, QTranslator, QTimer, pyqtSignal, QT_VERSION_STR, PYQT_VERSION_STR) from PyQt5.QtGui import QGuiApplication -from electrum.i18n import set_language, languages +from electrum.i18n import set_language, languages, language from electrum.plugin import run_hook from electrum.util import profiler from electrum.logging import Logger @@ -32,6 +32,16 @@ from .qeapp import ElectrumQmlApplication, Exception_Hook +class ElectrumTranslator(QTranslator): + def __init__(self, parent=None): + super().__init__(parent) + + def translate(self, context, source_text, disambiguation, n): + if source_text == "": + return "" + return language.gettext(source_text) + + class ElectrumGui(Logger): @profiler @@ -61,6 +71,8 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') self.gui_thread = threading.current_thread() self.plugins = plugins self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins) + self.translator = ElectrumTranslator() + self.app.installTranslator(self.translator) # timer self.timer = QTimer(self.app) From e58a61a135418a3b7b7622363dbfba56cf12d875 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 30 Jan 2023 12:56:11 +0100 Subject: [PATCH 0029/1143] add qttools5-dev-tools to requirements for building locale (needed by qml translations) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fb00135a..4cbaf6d22 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ $ python3 -m pip install --user -e . Create translations (optional): ``` -$ sudo apt-get install python-requests gettext +$ sudo apt-get install python-requests gettext qttools5-dev-tools $ ./contrib/pull_locale ``` From 264cb7846fe4bd336c2a93e3e1b186e14f58578e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 30 Jan 2023 14:01:29 +0100 Subject: [PATCH 0030/1143] qml: add language selection to qeconfig.py/Preferences --- electrum/gui/qml/components/Preferences.qml | 16 +++++++++++++++- electrum/gui/qml/qeconfig.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 0471e0f2b..805e2be96 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -46,7 +46,20 @@ Pane { ElComboBox { id: language - enabled: false + textRole: 'text' + valueRole: 'value' + model: Config.languagesAvailable + onCurrentValueChanged: { + if (activeFocus) { + if (Config.language != currentValue) { + Config.language = currentValue + var dialog = app.messageDialog.createObject(app, { + text: qsTr('Please restart Electrum to activate the new GUI settings') + }) + dialog.open() + } + } + } } Label { @@ -355,6 +368,7 @@ Pane { } Component.onCompleted: { + language.currentIndex = language.indexOfValue(Config.language) baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit) thousands.checked = Config.thousandsSeparator currencies.currentIndex = currencies.indexOfValue(Daemon.fx.fiatCurrency) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index ec0e354e5..0439bc801 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -2,6 +2,7 @@ from decimal import Decimal +from electrum.i18n import set_language, languages from electrum.logging import get_logger from electrum.util import DECIMAL_POINT_DEFAULT, format_satoshis from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING @@ -16,6 +17,25 @@ def __init__(self, config, parent=None): super().__init__(parent) self.config = config + languageChanged = pyqtSignal() + @pyqtProperty(str, notify=languageChanged) + def language(self): + return self.config.get('language') + + @language.setter + def language(self, language): + if language not in languages: + return + if self.config.get('language') != language: + self.config.set_key('language', language) + set_language(language) + self.languageChanged.emit() + + languagesChanged = pyqtSignal() + @pyqtProperty('QVariantList', notify=languagesChanged) + def languagesAvailable(self): + return list(map(lambda x: {'value': x[0], 'text': x[1]}, languages.items())) + autoConnectChanged = pyqtSignal() @pyqtProperty(bool, notify=autoConnectChanged) def autoConnect(self): From 26f7238eb54e0e39ae6675358458b851651df62c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 19 Jan 2023 13:40:03 +0100 Subject: [PATCH 0031/1143] qml: add initial generic listmodel filter QEFilterProxyModel --- electrum/gui/qml/components/Channels.qml | 7 +++--- electrum/gui/qml/qeapp.py | 6 +++++- electrum/gui/qml/qechannellistmodel.py | 9 ++++++++ electrum/gui/qml/qemodelfilter.py | 27 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 electrum/gui/qml/qemodelfilter.py diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 4db86b07e..810a36142 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -78,7 +78,7 @@ Pane { Layout.preferredWidth: parent.width Layout.fillHeight: true clip: true - model: Daemon.currentWallet.channelModel + model: Daemon.currentWallet.channelModel.filterModel('is_backup', false) delegate: ChannelDelegate { onClicked: { @@ -126,10 +126,9 @@ Pane { FlatButton { Layout.fillWidth: true - text: qsTr('Import channel backup') + text: qsTr('Channel backups') onClicked: { - var dialog = importChannelBackupDialog.createObject(root) - dialog.open() + app.stack.push(Qt.resolvedUrl('ChannelBackups.qml')) } icon.source: '../../icons/file.png' } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 95e90207a..1cebd04d8 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -8,7 +8,8 @@ import asyncio from typing import TYPE_CHECKING -from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer +from PyQt5.QtCore import (pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, + qInstallMessageHandler, QTimer, QSortFilterProxyModel) from PyQt5.QtGui import QGuiApplication, QFontDatabase from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType, QQmlApplicationEngine @@ -38,6 +39,7 @@ from .qechanneldetails import QEChannelDetails from .qeswaphelper import QESwapHelper from .qewizard import QENewWalletWizard, QEServerConnectWizard +from .qemodelfilter import QEFilterProxyModel if TYPE_CHECKING: from electrum.simple_config import SimpleConfig @@ -297,6 +299,8 @@ def __init__(self, args, config, daemon, plugins): qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'NewWalletWizard', 'NewWalletWizard can only be used as property') qmlRegisterUncreatableType(QEServerConnectWizard, 'org.electrum', 1, 0, 'ServerConnectWizard', 'ServerConnectWizard can only be used as property') + qmlRegisterUncreatableType(QEFilterProxyModel, 'org.electrum', 1, 0, 'FilterProxyModel', 'FilterProxyModel can only be used as property') + qmlRegisterUncreatableType(QSortFilterProxyModel, 'org.electrum', 1, 0, 'QSortFilterProxyModel', 'QSortFilterProxyModel can only be used as property') self.engine = QQmlApplicationEngine(parent=self) diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 4d33eff7c..8c612d2f0 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -8,6 +8,7 @@ from .qetypes import QEAmount from .util import QtEventListener, qt_event_listener +from .qemodelfilter import QEFilterProxyModel class QEChannelListModel(QAbstractListModel, QtEventListener): _logger = get_logger(__name__) @@ -165,3 +166,11 @@ def remove_channel(self, cid): self.countChanged.emit() return i = i + 1 + + @pyqtSlot(str, 'QVariant', result=QEFilterProxyModel) + def filterModel(self, role, match): + self._filterModel = QEFilterProxyModel(self, self) + self._filterModel.setFilterRole(QEChannelListModel._ROLE_RMAP[role]) + self._filterModel.setFilterValue(match) + return self._filterModel + diff --git a/electrum/gui/qml/qemodelfilter.py b/electrum/gui/qml/qemodelfilter.py new file mode 100644 index 000000000..dc7a0a36b --- /dev/null +++ b/electrum/gui/qml/qemodelfilter.py @@ -0,0 +1,27 @@ +from PyQt5.QtCore import QSortFilterProxyModel + +from electrum.logging import get_logger + +class QEFilterProxyModel(QSortFilterProxyModel): + _logger = get_logger(__name__) + + _filter_value = None + + def __init__(self, parent_model, parent=None): + super().__init__(parent) + self.setSourceModel(parent_model) + + def isCustomFilter(self): + return self._filter_value is not None + + def setFilterValue(self, filter_value): + self._filter_value = filter_value + + def filterAcceptsRow(self, s_row, s_parent): + if not self.isCustomFilter: + return super().filterAcceptsRow(s_row, s_parent) + + parent_model = self.sourceModel() + d = parent_model.data(parent_model.index(s_row, 0, s_parent), self.filterRole()) + # self._logger.debug(f'DATA in FilterProxy is {repr(d)}') + return True if self._filter_value is None else d == self._filter_value From c868ddedb5374914504c485b245923523d75b162 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 20 Jan 2023 10:24:11 +0100 Subject: [PATCH 0032/1143] qml: add ChannelBackups.qml --- .../gui/qml/components/ChannelBackups.qml | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 electrum/gui/qml/components/ChannelBackups.qml diff --git a/electrum/gui/qml/components/ChannelBackups.qml b/electrum/gui/qml/components/ChannelBackups.qml new file mode 100644 index 000000000..ab090e210 --- /dev/null +++ b/electrum/gui/qml/components/ChannelBackups.qml @@ -0,0 +1,123 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 +import QtQml.Models 2.2 + +import org.electrum 1.0 + +import "controls" + +Pane { + id: root + padding: 0 + + ColumnLayout { + id: layout + width: parent.width + height: parent.height + spacing: 0 + + GridLayout { + id: summaryLayout + Layout.preferredWidth: parent.width + Layout.topMargin: constants.paddingLarge + Layout.leftMargin: constants.paddingLarge + Layout.rightMargin: constants.paddingLarge + + columns: 2 + + Heading { + Layout.columnSpan: 2 + text: qsTr('Lightning Channel Backups') + } + } + + Frame { + id: channelsFrame + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge + Layout.leftMargin: constants.paddingMedium + Layout.rightMargin: constants.paddingMedium + + verticalPadding: 0 + horizontalPadding: 0 + background: PaneInsetBackground {} + + ColumnLayout { + spacing: 0 + anchors.fill: parent + + ListView { + id: listview + Layout.preferredWidth: parent.width + Layout.fillHeight: true + clip: true + model: Daemon.currentWallet.channelModel.filterModel('is_backup', true) + + delegate: ChannelDelegate { + onClicked: { + app.stack.push(Qt.resolvedUrl('ChannelDetails.qml'), { channelid: model.cid }) + } + } + + ScrollIndicator.vertical: ScrollIndicator { } + + Label { + visible: Daemon.currentWallet.channelModel.count == 0 + anchors.centerIn: parent + width: listview.width * 4/5 + font.pixelSize: constants.fontSizeXXLarge + color: constants.mutedForeground + text: qsTr('No Lightning channel backups present') + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + } + } + } + } + + FlatButton { + Layout.fillWidth: true + text: qsTr('Import channel backup') + onClicked: { + var dialog = importChannelBackupDialog.createObject(root) + dialog.open() + } + icon.source: '../../icons/file.png' + } + + } + + Connections { + target: Daemon.currentWallet + function onImportChannelBackupFailed(message) { + var dialog = app.messageDialog.createObject(root, { text: message }) + dialog.open() + } + } + + Component { + id: swapDialog + SwapDialog { + onClosed: destroy() + } + } + + Component { + id: openChannelDialog + OpenChannelDialog { + onClosed: destroy() + } + } + + Component { + id: importChannelBackupDialog + ImportChannelBackupDialog { + onClosed: destroy() + } + } + +} From bd10fbeaf0f67349fa3aad55b202bae3641a5b9d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 30 Jan 2023 16:26:44 +0100 Subject: [PATCH 0033/1143] qml: clean up Channels/ChannelBackups, implement proper count property on QEFilterProxyModel --- electrum/gui/qml/components/ChannelBackups.qml | 17 +---------------- electrum/gui/qml/components/Channels.qml | 17 +---------------- electrum/gui/qml/qemodelfilter.py | 8 ++++++-- 3 files changed, 8 insertions(+), 34 deletions(-) diff --git a/electrum/gui/qml/components/ChannelBackups.qml b/electrum/gui/qml/components/ChannelBackups.qml index ab090e210..491976d9f 100644 --- a/electrum/gui/qml/components/ChannelBackups.qml +++ b/electrum/gui/qml/components/ChannelBackups.qml @@ -2,7 +2,6 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.3 import QtQuick.Controls.Material 2.0 -import QtQml.Models 2.2 import org.electrum 1.0 @@ -66,7 +65,7 @@ Pane { ScrollIndicator.vertical: ScrollIndicator { } Label { - visible: Daemon.currentWallet.channelModel.count == 0 + visible: listview.model.count == 0 anchors.centerIn: parent width: listview.width * 4/5 font.pixelSize: constants.fontSizeXXLarge @@ -99,20 +98,6 @@ Pane { } } - Component { - id: swapDialog - SwapDialog { - onClosed: destroy() - } - } - - Component { - id: openChannelDialog - OpenChannelDialog { - onClosed: destroy() - } - } - Component { id: importChannelBackupDialog ImportChannelBackupDialog { diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 810a36142..c05c14c8b 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -89,7 +89,7 @@ Pane { ScrollIndicator.vertical: ScrollIndicator { } Label { - visible: Daemon.currentWallet.channelModel.count == 0 + visible: listview.model.count == 0 anchors.centerIn: parent width: listview.width * 4/5 font.pixelSize: constants.fontSizeXXLarge @@ -135,14 +135,6 @@ Pane { } - Connections { - target: Daemon.currentWallet - function onImportChannelBackupFailed(message) { - var dialog = app.messageDialog.createObject(root, { text: message }) - dialog.open() - } - } - Component { id: swapDialog SwapDialog { @@ -157,11 +149,4 @@ Pane { } } - Component { - id: importChannelBackupDialog - ImportChannelBackupDialog { - onClosed: destroy() - } - } - } diff --git a/electrum/gui/qml/qemodelfilter.py b/electrum/gui/qml/qemodelfilter.py index dc7a0a36b..f024bea4a 100644 --- a/electrum/gui/qml/qemodelfilter.py +++ b/electrum/gui/qml/qemodelfilter.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import QSortFilterProxyModel +from PyQt5.QtCore import pyqtSignal, pyqtProperty, QSortFilterProxyModel, QModelIndex from electrum.logging import get_logger @@ -11,6 +11,11 @@ def __init__(self, parent_model, parent=None): super().__init__(parent) self.setSourceModel(parent_model) + countChanged = pyqtSignal() + @pyqtProperty(int, notify=countChanged) + def count(self): + return self.rowCount(QModelIndex()) + def isCustomFilter(self): return self._filter_value is not None @@ -23,5 +28,4 @@ def filterAcceptsRow(self, s_row, s_parent): parent_model = self.sourceModel() d = parent_model.data(parent_model.index(s_row, 0, s_parent), self.filterRole()) - # self._logger.debug(f'DATA in FilterProxy is {repr(d)}') return True if self._filter_value is None else d == self._filter_value From d033e10e1710ae37185b964c430c702dcfac3e47 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 30 Jan 2023 16:59:23 +0100 Subject: [PATCH 0034/1143] qml: fix leftover in TxDetails --- electrum/gui/qml/components/TxDetails.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 1e2840b0a..ae220e179 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -93,12 +93,6 @@ Pane { amount: txdetails.lnAmount.isEmpty ? txdetails.amount : txdetails.lnAmount } - Label { - visible: !txdetails.isUnrelated && Daemon.fx.enabled && txdetails.lnAmount.satsInt != 0 - text: Daemon.fx.fiatValue(txdetails.lnAmount, false) + ' ' + Daemon.fx.fiatCurrency - } - - Label { visible: !txdetails.fee.isEmpty text: qsTr('Transaction fee') From 4f66afb8a8029ee4bbc773970799755d40bf74a5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 30 Jan 2023 17:18:46 +0000 Subject: [PATCH 0035/1143] refactor locale scripts follow-up 000a3de571ae2408025c15b512f482da15e55a80 --- .cirrus.yml | 2 +- README.md | 2 +- contrib/deterministic-build/electrum-locale | 2 +- contrib/pull_locale | 94 +++------------------ contrib/push_locale | 36 ++++++-- contrib/release.sh | 2 +- 6 files changed, 47 insertions(+), 91 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 2064b93d4..bbd06bf52 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -64,7 +64,7 @@ task: fingerprint_script: echo Locale && echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS_CI install_script: - apt-get update - - apt-get -y install gettext + - apt-get -y install gettext qttools5-dev-tools - pip install -r $ELECTRUM_REQUIREMENTS_CI - pip install requests locale_script: diff --git a/README.md b/README.md index 4cbaf6d22..683c4ce8e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ $ python3 -m pip install --user -e . Create translations (optional): ``` -$ sudo apt-get install python-requests gettext qttools5-dev-tools +$ sudo apt-get install python3-requests gettext qttools5-dev-tools $ ./contrib/pull_locale ``` diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 4941c1a92..a87f9a043 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 4941c1a92925f198cb0e8d4334692a09917ffc20 +Subproject commit a87f9a04302682bc23ce13bfa6672c12f0de798f diff --git a/contrib/pull_locale b/contrib/pull_locale index 9f2b8536b..8d056e022 100755 --- a/contrib/pull_locale +++ b/contrib/pull_locale @@ -1,89 +1,21 @@ #!/usr/bin/env python3 import os import subprocess -import io -import zipfile -import sys +import importlib.util -try: - import requests -except ImportError as e: - sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install '") +project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +locale_path = os.path.join(project_root, "electrum", "locale") -os.chdir(os.path.dirname(os.path.realpath(__file__))) -os.chdir('..') +# download latest .po files from crowdin +locale_update = os.path.join(project_root, "contrib", "deterministic-build", "electrum-locale", "update.py") +assert os.path.exists(locale_update) +# load update.py; needlessly complicated alternative to "imp.load_source": +lu_spec = importlib.util.spec_from_file_location('update', locale_update) +lu_module = importlib.util.module_from_spec(lu_spec) +lu_spec.loader.exec_module(lu_module) -cmd = "find electrum -type f -name '*.py' -o -name '*.kv'" - -files = subprocess.check_output(cmd, shell=True) - -with open("app.fil", "wb") as f: - f.write(files) - -print("Found {} files to translate".format(len(files.splitlines()))) - -# Generate fresh translation template -if not os.path.exists('electrum/locale'): - os.mkdir('electrum/locale') -cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=messages.pot' -print('Generate template') -os.system(cmd) - -# add QML translations -cmd = "find electrum/gui/qml -type f -name '*.qml'" - -files = subprocess.check_output(cmd, shell=True) - -with open("qml.lst", "wb") as f: - f.write(files) - -print("Found {} QML files to translate".format(len(files.splitlines()))) - -cmd = "lupdate @qml.lst -ts qml.ts" -print('Collecting strings') -os.system(cmd) - -cmd = "lconvert -of po -o qml.pot qml.ts" -print('Convert to gettext') -os.system(cmd) - -cmd = "msgcat -u -o electrum/locale/messages.pot messages.pot qml.pot" -print('Generate template') -os.system(cmd) - - - -os.chdir('electrum') - -crowdin_identifier = 'electrum' -crowdin_file_name = 'files[electrum-client/messages.pot]' -locale_file_name = 'locale/messages.pot' - -# Download & unzip -print('Download translations') -s = requests.request('GET', 'https://crowdin.com/backend/download/project/' + crowdin_identifier + '.zip').content -zfobj = zipfile.ZipFile(io.BytesIO(s)) - -print('Unzip translations') -for name in zfobj.namelist(): - if not name.startswith('electrum-client/locale'): - continue - if name.endswith('/'): - if not os.path.exists(name[16:]): - os.mkdir(name[16:]) - else: - with open(name[16:], 'wb') as output: - output.write(zfobj.read(name)) +lu_module.pull_locale(locale_path) # Convert .po to .mo -print('Installing') -for lang in os.listdir('locale'): - if lang.startswith('messages'): - continue - # Check LC_MESSAGES folder - mo_dir = 'locale/%s/LC_MESSAGES' % lang - if not os.path.exists(mo_dir): - os.mkdir(mo_dir) - cmd = 'msgfmt --output-file="%s/electrum.mo" "locale/%s/electrum.po"' % (mo_dir,lang) - print('Installing', lang) - os.system(cmd) +subprocess.check_output([f"{project_root}/contrib/build_locale.sh", locale_path, locale_path]) + diff --git a/contrib/push_locale b/contrib/push_locale index 6ccfa694a..487058e1b 100755 --- a/contrib/push_locale +++ b/contrib/push_locale @@ -6,13 +6,14 @@ import sys try: import requests except ImportError as e: - sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install '") + sys.exit(f"Error: {str(e)}. Try 'python3 -m pip install --user '") -os.chdir(os.path.dirname(os.path.realpath(__file__))) -os.chdir('..') -cmd = "find electrum -type f -name '*.py' -o -name '*.kv'" +project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + +os.chdir(project_root) +cmd = "find electrum -type f -name '*.py' -o -name '*.kv'" files = subprocess.check_output(cmd, shell=True) with open("app.fil", "wb") as f: @@ -24,10 +25,33 @@ print("Found {} files to translate".format(len(files.splitlines()))) if not os.path.exists('electrum/locale'): os.mkdir('electrum/locale') print('Generating template...') -cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot' +cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=messages.pot' +subprocess.check_output(cmd, shell=True) + + +# add QML translations +cmd = "find electrum/gui/qml -type f -name '*.qml'" +files = subprocess.check_output(cmd, shell=True) + +with open("qml.lst", "wb") as f: + f.write(files) + +print("Found {} QML files to translate".format(len(files.splitlines()))) + +cmd = "lupdate @qml.lst -ts qml.ts" +print('Collecting strings') +subprocess.check_output(cmd, shell=True) + +cmd = "lconvert -of po -o qml.pot qml.ts" +print('Convert to gettext') +subprocess.check_output(cmd, shell=True) + +cmd = "msgcat -u -o electrum/locale/messages.pot messages.pot qml.pot" +print('Generate template') subprocess.check_output(cmd, shell=True) -os.chdir('electrum') + +os.chdir(os.path.join(project_root, "electrum")) crowdin_api_key = None diff --git a/contrib/release.sh b/contrib/release.sh index cb325bdea..34d1fa3ab 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -19,7 +19,7 @@ # # Note: steps before doing a new release: # - update locale: -# 1. cd /opt/electrum-locale && ./update && git push +# 1. cd /opt/electrum-locale && ./update.py && git push # 2. cd to the submodule dir, and git pull # 3. cd .. && git push # - update RELEASE-NOTES and version.py From b2302032451e33c92b7063af92e58a69f51cbaab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 30 Jan 2023 17:28:31 +0000 Subject: [PATCH 0036/1143] contrib/push_locale: mv temp files from project_root to locale/ --- contrib/push_locale | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contrib/push_locale b/contrib/push_locale index 487058e1b..69f49eded 100755 --- a/contrib/push_locale +++ b/contrib/push_locale @@ -25,7 +25,7 @@ print("Found {} files to translate".format(len(files.splitlines()))) if not os.path.exists('electrum/locale'): os.mkdir('electrum/locale') print('Generating template...') -cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=messages.pot' +cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages_gettext.pot' subprocess.check_output(cmd, shell=True) @@ -33,24 +33,25 @@ subprocess.check_output(cmd, shell=True) cmd = "find electrum/gui/qml -type f -name '*.qml'" files = subprocess.check_output(cmd, shell=True) -with open("qml.lst", "wb") as f: +with open("electrum/locale/qml.lst", "wb") as f: f.write(files) print("Found {} QML files to translate".format(len(files.splitlines()))) -cmd = "lupdate @qml.lst -ts qml.ts" +cmd = "lupdate @electrum/locale/qml.lst -ts electrum/locale/qml.ts" print('Collecting strings') subprocess.check_output(cmd, shell=True) -cmd = "lconvert -of po -o qml.pot qml.ts" +cmd = "lconvert -of po -o electrum/locale/messages_qml.pot electrum/locale/qml.ts" print('Convert to gettext') subprocess.check_output(cmd, shell=True) -cmd = "msgcat -u -o electrum/locale/messages.pot messages.pot qml.pot" +cmd = "msgcat -u -o electrum/locale/messages.pot electrum/locale/messages_gettext.pot electrum/locale/messages_qml.pot" print('Generate template') subprocess.check_output(cmd, shell=True) +# prepare uploading to crowdin os.chdir(os.path.join(project_root, "electrum")) crowdin_api_key = None From c4061f143be0bed22dbda5aae240314c4246bb63 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 30 Jan 2023 17:34:54 +0000 Subject: [PATCH 0037/1143] CI: (trivial) make "Locale" task depend on same py version it uses --- .cirrus.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index bbd06bf52..1f03e32ee 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -74,7 +74,7 @@ task: ELECTRUM_REQUIREMENTS_CI: contrib/requirements/requirements-ci.txt # in addition, crowdin_api_key is set as an "override" in https://cirrus-ci.com/settings/... depends_on: - - Tox Python 3.9 + - Tox Python 3.8 only_if: $CIRRUS_BRANCH == 'master' task: From d2bab4d51a1b526d5f2ec320f6b0020985a917fc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 13:15:19 +0100 Subject: [PATCH 0038/1143] qml: confirm close electrum dialog instead of double-tap back button --- electrum/gui/qml/components/main.qml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 48f1faba1..ec31d5720 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -30,6 +30,8 @@ ApplicationWindow property variant activeDialogs: [] + property bool _wantClose: false + header: ToolBar { id: toolbar @@ -302,23 +304,25 @@ ApplicationWindow stack.pop() } else { // destroy most GUI components so that we don't dump so many null reference warnings on exit - if (closeMsgTimer.running) { + if (app._wantClose) { app.header.visible = false mainStackView.clear() } else { - notificationPopup.show('Press Back again to exit') - closeMsgTimer.start() + var dialog = app.messageDialog.createObject(app, { + text: qsTr('Close Electrum?'), + yesno: true + }) + dialog.yesClicked.connect(function() { + dialog.close() + app._wantClose = true + app.close() + }) + dialog.open() close.accepted = false } } } - Timer { - id: closeMsgTimer - interval: 5000 - repeat: false - } - Connections { target: Daemon function onWalletRequiresPassword() { From cf3e5c0dfd87f2551b7dd9a70ad287c238ae5245 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 13:31:27 +0100 Subject: [PATCH 0039/1143] qml: hamburger menu use icon, not label --- electrum/gui/icons/hamburger.png | Bin 0 -> 240 bytes electrum/gui/qml/components/WalletMainView.qml | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 electrum/gui/icons/hamburger.png diff --git a/electrum/gui/icons/hamburger.png b/electrum/gui/icons/hamburger.png new file mode 100644 index 0000000000000000000000000000000000000000..4240b6635bd737fb78653c0e862279f6e269845b GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^h9Jzr1|*B;ILZJi&H|6fVg?4j!ywFfJby(BP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IXprTq&7srr{#PayA$UuwL9h z>EFe9d!mAMln(i&#Ie>ZJlQZcC9T-~dxO4&VZGu75BXL3Hlb3CirnWP2DQomBFlai&0cNaFN&nsvD#ryDd&GJ@ XmxpteTh>;fLl`_={an^LB{Ts5t@%<4 literal 0 HcmV?d00001 diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index d78cb72bf..08f8c079e 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -160,7 +160,10 @@ Item { FlatButton { Layout.fillWidth: false Layout.preferredWidth: implicitHeight - text: qsTr('≡') + icon.source: '../../icons/hamburger.png' + icon.height: constants.iconSizeSmall + icon.width: constants.iconSizeSmall + onClicked: { mainView.menu.open() mainView.menu.y = mainView.height - mainView.menu.height From 6111c69f1e86ec1e5801af01587a48e8c1ed9c58 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 14:00:41 +0100 Subject: [PATCH 0040/1143] qml: move Lightning can receive amount to ReceiveDialog, rename Lightning can send to Lightning (balance) in BalanceSummary FormattedAmount is now aware of FX rate changing and updates accordingly --- electrum/gui/qml/components/ReceiveDialog.qml | 20 +++++++- .../components/controls/BalanceSummary.qml | 49 +------------------ .../components/controls/FormattedAmount.qml | 23 ++++++++- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 22a0fcc03..34d15ae1d 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -179,6 +179,25 @@ ElDialog { } } + RowLayout { + Layout.alignment: Qt.AlignHCenter + visible: Daemon.currentWallet.isLightning + spacing: constants.paddingXSmall + Image { + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + source: '../../icons/lightning.png' + } + Label { + text: qsTr('can receive:') + font.pixelSize: constants.fontSizeSmall + color: Material.accentColor + } + FormattedAmount { + amount: Daemon.currentWallet.lightningCanReceive + } + } + Rectangle { height: 1 Layout.alignment: Qt.AlignHCenter @@ -410,5 +429,4 @@ ElDialog { } } } - } diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index e3ef3afba..e3c9ffb92 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -11,18 +11,14 @@ Item { property string formattedTotalBalance property string formattedTotalBalanceFiat - property string formattedLightningCanReceive - property string formattedLightningCanReceiveFiat property string formattedLightningCanSend property string formattedLightningCanSendFiat function setBalances() { root.formattedTotalBalance = Config.formatSats(Daemon.currentWallet.totalBalance) - root.formattedLightningCanReceive = Config.formatSats(Daemon.currentWallet.lightningCanReceive) root.formattedLightningCanSend = Config.formatSats(Daemon.currentWallet.lightningCanSend) if (Daemon.fx.enabled) { root.formattedTotalBalanceFiat = Daemon.fx.fiatValue(Daemon.currentWallet.totalBalance, false) - root.formattedLightningCanReceiveFiat = Daemon.fx.fiatValue(Daemon.currentWallet.lightningCanReceive, false) root.formattedLightningCanSendFiat = Daemon.fx.fiatValue(Daemon.currentWallet.lightningCanSend, false) } } @@ -68,49 +64,6 @@ Item { color: constants.mutedForeground text: Daemon.fx.fiatCurrency } - RowLayout { - visible: Daemon.currentWallet.isLightning - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - source: '../../../icons/lightning.png' - } - Label { - text: qsTr('can receive:') - font.pixelSize: constants.fontSizeSmall - color: Material.accentColor - } - } - Label { - visible: Daemon.currentWallet.isLightning - Layout.alignment: Qt.AlignRight - text: formattedLightningCanReceive - font.family: FixedFont - } - Label { - visible: Daemon.currentWallet.isLightning - font.pixelSize: constants.fontSizeSmall - color: Material.accentColor - text: Config.baseUnit - } - Item { - visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled - Layout.preferredHeight: 1 - Layout.preferredWidth: 1 - } - Label { - Layout.alignment: Qt.AlignRight - visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled - font.pixelSize: constants.fontSizeSmall - color: constants.mutedForeground - text: formattedLightningCanReceiveFiat - } - Label { - visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled - font.pixelSize: constants.fontSizeSmall - color: constants.mutedForeground - text: Daemon.fx.fiatCurrency - } RowLayout { visible: Daemon.currentWallet.isLightning @@ -120,7 +73,7 @@ Item { source: '../../../icons/lightning.png' } Label { - text: qsTr('can send:') + text: qsTr('Lightning:') font.pixelSize: constants.fontSizeSmall color: Material.accentColor } diff --git a/electrum/gui/qml/components/controls/FormattedAmount.qml b/electrum/gui/qml/components/controls/FormattedAmount.qml index ae1e716e0..cafc3d3c3 100644 --- a/electrum/gui/qml/components/controls/FormattedAmount.qml +++ b/electrum/gui/qml/components/controls/FormattedAmount.qml @@ -34,9 +34,30 @@ GridLayout { } Label { + id: fiatLabel Layout.columnSpan: singleLine ? 1 : 2 visible: showAlt && Daemon.fx.enabled && valid - text: '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' font.pixelSize: constants.fontSizeSmall } + + function setFiatValue() { + fiatLabel.text = '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' + } + + Connections { + target: Daemon.fx + function onQuotesUpdated() { setFiatValue() } + } + + Connections { + target: amount + function onValueChanged() { + setFiatValue() + } + } + + Component.onCompleted: { + if (showAlt) + setFiatValue() + } } From 845ea599faf122f22ed495608ccc4687ef378d09 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 14:02:16 +0100 Subject: [PATCH 0041/1143] qml: add bug icon --- electrum/gui/icons/bug.png | Bin 0 -> 3075 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 electrum/gui/icons/bug.png diff --git a/electrum/gui/icons/bug.png b/electrum/gui/icons/bug.png new file mode 100644 index 0000000000000000000000000000000000000000..0699a1abf2e943618ed3f15c5385e71533ec86f3 GIT binary patch literal 3075 zcmV+e4E*znP)Nc8q<}zKRIs=}AQVKTKuijn zC<#c=pisobnlxesK?TKxuoa34mPHXLOA88!MSobs7CTkb{;m2t3U;)Hra!lZU{~ixlp}r~IpaEJiO@+3QI^D_KCbjTfID*2S8pHrDXYP_-vKlj)S5R1F@~_PI zrI(x*CzAhDf`-3@xm^amp}<1aV)74M;qQ;lpf?E+CiQ(PFn``Nhgc)*PX1wQjENRz z&>9UGFkv~?#|ckTHJTrWC0NWLj%NOa{CsdO4MF|U_*au(#Sur}GKbj6n997K{Csg4 z4R9ww&&M>$=leIGhC6PKy5Y^{ldo`o7NrKj{8n4Z$C-QOk(mYWWUeN?e?FH-W;jkD zzk%LXd1Ph3oJ@QD-BDsE9VaiwY0y=r#i2i>pZIyA_mn-9wFUkgfu^Q+e{6urXDs z>lz=Po=Ib0Ani%Bs)sj3(>EWlHB1?Q&3ufib@&+`_P3{Mod8R$zoKe|@C^a==!drY zkm&MV=AUZfzjtW(yG^MY6ia@@7^^c&$!Ji`dOoIluWk7KHhq4N1*2IC`?IKdaS3Hw zbb?s=uo#U6xG20@N%URa z=XZ#SXo5vw46l|Aur#Yl@|GJf4tK)f@K|N#Y>-P!h0Sib^D9V{*P){$QcWUzXk5QC`(X=*0XdUUDi!v>e zNMh=18U#+naf-4Ypu~%4hbu&ld5C&D=_80fPooub>O+u7vu2PqOnjFh265}%J~}d`1v}^?;cMcC_>8@eXbZ z#>7G{eLIt!Yw<|~JALtxV0@W@4PWP`?gY8@-7MI-*T6<2W(tPExh-vkIyY4(=z_iv zDeOc!v*--LC;}U8_Ug5DZtPCb6@61k4mN$EMfrUpW&5;Qd9!kEtWMB1eT#+if7GJv zMr=;<=s$E0ig0MGP^(YU#vtk2PbeF!gr?^h&Jhfv(xG{3%Y?GW+wfg%`BtE&uIs~j z0#6HN9~4-2BhC?w-4IwV;@r;(Wsi4m588S7Zvu4jh)GwJ85~N4afNt}bk$tLysS<|r3@mW)#GX9$K+ z!8U8rcv7BiDAyt>^z|)tbzL7C_nC%r$8&m%@?XLS1S1x&8aNqG8eX^f=3-2-y~pp- zNim?pH}w}(Jy{lw-*tWWle=w9as;Grjm1d22lVPLZx8R9KAcD7YMT=+j4Tq2EU++A z5QJ07-8Sc07?HLswT=H5ghNWqwOE`J^U?Zk_$Y{v_mI17er#dn=Yo+gpW8R=!1UYI zHV<1EStA%RnQ9aS0Vl4y+Gdr75hde)u-I+s1V^lNu=!LJGnlZPh#2 z!pL@l5u_(~DM?~ffLzUgkh^WRwK5blDYPIAI7`sgCP-Z~J5xATT6jS?K1=R4x!=;* z*9Bvt8$$}hfx8x5ZE}{Su|ox8ZMKFL_yY@Y()a)UmIgN?d2{H#mIe!)K)Sc9EpSwf z`AsbM2u2rK8!YGxoPg?T3zHD=L`b)QwLKuBKnHC5{yVwb;AV0BDfK{NIs`D&h%VhWf>X%31kXvQ_<_C4BW z>)0ZsqhfKKsm0km1sg(hNaT$vgd8A<02ikaGGf4+Rr34(sl-Uf#p5loM=CKRfIUfm z`tMFDNIEqPM4mvZ+d3?W5U$PI;4?tOiGtNwZ-d5cSz+Ck##fzJ?;*LxMdW# zccysl_v6zM>xEap<}TqClrbXbU0mcwCLXX$1QijblJ29iVJM*uaw?qw!xu)qXxn1AJ|g@@)ytE#lx0 zsM^Twv!aqAv&^|^WS;s62PeC=x=>nn%kk?D_5kbY2w43=qm zWM&1Td{Tzhc$%F1n-bF2F|Np@6xIzVRL4r@kG1|qGG7N76 z)Zu8-&yIThCUtna7lQfZ=YZY-ajzU=jewg*q#adF>|~(F0w<7uir6EjKbMl<6)m}e zRZ`^eh1B)q8T2Lv(#b+Hc;K8XsoVPwaFZ7jRD2sA#3y24=J{BE76rz}mst6z((-2^mO1-;dG`n6N(|TePuDSK6p4 zb>^=o*4{bbwf9>JcVdso3_|)Dwyz;2-EszjnG901aV!hcw4}osG+aX+T++=2yhbtd z^$Cz@It20$aJAa!soJ@WXd@1^!Xe8z<$Q9@srA31KgGB71$dbA}=)h^%$RNtyfT6?|I!t@ZL7FXr zO^-K`E>ku)U#PVv+o(B8z`*Tzs^-nP8+syFQWfrwzCXX90tQn!3<(!n+YL0jUGLx}@B%N&>LX?SV<=M5ZQBGy+JP^3SydHrToMVjT=v%0IVsV`+ zZq&qj#Pc;`XT(&T;E(51-55v-QK|w)5K|w)5K|w)5K|#R?<9`!tE~pSN RmHhw!002ovPDHLkV1o3O*`@#h literal 0 HcmV?d00001 From d43f37d0783a2358859f744819eb46ac3dd9ee9a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 15:05:18 +0100 Subject: [PATCH 0042/1143] qml: add simple Toaster, mainly to indicate copy to clipboard --- .../gui/qml/components/ExportTxDialog.qml | 9 ++- .../gui/qml/components/GenericShareDialog.qml | 9 ++- electrum/gui/qml/components/ReceiveDialog.qml | 5 ++ .../gui/qml/components/controls/Toaster.qml | 57 +++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 electrum/gui/qml/components/controls/Toaster.qml diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 997276ef6..3170c3448 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -71,7 +71,10 @@ ElDialog { FlatButton { text: qsTr('Copy') icon.source: '../../icons/copy_bw.png' - onClicked: AppController.textToClipboard(dialog.text) + onClicked: { + AppController.textToClipboard(dialog.text) + toaster.show(this, qsTr('Copied!')) + } } FlatButton { text: qsTr('Share') @@ -84,6 +87,10 @@ ElDialog { } } + Toaster { + id: toaster + } + Connections { target: dialog.enter function onRunningChanged() { diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index 0c3ee2ebb..be8c8553a 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -79,7 +79,10 @@ ElDialog { FlatButton { text: qsTr('Copy') icon.source: '../../icons/copy_bw.png' - onClicked: AppController.textToClipboard(dialog.text) + onClicked: { + AppController.textToClipboard(dialog.text) + toaster.show(this, qsTr('Copied!')) + } } FlatButton { text: qsTr('Share') @@ -100,4 +103,8 @@ ElDialog { } } } + + Toaster { + id: toaster + } } diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 34d15ae1d..c37b1f018 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -255,6 +255,7 @@ ElDialog { AppController.textToClipboard(_bip21uri) else AppController.textToClipboard(_address) + toaster.show(this, qsTr('Copied!')) } } FlatButton { @@ -415,6 +416,10 @@ ElDialog { } } + Toaster { + id: toaster + } + Component.onCompleted: { // callLater to make sure any popups are on top of the dialog stacking order Qt.callLater(createDefaultRequest) diff --git a/electrum/gui/qml/components/controls/Toaster.qml b/electrum/gui/qml/components/controls/Toaster.qml new file mode 100644 index 000000000..8255187c5 --- /dev/null +++ b/electrum/gui/qml/components/controls/Toaster.qml @@ -0,0 +1,57 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.14 +import QtQuick.Controls.Material 2.0 + +import ".." + +Item { + id: toaster + width: rect.width + height: rect.height + visible: false + + property int _y + property string _text + + function show(item, text) { + _text = text + var r = item.mapToItem(parent, item.x, item.y) + x = r.x + y = r.y - toaster.height - constants.paddingLarge + toaster._y = y - 35 + ani.restart() + } + + SequentialAnimation { + id: ani + running: false + PropertyAction { target: toaster; property: 'visible'; value: true } + PropertyAction { target: toaster; property: 'opacity'; value: 1 } + PauseAnimation { duration: 1000} + ParallelAnimation { + NumberAnimation { target: toaster; property: 'y'; to: toaster._y; duration: 1000; easing.type: Easing.InQuad } + NumberAnimation { target: toaster; property: 'opacity'; to: 0; duration: 1000 } + } + PropertyAction { target: toaster; property: 'visible'; value: false } + } + + Rectangle { + id: rect + width: contentItem.width + height: contentItem.height + color: constants.colorAlpha(Material.dialogColor, 0.90) + border { + color: Material.accentColor + width: 1 + } + + RowLayout { + id: contentItem + Label { + Layout.margins: 10 + text: toaster._text + } + } + } +} From f304fa2e6041e61e929d75f55ef500cc42fe8186 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 15:08:09 +0100 Subject: [PATCH 0043/1143] qml: remove RequestDialog (unused) --- electrum/gui/qml/components/ReceiveDialog.qml | 7 - electrum/gui/qml/components/RequestDialog.qml | 301 ------------------ 2 files changed, 308 deletions(-) delete mode 100644 electrum/gui/qml/components/RequestDialog.qml diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index c37b1f018..2e89c7fd4 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -325,13 +325,6 @@ ElDialog { FocusScope { id: parkFocus } } - Component { - id: requestdialog - RequestDialog { - onClosed: destroy() - } - } - function createRequest() { var qamt = Config.unitsToSats(receiveDetailsDialog.amount) if (qamt.satsInt > Daemon.currentWallet.lightningCanReceive.satsInt) { diff --git a/electrum/gui/qml/components/RequestDialog.qml b/electrum/gui/qml/components/RequestDialog.qml deleted file mode 100644 index e0a9bc18d..000000000 --- a/electrum/gui/qml/components/RequestDialog.qml +++ /dev/null @@ -1,301 +0,0 @@ -import QtQuick 2.6 -import QtQuick.Layouts 1.0 -import QtQuick.Controls 2.14 -import QtQuick.Controls.Material 2.0 - -import org.electrum 1.0 - -import "controls" - -ElDialog { - id: dialog - title: qsTr('Payment Request') - - property string key - - property string _bolt11 - property string _bip21uri - property string _address - - property bool _render_qr: false // delay qr rendering until dialog is shown - - parent: Overlay.overlay - modal: true - - width: parent.width - height: parent.height - - Overlay.modal: Rectangle { - color: "#aa000000" - } - - Flickable { - anchors.fill: parent - contentHeight: rootLayout.height - clip:true - interactive: height < contentHeight - - ColumnLayout { - id: rootLayout - width: parent.width - spacing: constants.paddingMedium - - states: [ - State { - name: 'bolt11' - PropertyChanges { target: qrloader; sourceComponent: qri_bolt11 } - PropertyChanges { target: bolt11label; font.bold: true } - }, - State { - name: 'bip21uri' - PropertyChanges { target: qrloader; sourceComponent: qri_bip21uri } - PropertyChanges { target: bip21label; font.bold: true } - }, - State { - name: 'address' - PropertyChanges { target: qrloader; sourceComponent: qri_address } - PropertyChanges { target: addresslabel; font.bold: true } - } - ] - - Rectangle { - height: 1 - Layout.fillWidth: true - color: Material.accentColor - } - - Item { - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: constants.paddingSmall - Layout.bottomMargin: constants.paddingSmall - - Layout.preferredWidth: qrloader.width - Layout.preferredHeight: qrloader.height - - Loader { - id: qrloader - Component { - id: qri_bolt11 - QRImage { - qrdata: _bolt11 - render: _render_qr - } - } - Component { - id: qri_bip21uri - QRImage { - qrdata: _bip21uri - render: _render_qr - } - } - Component { - id: qri_address - QRImage { - qrdata: _address - render: _render_qr - } - } - } - - MouseArea { - anchors.fill: parent - onClicked: { - if (rootLayout.state == 'bolt11') { - if (_bip21uri != '') - rootLayout.state = 'bip21uri' - else if (_address != '') - rootLayout.state = 'address' - } else if (rootLayout.state == 'bip21uri') { - if (_address != '') - rootLayout.state = 'address' - else if (_bolt11 != '') - rootLayout.state = 'bolt11' - } else if (rootLayout.state == 'address') { - if (_bolt11 != '') - rootLayout.state = 'bolt11' - else if (_bip21uri != '') - rootLayout.state = 'bip21uri' - } - } - } - } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: constants.paddingLarge - Label { - id: bolt11label - text: qsTr('BOLT11') - color: _bolt11 ? Material.foreground : constants.mutedForeground - } - Rectangle { - Layout.preferredWidth: constants.paddingXXSmall - Layout.preferredHeight: constants.paddingXXSmall - radius: constants.paddingXXSmall / 2 - color: Material.accentColor - } - Label { - id: bip21label - text: qsTr('BIP21') - color: _bip21uri ? Material.foreground : constants.mutedForeground - } - Rectangle { - Layout.preferredWidth: constants.paddingXXSmall - Layout.preferredHeight: constants.paddingXXSmall - radius: constants.paddingXXSmall / 2 - color: Material.accentColor - } - Label { - id: addresslabel - text: qsTr('ADDRESS') - color: _address ? Material.foreground : constants.mutedForeground - } - } - - Rectangle { - height: 1 - Layout.fillWidth: true - color: Material.accentColor - } - - RowLayout { - Layout.alignment: Qt.AlignHCenter - Button { - icon.source: '../../icons/delete.png' - text: qsTr('Delete') - onClicked: { - Daemon.currentWallet.delete_request(request.key) - dialog.close() - } - } - Button { - icon.source: '../../icons/copy_bw.png' - icon.color: 'transparent' - text: 'Copy' - onClicked: { - if (request.isLightning && rootLayout.state == 'bolt11') - AppController.textToClipboard(_bolt11) - else if (rootLayout.state == 'bip21uri') - AppController.textToClipboard(_bip21uri) - else - AppController.textToClipboard(_address) - } - } - Button { - icon.source: '../../icons/share.png' - text: 'Share' - onClicked: { - enabled = false - if (request.isLightning && rootLayout.state == 'bolt11') - AppController.doShare(_bolt11, qsTr('Payment Request')) - else if (rootLayout.state == 'bip21uri') - AppController.doShare(_bip21uri, qsTr('Payment Request')) - else - AppController.doShare(_address, qsTr('Onchain address')) - - enabled = true - } - } - } - - GridLayout { - columns: 2 - - Label { - visible: request.message != '' - text: qsTr('Description') - } - Label { - visible: request.message != '' - Layout.fillWidth: true - wrapMode: Text.Wrap - text: request.message - font.pixelSize: constants.fontSizeLarge - } - - Label { - visible: request.amount.satsInt != 0 - text: qsTr('Amount') - } - RowLayout { - visible: request.amount.satsInt != 0 - Label { - text: Config.formatSats(request.amount) - font.family: FixedFont - font.pixelSize: constants.fontSizeLarge - font.bold: true - } - Label { - text: Config.baseUnit - color: Material.accentColor - font.pixelSize: constants.fontSizeLarge - } - - Label { - id: fiatValue - Layout.fillWidth: true - text: Daemon.fx.enabled - ? '(' + Daemon.fx.fiatValue(request.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')' - : '' - font.pixelSize: constants.fontSizeMedium - wrapMode: Text.Wrap - } - } - - Label { - visible: request.address - text: qsTr('Address') - } - - Label { - visible: request.address - Layout.fillWidth: true - font.family: FixedFont - font.pixelSize: constants.fontSizeLarge - wrapMode: Text.WrapAnywhere - text: request.address - } - - Label { - text: qsTr('Status') - } - Label { - Layout.fillWidth: true - font.pixelSize: constants.fontSizeLarge - text: request.status_str - } - } - } - } - - Component.onCompleted: { - if (!request.isLightning) { - _bip21uri = request.bip21 - _address = request.address - rootLayout.state = 'bip21uri' - } else { - _bolt11 = request.bolt11 - rootLayout.state = 'bolt11' - if (request.address != '') { - _bip21uri = request.bip21 - _address = request.address - } - } - } - - RequestDetails { - id: request - wallet: Daemon.currentWallet - key: dialog.key - } - - // hack. delay qr rendering until dialog is shown - Connections { - target: dialog.enter - function onRunningChanged() { - if (!dialog.enter.running) { - dialog._render_qr = true - } - } - } -} From 0b2db9ca46f05fecd8a3cfc42dd42b09f37d417e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 15:52:31 +0100 Subject: [PATCH 0044/1143] qml: FormattedAmount: also update fiat when the amount instance changes --- .../gui/qml/components/controls/FormattedAmount.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/controls/FormattedAmount.qml b/electrum/gui/qml/components/controls/FormattedAmount.qml index cafc3d3c3..52327d7ff 100644 --- a/electrum/gui/qml/components/controls/FormattedAmount.qml +++ b/electrum/gui/qml/components/controls/FormattedAmount.qml @@ -41,9 +41,12 @@ GridLayout { } function setFiatValue() { - fiatLabel.text = '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' + if (showAlt) + fiatLabel.text = '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' } + onAmountChanged: setFiatValue() + Connections { target: Daemon.fx function onQuotesUpdated() { setFiatValue() } @@ -56,8 +59,5 @@ GridLayout { } } - Component.onCompleted: { - if (showAlt) - setFiatValue() - } + Component.onCompleted: setFiatValue() } From 80f3492f2ac42aa25d442295fbca56e62a51e16e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 16:32:37 +0100 Subject: [PATCH 0045/1143] qml: add is_imported attribute to channel list item, show different icon if channel backup is imported --- electrum/gui/qml/components/controls/ChannelDelegate.qml | 4 +++- electrum/gui/qml/qechannellistmodel.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index 345a2f35c..54b8a05a2 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -37,7 +37,9 @@ ItemDelegate { Image { id: walleticon source: model.is_backup - ? '../../../icons/nocloud.png' + ? model.is_imported + ? '../../../icons/nocloud.png' + : '../../../icons/lightning_disconnected.png' : model.is_trampoline ? '../../../icons/kangaroo.png' : '../../../icons/lightning.png' diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 8c612d2f0..2f4c4c8f7 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -17,7 +17,7 @@ class QEChannelListModel(QAbstractListModel, QtEventListener): _ROLE_NAMES=('cid','state','state_code','initiator','capacity','can_send', 'can_receive','l_csv_delay','r_csv_delay','send_frozen','receive_frozen', 'type','node_id','node_alias','short_cid','funding_tx','is_trampoline', - 'is_backup') + 'is_backup', 'is_imported') _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)) @@ -86,9 +86,11 @@ def channel_to_model(self, lnc): if lnc.is_backup(): item['can_send'] = QEAmount() item['can_receive'] = QEAmount() + item['is_imported'] = lnc.is_imported else: item['can_send'] = QEAmount(amount_msat=lnc.available_to_spend(LOCAL)) item['can_receive'] = QEAmount(amount_msat=lnc.available_to_spend(REMOTE)) + item['is_imported'] = False return item numOpenChannelsChanged = pyqtSignal() From ca1edd154588c9cf5b7843229dc4cef079b47f76 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 31 Jan 2023 16:33:48 +0100 Subject: [PATCH 0046/1143] qml: remove channel backup button if channel list item is channelbackup --- electrum/gui/qml/components/ChannelDetails.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index ab03dffb0..6dba0e740 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -205,6 +205,7 @@ Pane { FlatButton { Layout.fillWidth: true + visible: !channeldetails.isBackup text: qsTr('Backup'); onClicked: { var dialog = app.genericShareDialog.createObject(root, From 9fd7bfd65ce6ad2d786ef1336565889605420f6d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 1 Feb 2023 10:42:49 +0100 Subject: [PATCH 0047/1143] handle empty stack in base_crash_handler --- electrum/base_crash_reporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index 5a77f33db..b3441afc1 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -84,8 +84,8 @@ def get_traceback_info(self): stack = traceback.extract_tb(self.exc_args[2]) readable_trace = self.__get_traceback_str_to_send() id = { - "file": stack[-1].filename, - "name": stack[-1].name, + "file": stack[-1].filename if len(stack) else '', + "name": stack[-1].name if len(stack) else '', "type": self.exc_args[0].__name__ } return { From a174e3780b1f15d467fcfc70914d4a9398fced33 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 1 Feb 2023 12:14:48 +0100 Subject: [PATCH 0048/1143] qml: add flickable to exceptiondialog --- .../gui/qml/components/ExceptionDialog.qml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/ExceptionDialog.qml b/electrum/gui/qml/components/ExceptionDialog.qml index 7e24e234c..843590156 100644 --- a/electrum/gui/qml/components/ExceptionDialog.qml +++ b/electrum/gui/qml/components/ExceptionDialog.qml @@ -54,7 +54,8 @@ ElDialog Layout.alignment: Qt.AlignCenter text: qsTr('Show report contents') onClicked: { - console.log('traceback: ' + crashData.traceback.stack) + if (crashData.traceback) + console.log('traceback: ' + crashData.traceback.stack) var dialog = report.createObject(app, { reportText: crashData.reportstring }) @@ -123,10 +124,17 @@ ElDialog header: null - Label { - text: reportText - wrapMode: Text.Wrap - width: parent.width + Flickable { + anchors.fill: parent + contentHeight: reportLabel.implicitHeight + interactive: height < contentHeight + + Label { + id: reportLabel + text: reportText + wrapMode: Text.Wrap + width: parent.width + } } } } From dbd2e56e855ab07deaa75c3dc9aff9b4c17e1993 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 2 Feb 2023 15:25:15 +0000 Subject: [PATCH 0049/1143] qt gui: more resilient startup Example log: app tries to auto-open to wallet "test_segwit_2", which has too new db version, then user manually tries to open wallet "test_segwit_3" instead, which opens okay but - immediately after - the process shuts down (due to line 383 -> line 458). ``` $ ./run_electrum -v --testnet -o 0.59 | I | simple_config.SimpleConfig | electrum directory /home/user/.electrum/testnet 0.59 | I | logging | Electrum version: 4.3.4 - https://electrum.org - https://github.com/spesmilo/electrum 0.59 | I | logging | Python version: 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0]. On platform: Linux-5.15.0-58-generic-x86_64-with-glibc2.35 0.59 | I | logging | Logging to file: /home/user/.electrum/testnet/logs/electrum_log_20230202T151759Z_220451.log 0.59 | I | logging | Log filters: verbosity '*', verbosity_shortcuts '' 0.59 | I | p/plugin.Plugins | registering hardware bitbox02: ('hardware', 'bitbox02', 'BitBox02') 0.59 | I | p/plugin.Plugins | registering hardware coldcard: ('hardware', 'coldcard', 'Coldcard Wallet') 0.59 | I | p/plugin.Plugins | registering hardware digitalbitbox: ('hardware', 'digitalbitbox', 'Digital Bitbox wallet') 0.60 | I | p/plugin.Plugins | registering hardware jade: ('hardware', 'jade', 'Jade wallet') 0.60 | I | p/plugin.Plugins | registering hardware keepkey: ('hardware', 'keepkey', 'KeepKey wallet') 0.60 | I | p/plugin.Plugins | registering hardware ledger: ('hardware', 'ledger', 'Ledger wallet') 1.74 | I | p/plugin.Plugins | loaded payserver 1.74 | I | p/plugin.Plugins | registering hardware safe_t: ('hardware', 'safe_t', 'Safe-T mini wallet') 1.74 | I | p/plugin.Plugins | registering hardware trezor: ('hardware', 'trezor', 'Trezor wallet') 1.74 | I | p/plugin.Plugins | registering wallet type ('2fa', 'trustedcoin') 1.74 | D | util.profiler | Plugins.__init__ 1.1522 sec 1.74 | I | exchange_rate.FxThread | using exchange CoinGecko 1.75 | D | util.profiler | Daemon.__init__ 0.0033 sec 1.75 | I | daemon.Daemon | starting taskgroup. 1.75 | I | daemon.Daemon | launching GUI: qt 1.75 | I | gui.qt.ElectrumGui | Qt GUI starting up... Qt=5.15.3, PyQt=5.15.6 1.75 | I | daemon.CommandsServer | now running and listening. socktype=unix, addr=/home/user/.electrum/testnet/daemon_rpc_socket Warning: Ignoring XDG_SESSION_TYPE=wayland on Gnome. Use QT_QPA_PLATFORM=wayland to run on Wayland anyway. 2.04 | D | util.profiler | ElectrumGui.__init__ 0.2865 sec 2.04 | I | storage.WalletStorage | wallet path /home/user/.electrum/testnet/wallets/test_segwit_2 2.13 | I | storage.WalletStorage | wallet path /home/user/.electrum/testnet/wallets/test_segwit_2 5.24 | E | gui.qt.ElectrumGui | Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/__init__.py", line 354, in start_new_window wallet = self._start_wizard_to_select_or_create_wallet(path) File "/home/user/wspace/electrum/electrum/gui/qt/__init__.py", line 401, in _start_wizard_to_select_or_create_wallet db = WalletDB(storage.read(), manual_upgrades=False) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 72, in __init__ self.load_data(raw) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 102, in load_data if not self.requires_upgrade(): File "/home/user/wspace/electrum/electrum/wallet_db.py", line 153, in requires_upgrade return self.get_seed_version() < FINAL_SEED_VERSION File "/home/user/wspace/electrum/electrum/json_db.py", line 44, in wrapper return func(self, *args, **kwargs) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 1035, in get_seed_version raise WalletFileException('This version of Electrum is too old to open this wallet.\n' electrum.util.WalletFileException: This version of Electrum is too old to open this wallet. (highest supported storage version: 50, version of this file: 51) 5.35 | I | storage.WalletStorage | wallet path /home/user/.electrum/testnet/wallets/wallet_20 7.90 | I | storage.WalletStorage | wallet path /home/user/.electrum/testnet/wallets/test_segwit_3 8.48 | D | util.profiler | WalletDB._load_transactions 0.0517 sec 8.48 | D | util.profiler | AddressSynchronizer.load_local_history 0.0005 sec 8.48 | D | util.profiler | AddressSynchronizer.check_history 0.0005 sec 8.70 | D | util.profiler | AddressList.update 0.0000 sec 9.00 | D | util.profiler | Deterministic_Wallet.try_detecting_internal_addresses_corruption 0.0223 sec 9.01 | D | util.profiler | ElectrumWindow.load_wallet 0.0808 sec 9.01 | I | daemon.Daemon | stop() entered. initiating shutdown 9.01 | I | gui.qt.ElectrumGui | closing GUI 9.01 | I | daemon.Daemon | stopping all wallets 9.04 | I | storage.WalletStorage | saved /home/user/.electrum/testnet/wallets/test_segwit_3 9.04 | D | util.profiler | WalletDB._write 0.0265 sec 9.04 | I | daemon.Daemon | stopping network and taskgroup 9.04 | I | daemon.Daemon | taskgroup stopped. 9.04 | I | daemon.Daemon | removing lockfile 9.04 | I | daemon.Daemon | stopped 9.08 | I | p/plugin.Plugins | stopped QThread: Destroyed while thread is still running Aborted (core dumped) ``` --- electrum/gui/qt/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 2ff8ce669..73ba81e53 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -330,8 +330,11 @@ def start_new_window( app_is_starting: bool = False, force_wizard: bool = False, ) -> Optional[ElectrumWindow]: - '''Raises the window for the wallet if it is open. Otherwise - opens the wallet and creates a new window for it''' + """Raises the window for the wallet if it is open. + Otherwise, opens the wallet and creates a new window for it. + Warning: the returned window might be for a completely different wallet + than the provided path, as we allow user interaction to change the path. + """ wallet = None # Try to open with daemon first. If this succeeds, there won't be a wizard at all # (the wallet main window will appear directly). @@ -379,7 +382,7 @@ def start_new_window( path = self.config.get_fallback_wallet_path() else: path = os.path.join(wallet_dir, filename) - self.start_new_window(path, uri=None, force_wizard=True) + return self.start_new_window(path, uri=None, force_wizard=True) return window.bring_to_top() window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) From 96ac199f5cb27b6f3158c922afbb1322c5660121 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 3 Feb 2023 10:45:14 +0100 Subject: [PATCH 0050/1143] minor fix; follow-up d6febb5c1243f3f80d5a79af9aa39312c8166c91 --- electrum/gui/qt/transaction_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 0916af943..f2e0a5fec 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -614,7 +614,7 @@ def insert_tx_io(cursor, is_coinbase, short_id, address, value): for txin in self.tx.inputs(): addr = self.wallet.adb.get_txin_address(txin) txin_value = self.wallet.adb.get_txin_value(txin) - insert_tx_io(cursor, txin.is_coinbase_output(), txin.short_id, addr, txin_value) + insert_tx_io(cursor, txin.is_coinbase_input(), txin.short_id, addr, txin_value) self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) o_text = self.outputs_textedit From 7ea2a2a8eab680c6ac8c8c6abca5e3a5ae9baaa1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 1 Feb 2023 15:44:30 +0100 Subject: [PATCH 0051/1143] qml: use dirty flag on qetransactionlistmodel --- electrum/gui/qml/components/ChannelDetails.qml | 2 +- electrum/gui/qml/components/OpenChannelDialog.qml | 2 +- electrum/gui/qml/components/TxDetails.qml | 2 +- electrum/gui/qml/qetransactionlistmodel.py | 14 +++++++++++++- electrum/gui/qml/qewallet.py | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 6dba0e740..2ba369398 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -248,7 +248,7 @@ Pane { dialog.yesClicked.connect(function() { channeldetails.deleteChannel() app.stack.pop() - Daemon.currentWallet.historyModel.init_model() // needed here? + Daemon.currentWallet.historyModel.init_model(true) // needed here? Daemon.currentWallet.channelModel.remove_channel(channelid) }) dialog.open() diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 44545c04f..642e98ca6 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -227,7 +227,7 @@ ElDialog { + qsTr('This channel will be usable after %1 confirmations').arg(min_depth) if (!tx_complete) { message = message + '\n\n' + qsTr('Please sign and broadcast the funding transaction.') - channelopener.wallet.historyModel.init_model() // local tx doesn't trigger model update + channelopener.wallet.historyModel.init_model(true) // local tx doesn't trigger model update } app.channelOpenProgressDialog.state = 'success' app.channelOpenProgressDialog.info = message diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index ae220e179..06c6602b3 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -384,7 +384,7 @@ Pane { dialog.yesClicked.connect(function() { dialog.close() txdetails.removeLocalTx(true) - txdetails.wallet.historyModel.init_model() + txdetails.wallet.historyModel.init_model(true) root.close() }) dialog.open() diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 478a7b8c1..33f2f4bde 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -32,6 +32,7 @@ def __init__(self, wallet, parent=None, *, onchain_domain=None, include_lightnin self.destroyed.connect(lambda: self.on_destroy()) self.requestRefresh.connect(lambda: self.init_model()) + self.setDirty() self.init_model() def on_destroy(self): @@ -71,6 +72,10 @@ def data(self, index, role): return value.value return str(value) + @pyqtSlot() + def setDirty(self): + self._dirty = True + def clear(self): self.beginResetModel() self.tx_history = [] @@ -139,7 +144,12 @@ def format_date_by_section(self, section, date): # initial model data @pyqtSlot() - def init_model(self): + @pyqtSlot(bool) + def init_model(self, force: bool = False): + # only (re)construct if dirty or forced + if not self._dirty and not force: + return + self._logger.debug('retrieving history') history = self.wallet.get_full_history(onchain_domain=self.onchain_domain, include_lightning=self.include_lightning) @@ -155,6 +165,8 @@ def init_model(self): self.countChanged.emit() + self._dirty = False + def on_tx_verified(self, txid, info): i = 0 for tx in self.tx_history: diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 15871546a..06e874633 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -148,6 +148,9 @@ def on_event_status(self): self._isUpToDate = uptodate self.isUptodateChanged.emit() + if uptodate: + self.historyModel.init_model() + if self.wallet.network.is_connected(): server_height = self.wallet.network.get_server_height() server_lag = self.wallet.network.get_local_height() - server_height @@ -187,7 +190,7 @@ def on_event_new_transaction(self, wallet, tx): self._logger.info(f'new transaction {tx.txid()}') self.add_tx_notification(tx) self.addressModel.setDirty() - self.historyModel.init_model() # TODO: be less dramatic + self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after @event_listener def on_event_wallet_updated(self, wallet): From 41f137a1276d997e6513362447e82233c461d285 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 11:43:37 +0100 Subject: [PATCH 0052/1143] qml: qewallet sync status and progress --- electrum/gui/qml/qewallet.py | 51 ++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 06e874633..563393d15 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -106,6 +106,11 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.notification_timer.setInterval(500) # msec self.notification_timer.timeout.connect(self.notify_transactions) + self.sync_progress_timer = QTimer(self) + self.sync_progress_timer.setSingleShot(False) + self.sync_progress_timer.setInterval(2000) + self.sync_progress_timer.timeout.connect(self.update_sync_progress) + # To avoid leaking references to "self" that prevent the # window from being GC-ed when closed, callbacks should be # methods of this class only, and specifically not be @@ -114,6 +119,8 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) + self.synchronizing = True # start in sync state + @pyqtProperty(bool, notify=isUptodateChanged) def isUptodate(self): return self._isUpToDate @@ -126,21 +133,29 @@ def synchronizing(self): @synchronizing.setter def synchronizing(self, synchronizing): if self._synchronizing != synchronizing: + self._logger.info(f'SYNC {self._synchronizing} -> {synchronizing}') self._synchronizing = synchronizing self.synchronizingChanged.emit() + if synchronizing: + if not self.sync_progress_timer.isActive(): + self.update_sync_progress() + self.sync_progress_timer.start() + else: + self.sync_progress_timer.stop() synchronizingProgressChanged = pyqtSignal() @pyqtProperty(str, notify=synchronizingProgressChanged) - def synchronizing_progress(self): + def synchronizingProgress(self): return self._synchronizing_progress - @synchronizing_progress.setter - def synchronizing_progress(self, progress): + @synchronizingProgress.setter + def synchronizingProgress(self, progress): if self._synchronizing_progress != progress: self._synchronizing_progress = progress + self._logger.info(progress) self.synchronizingProgressChanged.emit() - @event_listener + @qt_event_listener def on_event_status(self): self._logger.debug('status') uptodate = self.wallet.is_up_to_date() @@ -151,21 +166,6 @@ def on_event_status(self): if uptodate: self.historyModel.init_model() - if self.wallet.network.is_connected(): - server_height = self.wallet.network.get_server_height() - server_lag = self.wallet.network.get_local_height() - server_height - # Server height can be 0 after switching to a new server - # until we get a headers subscription request response. - # Display the synchronizing message in that case. - if not self._isUpToDate or server_height == 0: - num_sent, num_answered = self.wallet.adb.get_history_sync_state_details() - self.synchronizing_progress = ("{} ({}/{})" - .format(_("Synchronizing..."), num_answered, num_sent)) - self.synchronizing = True - else: - self.synchronizing_progress = '' - self.synchronizing = False - @qt_event_listener def on_event_request_status(self, wallet, key, status): if wallet == self.wallet: @@ -176,7 +176,7 @@ def on_event_request_status(self, wallet, key, status): # TODO: only update if it was paid over lightning, # and even then, we can probably just add the payment instead # of recreating the whole history (expensive) - self.historyModel.init_model() + self.historyModel.init_model(True) @event_listener def on_event_invoice_status(self, wallet, key, status): @@ -192,11 +192,12 @@ def on_event_new_transaction(self, wallet, tx): self.addressModel.setDirty() self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after - @event_listener + @qt_event_listener def on_event_wallet_updated(self, wallet): if wallet == self.wallet: self._logger.debug('wallet %s updated' % str(wallet)) self.balanceChanged.emit() + self.synchronizing = not wallet.is_up_to_date() @event_listener def on_event_channel(self, wallet, channel): @@ -214,7 +215,7 @@ def on_event_channels_updated(self, wallet): def on_event_payment_succeeded(self, wallet, key): if wallet == self.wallet: self.paymentSucceeded.emit(key) - self.historyModel.init_model() # TODO: be less dramatic + self.historyModel.init_model(True) # TODO: be less dramatic @event_listener def on_event_payment_failed(self, wallet, key, reason): @@ -269,6 +270,12 @@ def notify_transactions(self): self.userNotify.emit(self.wallet, _("New transaction: {}").format(config.format_amount_and_units(tx_wallet_delta.delta))) + def update_sync_progress(self): + if self.wallet.network.is_connected(): + num_sent, num_answered = self.wallet.adb.get_history_sync_state_details() + self.synchronizingProgress = \ + ("{} ({}/{})".format(_("Synchronizing..."), num_answered, num_sent)) + historyModelChanged = pyqtSignal() @pyqtProperty(QETransactionListModel, notify=historyModelChanged) def historyModel(self): From 13e340870af4f3c57c9f83c2e9eec99b51f6d493 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Feb 2023 00:11:37 +0100 Subject: [PATCH 0053/1143] qml: BalanceSummary sync progress --- .../gui/qml/components/controls/BalanceSummary.qml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index e3c9ffb92..f92d458e4 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -25,9 +25,12 @@ Item { TextHighlightPane { id: balancePane + leftPadding: constants.paddingXLarge + rightPadding: constants.paddingXLarge GridLayout { columns: 3 + opacity: Daemon.currentWallet.synchronizing ? 0 : 1 Label { font.pixelSize: constants.fontSizeXLarge @@ -112,6 +115,14 @@ Item { } + Label { + opacity: Daemon.currentWallet.synchronizing ? 1 : 0 + anchors.centerIn: balancePane + text: Daemon.currentWallet.synchronizingProgress + color: Material.accentColor + font.pixelSize: constants.fontSizeLarge + } + MouseArea { anchors.fill: balancePane onClicked: { From 5af399d19639b0c141398db964270c4974f124ac Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 3 Feb 2023 11:42:15 +0100 Subject: [PATCH 0054/1143] transaction_dialog: move tx in/out widgets into own class - this widget will be used in various dialogs - making this change now will prevent downstream conflicts - the part that displays "coin selection active" was commented out --- electrum/gui/qt/transaction_dialog.py | 200 ++++++++++++++------------ 1 file changed, 108 insertions(+), 92 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index f2e0a5fec..c7381757b 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -78,6 +78,111 @@ class QTextEditWithDefaultSize(QTextEdit): def sizeHint(self): return QSize(0, 100) +class TxInOutWidget(QWidget): + + def __init__(self, main_window, wallet): + QWidget.__init__(self) + + self.wallet = wallet + self.main_window = main_window + self.inputs_header = QLabel() + self.inputs_textedit = QTextEditWithDefaultSize() + self.txo_color_recv = TxOutputColoring( + legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address")) + self.txo_color_change = TxOutputColoring( + legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address")) + self.txo_color_2fa = TxOutputColoring( + legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) + self.outputs_header = QLabel() + self.outputs_textedit = QTextEditWithDefaultSize() + + self.inputs_textedit.setMinimumWidth(950) + self.outputs_textedit.setMinimumWidth(950) + + outheader_hbox = QHBoxLayout() + outheader_hbox.setContentsMargins(0, 0, 0, 0) + outheader_hbox.addWidget(self.outputs_header) + outheader_hbox.addStretch(2) + outheader_hbox.addWidget(self.txo_color_recv.legend_label) + outheader_hbox.addWidget(self.txo_color_change.legend_label) + outheader_hbox.addWidget(self.txo_color_2fa.legend_label) + + vbox = QVBoxLayout() + vbox.addWidget(self.inputs_header) + vbox.addWidget(self.inputs_textedit) + vbox.addLayout(outheader_hbox) + vbox.addWidget(self.outputs_textedit) + self.setLayout(vbox) + + def update(self, tx): + self.tx = tx + inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs()) + + #if not self.finalized: + # selected_coins = self.main_window.get_manually_selected_coins() + # if selected_coins is not None: + # inputs_header_text += f" - " + _("Coin selection active ({} UTXOs selected)").format(len(selected_coins)) + + self.inputs_header.setText(inputs_header_text) + + ext = QTextCharFormat() + tf_used_recv, tf_used_change, tf_used_2fa = False, False, False + def text_format(addr): + nonlocal tf_used_recv, tf_used_change, tf_used_2fa + if self.wallet.is_mine(addr): + if self.wallet.is_change(addr): + tf_used_change = True + return self.txo_color_change.text_char_format + else: + tf_used_recv = True + return self.txo_color_recv.text_char_format + elif self.wallet.is_billing_address(addr): + tf_used_2fa = True + return self.txo_color_2fa.text_char_format + return ext + + def insert_tx_io(cursor, is_coinbase, short_id, address, value): + if is_coinbase: + cursor.insertText('coinbase') + else: + address_str = address or '
' + value_str = self.main_window.format_amount(value, whitespaces=True) + cursor.insertText("%-15s\t"%str(short_id), ext) + cursor.insertText("%-62s"%address_str, text_format(address)) + cursor.insertText('\t', ext) + cursor.insertText(value_str, ext) + cursor.insertBlock() + + i_text = self.inputs_textedit + i_text.clear() + i_text.setFont(QFont(MONOSPACE_FONT)) + i_text.setReadOnly(True) + cursor = i_text.textCursor() + for txin in self.tx.inputs(): + addr = self.wallet.adb.get_txin_address(txin) + txin_value = self.wallet.adb.get_txin_value(txin) + insert_tx_io(cursor, txin.is_coinbase_input(), txin.short_id, addr, txin_value) + + self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) + o_text = self.outputs_textedit + o_text.clear() + o_text.setFont(QFont(MONOSPACE_FONT)) + o_text.setReadOnly(True) + tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) + tx_hash = bytes.fromhex(self.tx.txid()) + cursor = o_text.textCursor() + for index, o in enumerate(self.tx.outputs()): + if tx_pos is not None and tx_pos >= 0: + short_id = ShortID.from_components(tx_height, tx_pos, index) + else: + short_id = TxOutpoint(tx_hash, index).short_name() + + addr, value = o.get_ui_address_str(), o.value + insert_tx_io(cursor, False, short_id, addr, value) + + self.txo_color_recv.legend_label.setVisible(tf_used_recv) + self.txo_color_change.legend_label.setVisible(tf_used_change) + self.txo_color_2fa.legend_label.setVisible(tf_used_2fa) _logger = get_logger(__name__) @@ -113,7 +218,6 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.saved = False self.desc = desc self.setMinimumWidth(640) - self.resize(1200,600) self.set_title() self.psbt_only_widgets = [] # type: List[QWidget] @@ -129,30 +233,8 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz vbox.addSpacing(10) - self.inputs_header = QLabel() - vbox.addWidget(self.inputs_header) - self.inputs_textedit = QTextEditWithDefaultSize() - vbox.addWidget(self.inputs_textedit) - - self.txo_color_recv = TxOutputColoring( - legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address")) - self.txo_color_change = TxOutputColoring( - legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address")) - self.txo_color_2fa = TxOutputColoring( - legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) - - outheader_hbox = QHBoxLayout() - outheader_hbox.setContentsMargins(0, 0, 0, 0) - vbox.addLayout(outheader_hbox) - self.outputs_header = QLabel() - outheader_hbox.addWidget(self.outputs_header) - outheader_hbox.addStretch(2) - outheader_hbox.addWidget(self.txo_color_recv.legend_label) - outheader_hbox.addWidget(self.txo_color_change.legend_label) - outheader_hbox.addWidget(self.txo_color_2fa.legend_label) - - self.outputs_textedit = QTextEditWithDefaultSize() - vbox.addWidget(self.outputs_textedit) + self.io_widget = TxInOutWidget(self.main_window, self.wallet) + vbox.addWidget(self.io_widget) self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) @@ -418,7 +500,7 @@ def update(self): self.finalize_button.setEnabled(self.can_finalize()) if self.tx is None: return - self.update_io() + self.io_widget.update(self.tx) desc = self.desc base_unit = self.main_window.base_unit() format_amount = self.main_window.format_amount @@ -570,72 +652,6 @@ def update(self): run_hook('transaction_dialog_update', self) - def update_io(self): - inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs()) - if not self.finalized: - selected_coins = self.main_window.get_manually_selected_coins() - if selected_coins is not None: - inputs_header_text += f" - " + _("Coin selection active ({} UTXOs selected)").format(len(selected_coins)) - self.inputs_header.setText(inputs_header_text) - - ext = QTextCharFormat() - tf_used_recv, tf_used_change, tf_used_2fa = False, False, False - def text_format(addr): - nonlocal tf_used_recv, tf_used_change, tf_used_2fa - if self.wallet.is_mine(addr): - if self.wallet.is_change(addr): - tf_used_change = True - return self.txo_color_change.text_char_format - else: - tf_used_recv = True - return self.txo_color_recv.text_char_format - elif self.wallet.is_billing_address(addr): - tf_used_2fa = True - return self.txo_color_2fa.text_char_format - return ext - - def insert_tx_io(cursor, is_coinbase, short_id, address, value): - if is_coinbase: - cursor.insertText('coinbase') - else: - address_str = address or '
' - value_str = self.main_window.format_amount(value, whitespaces=True) - cursor.insertText("%-15s\t"%str(short_id), ext) - cursor.insertText("%-62s"%address_str, text_format(address)) - cursor.insertText('\t', ext) - cursor.insertText(value_str, ext) - cursor.insertBlock() - - i_text = self.inputs_textedit - i_text.clear() - i_text.setFont(QFont(MONOSPACE_FONT)) - i_text.setReadOnly(True) - cursor = i_text.textCursor() - for txin in self.tx.inputs(): - addr = self.wallet.adb.get_txin_address(txin) - txin_value = self.wallet.adb.get_txin_value(txin) - insert_tx_io(cursor, txin.is_coinbase_input(), txin.short_id, addr, txin_value) - - self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) - o_text = self.outputs_textedit - o_text.clear() - o_text.setFont(QFont(MONOSPACE_FONT)) - o_text.setReadOnly(True) - tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) - tx_hash = bytes.fromhex(self.tx.txid()) - cursor = o_text.textCursor() - for index, o in enumerate(self.tx.outputs()): - if tx_pos is not None and tx_pos >= 0: - short_id = ShortID.from_components(tx_height, tx_pos, index) - else: - short_id = TxOutpoint(tx_hash, index).short_name() - - addr, value = o.get_ui_address_str(), o.value - insert_tx_io(cursor, False, short_id, addr, value) - - self.txo_color_recv.legend_label.setVisible(tf_used_recv) - self.txo_color_change.legend_label.setVisible(tf_used_change) - self.txo_color_2fa.legend_label.setVisible(tf_used_2fa) def add_tx_stats(self, vbox): hbox_stats = QHBoxLayout() From 4bb4941a52fefa57c403cc3c38780ab443027ec2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 13:21:52 +0100 Subject: [PATCH 0055/1143] qml: add ButtonContainer, which lays out its children horizontally with separators in between --- .../components/controls/ButtonContainer.qml | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 electrum/gui/qml/components/controls/ButtonContainer.qml diff --git a/electrum/gui/qml/components/controls/ButtonContainer.qml b/electrum/gui/qml/components/controls/ButtonContainer.qml new file mode 100644 index 000000000..98e5e89cc --- /dev/null +++ b/electrum/gui/qml/components/controls/ButtonContainer.qml @@ -0,0 +1,63 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 + +Container { + id: root + + property Item _layout + + function fillContentItem() { + var contentRoot = containerLayout.createObject(root) + + contentRoot.children.length = 0 // empty array + let total = contentChildren.length + + let rowheight = 0 + for (let i = 0; i < contentChildren.length; i++) { + rowheight = Math.max(rowheight, root.itemAt(i).implicitHeight) + } + + for (let i = 0; i < contentChildren.length; i++) { + var button = root.itemAt(i) + + contentRoot.children.push(verticalSeparator.createObject(_layout, { + pheight: rowheight * 2/3, + visible: Qt.binding(function() { + let anybefore_visible = false + for (let j = i-1; j >= 0; j--) { + anybefore_visible = anybefore_visible || root.itemAt(j).visible + } + return button.visible && anybefore_visible + }) + })) + + contentRoot.children.push(button) + } + + contentItem = contentRoot + } + + Component.onCompleted: fillContentItem() + + Component { + id: containerLayout + RowLayout { + spacing: 0 + } + } + + Component { + id: verticalSeparator + Rectangle { + required property int pheight + Layout.fillWidth: false + Layout.preferredWidth: 2 + Layout.preferredHeight: pheight + Layout.alignment: Qt.AlignVCenter + color: constants.darkerBackground + } + } + +} From d89aebc4bb72b9ac22fb4834e6d67d01b07e0e03 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 13:32:40 +0100 Subject: [PATCH 0056/1143] qml: FlatButton icon above text, smaller fontsize --- .../gui/qml/components/controls/FlatButton.qml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/electrum/gui/qml/components/controls/FlatButton.qml b/electrum/gui/qml/components/controls/FlatButton.qml index 30868226c..df336eaed 100644 --- a/electrum/gui/qml/components/controls/FlatButton.qml +++ b/electrum/gui/qml/components/controls/FlatButton.qml @@ -1,6 +1,24 @@ import QtQuick 2.6 import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.15 +import QtQuick.Controls.impl 2.15 +import QtQuick.Controls.Material.impl 2.15 TabButton { + id: control checkable: false + + font.pixelSize: constants.fontSizeSmall + display: IconLabel.TextUnderIcon + + contentItem: IconLabel { + spacing: control.spacing + mirrored: control.mirrored + display: control.display + + icon: control.icon + text: control.text + font: control.font + color: !control.enabled ? control.Material.hintTextColor : control.down || control.checked ? control.Material.accentColor : control.Material.foreground + } } From b2a02dd047f409757a25aba0f0b1483807f81a78 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 13:33:17 +0100 Subject: [PATCH 0057/1143] qml: put FlatButtons in ButtonContainer where (potentially) more buttons are grouped --- .../gui/qml/components/ChannelDetails.qml | 84 ++++++++++--------- electrum/gui/qml/components/Channels.qml | 50 ++++++----- .../gui/qml/components/ExportTxDialog.qml | 4 +- electrum/gui/qml/components/InvoiceDialog.qml | 65 +++++++------- .../gui/qml/components/NetworkOverview.qml | 35 ++++---- electrum/gui/qml/components/ReceiveDialog.qml | 2 +- electrum/gui/qml/components/SendDialog.qml | 37 ++++---- electrum/gui/qml/components/TxDetails.qml | 25 +++--- electrum/gui/qml/components/WalletDetails.qml | 58 +++++++------ .../gui/qml/components/WalletMainView.qml | 42 +++++----- 10 files changed, 221 insertions(+), 181 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 2ba369398..243f033a5 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -203,58 +203,60 @@ Pane { } } - FlatButton { + ButtonContainer { Layout.fillWidth: true - visible: !channeldetails.isBackup - text: qsTr('Backup'); - onClicked: { - var dialog = app.genericShareDialog.createObject(root, - { + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + visible: !channeldetails.isBackup + text: qsTr('Backup') + onClicked: { + var dialog = app.genericShareDialog.createObject(root, { title: qsTr('Channel Backup for %1').arg(channeldetails.short_cid), text: channeldetails.channelBackup(), text_help: channeldetails.channelBackupHelpText(), iconSource: Qt.resolvedUrl('../../icons/file.png') - } - ) - dialog.open() + }) + dialog.open() + } + icon.source: '../../icons/file.png' } - icon.source: '../../icons/file.png' - } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Close channel'); - visible: channeldetails.canClose - onClicked: { - var dialog = closechannel.createObject(root, { 'channelid': channelid }) - dialog.open() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Close channel'); + visible: channeldetails.canClose + onClicked: { + var dialog = closechannel.createObject(root, { channelid: channelid }) + dialog.open() + } + icon.source: '../../icons/closebutton.png' } - icon.source: '../../icons/closebutton.png' - } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Delete channel'); - visible: channeldetails.canDelete - onClicked: { - var dialog = app.messageDialog.createObject(root, - { - 'text': qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), - 'yesno': true - } - ) - dialog.yesClicked.connect(function() { - channeldetails.deleteChannel() - app.stack.pop() - Daemon.currentWallet.historyModel.init_model(true) // needed here? - Daemon.currentWallet.channelModel.remove_channel(channelid) - }) - dialog.open() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Delete channel'); + visible: channeldetails.canDelete + onClicked: { + var dialog = app.messageDialog.createObject(root, { + text: qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), + yesno: true + }) + dialog.yesClicked.connect(function() { + channeldetails.deleteChannel() + app.stack.pop() + Daemon.currentWallet.historyModel.init_model(true) // needed here? + Daemon.currentWallet.channelModel.remove_channel(channelid) + }) + dialog.open() + } + icon.source: '../../icons/delete.png' } - icon.source: '../../icons/delete.png' } + } ChannelDetails { diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index c05c14c8b..3e04d9cc4 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -103,34 +103,40 @@ Pane { } - FlatButton { + ButtonContainer { Layout.fillWidth: true - text: qsTr('Swap'); - visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 - icon.source: '../../icons/status_waiting.png' - onClicked: { - var dialog = swapDialog.createObject(root) - dialog.open() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Swap'); + visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 + icon.source: '../../icons/status_waiting.png' + onClicked: { + var dialog = swapDialog.createObject(root) + dialog.open() + } } - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Open Channel') - onClicked: { - var dialog = openChannelDialog.createObject(root) - dialog.open() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Open Channel') + onClicked: { + var dialog = openChannelDialog.createObject(root) + dialog.open() + } + icon.source: '../../icons/lightning.png' } - icon.source: '../../icons/lightning.png' - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Channel backups') - onClicked: { - app.stack.push(Qt.resolvedUrl('ChannelBackups.qml')) + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Channel backups') + onClicked: { + app.stack.push(Qt.resolvedUrl('ChannelBackups.qml')) + } + icon.source: '../../icons/file.png' } - icon.source: '../../icons/file.png' } } diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 3170c3448..e745b81ba 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -64,8 +64,8 @@ ElDialog { color: Material.accentColor } - RowLayout { - Layout.fillWidth: true + ButtonContainer { + // Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter FlatButton { diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 7a23b7768..a36db7c6a 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -337,39 +337,46 @@ ElDialog { } } - FlatButton { + ButtonContainer { Layout.fillWidth: true - text: qsTr('Pay') - icon.source: '../../icons/confirmed.png' - enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay && !amountContainer.editmode - onClicked: { - if (invoice_key == '') // save invoice if not retrieved from key - invoice.save_invoice() - dialog.close() - doPay() // only signal here + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Pay') + icon.source: '../../icons/confirmed.png' + enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay && !amountContainer.editmode + onClicked: { + if (invoice_key == '') // save invoice if not retrieved from key + invoice.save_invoice() + dialog.close() + doPay() // only signal here + } } - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Delete') - icon.source: '../../icons/delete.png' - visible: invoice_key != '' - onClicked: { - invoice.wallet.delete_invoice(invoice_key) - dialog.close() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Delete') + icon.source: '../../icons/delete.png' + visible: invoice_key != '' + onClicked: { + invoice.wallet.delete_invoice(invoice_key) + dialog.close() + } } - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Save') - icon.source: '../../icons/save.png' - visible: invoice_key == '' - enabled: invoice.canSave - onClicked: { - app.stack.push(Qt.resolvedUrl('Invoices.qml')) - invoice.save_invoice() - dialog.close() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Save') + icon.source: '../../icons/save.png' + visible: invoice_key == '' + enabled: invoice.canSave + onClicked: { + app.stack.push(Qt.resolvedUrl('Invoices.qml')) + invoice.save_invoice() + dialog.close() + } } } diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index bcf78f52e..2ce5da262 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -161,26 +161,31 @@ Pane { } - FlatButton { + ButtonContainer { Layout.fillWidth: true - text: qsTr('Server Settings'); - icon.source: '../../icons/network.png' - onClicked: { - var dialog = serverConfig.createObject(root) - dialog.open() + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Server Settings'); + icon.source: '../../icons/network.png' + onClicked: { + var dialog = serverConfig.createObject(root) + dialog.open() + } } - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Proxy Settings'); - icon.source: '../../icons/status_connected_proxy.png' - onClicked: { - var dialog = proxyConfig.createObject(root) - dialog.open() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Proxy Settings'); + icon.source: '../../icons/status_connected_proxy.png' + onClicked: { + var dialog = proxyConfig.createObject(root) + dialog.open() + } } } - } function setFeeHistogram() { diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 2e89c7fd4..54bfa0477 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -241,7 +241,7 @@ ElDialog { color: Material.accentColor } - RowLayout { + ButtonContainer { id: buttons Layout.alignment: Qt.AlignHCenter FlatButton { diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index 656784cd3..56e4457a0 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -49,25 +49,32 @@ ElDialog { onFound: dialog.dispatch(scanData) } - FlatButton { + ButtonContainer { Layout.fillWidth: true - icon.source: '../../icons/pen.png' - text: qsTr('Manual input') - onClicked: { - var _mid = manualInputDialog.createObject(mainView) - _mid.accepted.connect(function() { - dialog.dispatch(_mid.recipient) - }) - _mid.open() + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + icon.source: '../../icons/pen.png' + text: qsTr('Manual input') + onClicked: { + var _mid = manualInputDialog.createObject(mainView) + _mid.accepted.connect(function() { + dialog.dispatch(_mid.recipient) + }) + _mid.open() + } } - } - FlatButton { - Layout.fillWidth: true - icon.source: '../../icons/paste.png' - text: qsTr('Paste from clipboard') - onClicked: dialog.dispatch(AppController.clipboardToText()) + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + icon.source: '../../icons/paste.png' + text: qsTr('Paste from clipboard') + onClicked: dialog.dispatch(AppController.clipboardToText()) + } } + } Component { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 06c6602b3..afb85f2ab 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -315,8 +315,10 @@ Pane { } - RowLayout { + ButtonContainer { + Layout.fillWidth: true visible: txdetails.canSign || txdetails.canBroadcast + FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -333,7 +335,9 @@ Pane { } } - RowLayout { + ButtonContainer { + Layout.fillWidth: true + FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -359,15 +363,16 @@ Pane { visible: txdetails.canRemove onClicked: txdetails.removeLocalTx() } - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Cancel Tx') - visible: txdetails.canCancel - onClicked: { - var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) - dialog.open() + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Cancel Tx') + visible: txdetails.canCancel + onClicked: { + var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) + dialog.open() + } } } diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index b8389d49d..ba908f70f 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -460,32 +460,40 @@ Pane { } } - FlatButton { + ButtonContainer { Layout.fillWidth: true - visible: Daemon.currentWallet.walletType == 'imported' - text: Daemon.currentWallet.isWatchOnly - ? qsTr('Import additional addresses') - : qsTr('Import additional keys') - onClicked: rootItem.importAddressesKeys() - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Change Password') - onClicked: rootItem.changePassword() - icon.source: '../../icons/lock.png' - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Delete Wallet') - onClicked: rootItem.deleteWallet() - icon.source: '../../icons/delete.png' - } - FlatButton { - Layout.fillWidth: true - text: qsTr('Enable Lightning') - onClicked: rootItem.enableLightning() - visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning - icon.source: '../../icons/lightning.png' + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + visible: Daemon.currentWallet.walletType == 'imported' + text: Daemon.currentWallet.isWatchOnly + ? qsTr('Import additional addresses') + : qsTr('Import additional keys') + onClicked: rootItem.importAddressesKeys() + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Change Password') + onClicked: rootItem.changePassword() + icon.source: '../../icons/lock.png' + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Delete Wallet') + onClicked: rootItem.deleteWallet() + icon.source: '../../icons/delete.png' + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Enable Lightning') + onClicked: rootItem.enableLightning() + visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning + icon.source: '../../icons/lightning.png' + } } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 08f8c079e..731d9ed07 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -154,8 +154,8 @@ Item { } } - RowLayout { - spacing: 0 + ButtonContainer { + Layout.fillWidth: true FlatButton { Layout.fillWidth: false @@ -169,17 +169,17 @@ Item { mainView.menu.y = mainView.height - mainView.menu.height } } - Rectangle { - Layout.fillWidth: false - Layout.preferredWidth: 2 - Layout.preferredHeight: parent.height * 2/3 - Layout.alignment: Qt.AlignVCenter - color: constants.darkerBackground - } - Item { - visible: !Daemon.currentWallet - Layout.fillWidth: true - } + // Rectangle { + // Layout.fillWidth: false + // Layout.preferredWidth: 2 + // Layout.preferredHeight: parent.height * 2/3 + // Layout.alignment: Qt.AlignVCenter + // color: constants.darkerBackground + // } + // Item { + // visible: !Daemon.currentWallet + // Layout.fillWidth: true + // } FlatButton { visible: Daemon.currentWallet Layout.fillWidth: true @@ -191,14 +191,14 @@ Item { dialog.open() } } - Rectangle { - visible: Daemon.currentWallet - Layout.fillWidth: false - Layout.preferredWidth: 2 - Layout.preferredHeight: parent.height * 2/3 - Layout.alignment: Qt.AlignVCenter - color: constants.darkerBackground - } + // Rectangle { + // visible: Daemon.currentWallet + // Layout.fillWidth: false + // Layout.preferredWidth: 2 + // Layout.preferredHeight: parent.height * 2/3 + // Layout.alignment: Qt.AlignVCenter + // color: constants.darkerBackground + // } FlatButton { visible: Daemon.currentWallet Layout.fillWidth: true From 8b933f3488dd85226bf80987ce505a1e6c0b1669 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 13:42:37 +0100 Subject: [PATCH 0058/1143] followup b2a02dd047f409757a25aba0f0b1483807f81a78 --- electrum/gui/qml/components/GenericShareDialog.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index be8c8553a..1cc8bbd6b 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -72,8 +72,7 @@ ElDialog { color: Material.accentColor } - RowLayout { - Layout.fillWidth: true + ButtonContainer { Layout.alignment: Qt.AlignHCenter FlatButton { From daa5c984fac18983d7c759b6c6a691773cb74e13 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 13:49:21 +0100 Subject: [PATCH 0059/1143] qml: remove leftovers WalletMainView --- .../gui/qml/components/WalletMainView.qml | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 731d9ed07..09fe37deb 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -169,17 +169,7 @@ Item { mainView.menu.y = mainView.height - mainView.menu.height } } - // Rectangle { - // Layout.fillWidth: false - // Layout.preferredWidth: 2 - // Layout.preferredHeight: parent.height * 2/3 - // Layout.alignment: Qt.AlignVCenter - // color: constants.darkerBackground - // } - // Item { - // visible: !Daemon.currentWallet - // Layout.fillWidth: true - // } + FlatButton { visible: Daemon.currentWallet Layout.fillWidth: true @@ -191,14 +181,7 @@ Item { dialog.open() } } - // Rectangle { - // visible: Daemon.currentWallet - // Layout.fillWidth: false - // Layout.preferredWidth: 2 - // Layout.preferredHeight: parent.height * 2/3 - // Layout.alignment: Qt.AlignVCenter - // color: constants.darkerBackground - // } + FlatButton { visible: Daemon.currentWallet Layout.fillWidth: true From 420546c201daaddd4b3f97c909ab0ceccec1be40 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 15:43:40 +0100 Subject: [PATCH 0060/1143] qml: assert role exists in qechannellistmodel.filterModel --- electrum/gui/qml/qechannellistmodel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 2f4c4c8f7..6b33db057 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -172,7 +172,8 @@ def remove_channel(self, cid): @pyqtSlot(str, 'QVariant', result=QEFilterProxyModel) def filterModel(self, role, match): self._filterModel = QEFilterProxyModel(self, self) - self._filterModel.setFilterRole(QEChannelListModel._ROLE_RMAP[role]) + assert role in self._ROLE_RMAP + self._filterModel.setFilterRole(self._ROLE_RMAP[role]) self._filterModel.setFilterValue(match) return self._filterModel From 53ca75d878fe1a08873ec1889fd7a82c62fc76c8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 2 Feb 2023 16:38:39 +0000 Subject: [PATCH 0061/1143] qt AddressDialog: separate parent and window --- electrum/gui/qt/address_dialog.py | 24 ++++++++++--------- electrum/gui/qt/custom_model.py | 3 ++- electrum/gui/qt/history_list.py | 38 ++++++++++++++++--------------- electrum/gui/qt/main_window.py | 4 ++-- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index a3688ace0..36cd426c8 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -38,8 +38,8 @@ class AddressHistoryModel(HistoryModel): - def __init__(self, parent: 'ElectrumWindow', address): - super().__init__(parent) + def __init__(self, window: 'ElectrumWindow', address): + super().__init__(window) self.address = address def get_domain(self): @@ -51,13 +51,15 @@ def should_include_lightning_payments(self) -> bool: class AddressDialog(WindowModalDialog): - def __init__(self, parent: 'ElectrumWindow', address: str): + def __init__(self, window: 'ElectrumWindow', address: str, *, parent=None): + if parent is None: + parent = window WindowModalDialog.__init__(self, parent, _("Address")) self.address = address - self.parent = parent - self.config = parent.config - self.wallet = parent.wallet - self.app = parent.app + self.window = window + self.config = window.config + self.wallet = window.wallet + self.app = window.app self.saved = True self.setMinimumWidth(700) @@ -101,18 +103,18 @@ def __init__(self, parent: 'ElectrumWindow', address: str): vbox.addWidget(der_path_e) vbox.addWidget(QLabel(_("History"))) - addr_hist_model = AddressHistoryModel(self.parent, self.address) - self.hw = HistoryList(self.parent, addr_hist_model) + addr_hist_model = AddressHistoryModel(self.window, self.address) + self.hw = HistoryList(self.window, addr_hist_model) addr_hist_model.set_view(self.hw) vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) - self.format_amount = self.parent.format_amount + self.format_amount = self.window.format_amount addr_hist_model.refresh('address dialog constructor') def show_qr(self): text = self.address try: - self.parent.show_qrcode(text, 'Address', parent=self) + self.window.show_qrcode(text, 'Address', parent=self) except Exception as e: self.show_message(repr(e)) diff --git a/electrum/gui/qt/custom_model.py b/electrum/gui/qt/custom_model.py index bca44be15..eaaa5e298 100644 --- a/electrum/gui/qt/custom_model.py +++ b/electrum/gui/qt/custom_model.py @@ -3,9 +3,10 @@ from PyQt5 import QtCore, QtWidgets + class CustomNode: - def __init__(self, model, data): + def __init__(self, model: 'CustomModel', data): self.model = model self._data = data self._children = [] diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 073e58c82..374bb84c4 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -116,12 +116,14 @@ def get_item_key(tx_item): class HistoryNode(CustomNode): + model: 'HistoryModel' + def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVariant: # note: this method is performance-critical. # it is called a lot, and so must run extremely fast. assert index.isValid() col = index.column() - window = self.model.parent + window = self.model.window tx_item = self.get_data() is_lightning = tx_item.get('lightning', False) timestamp = tx_item['timestamp'] @@ -228,10 +230,10 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria class HistoryModel(CustomModel, Logger): - def __init__(self, parent: 'ElectrumWindow'): - CustomModel.__init__(self, parent, len(HistoryColumns)) + def __init__(self, window: 'ElectrumWindow'): + CustomModel.__init__(self, window, len(HistoryColumns)) Logger.__init__(self) - self.parent = parent + self.window = window self.view = None # type: HistoryList self.transactions = OrderedDictWithIndex() self.tx_status_cache = {} # type: Dict[str, Tuple[int, str]] @@ -244,14 +246,14 @@ def set_view(self, history_list: 'HistoryList'): def update_label(self, index): tx_item = index.internalPointer().get_data() - tx_item['label'] = self.parent.wallet.get_label_for_txid(get_item_key(tx_item)) + tx_item['label'] = self.window.wallet.get_label_for_txid(get_item_key(tx_item)) topLeft = bottomRight = self.createIndex(index.row(), HistoryColumns.DESCRIPTION) self.dataChanged.emit(topLeft, bottomRight, [Qt.DisplayRole]) - self.parent.utxo_list.update() + self.window.utxo_list.update() def get_domain(self): """Overridden in address_dialog.py""" - return self.parent.wallet.get_addresses() + return self.window.wallet.get_addresses() def should_include_lightning_payments(self) -> bool: """Overridden in address_dialog.py""" @@ -260,7 +262,7 @@ def should_include_lightning_payments(self) -> bool: @profiler def refresh(self, reason: str): self.logger.info(f"refreshing... reason: {reason}") - assert self.parent.gui_thread == threading.current_thread(), 'must be called from GUI thread' + assert self.window.gui_thread == threading.current_thread(), 'must be called from GUI thread' assert self.view, 'view not set' if self.view.maybe_defer_update(): return @@ -268,12 +270,12 @@ def refresh(self, reason: str): selected_row = None if selected: selected_row = selected.row() - fx = self.parent.fx + fx = self.window.fx if fx: fx.history_used_spot = False - wallet = self.parent.wallet + wallet = self.window.wallet self.set_visibility_of_columns() transactions = wallet.get_full_history( - self.parent.fx, + self.window.fx, onchain_domain=self.get_domain(), include_lightning=self.should_include_lightning_payments()) if transactions == self.transactions: @@ -347,7 +349,7 @@ def refresh(self, reason: str): for txid, tx_item in self.transactions.items(): if not tx_item.get('lightning', False): tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) - self.tx_status_cache[txid] = self.parent.wallet.get_tx_status(txid, tx_mined_info) + self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info) def set_visibility_of_columns(self): def set_visible(col: int, b: bool): @@ -355,8 +357,8 @@ def set_visible(col: int, b: bool): # txid set_visible(HistoryColumns.TXID, False) # fiat - history = self.parent.fx.show_history() - cap_gains = self.parent.fx.get_history_capital_gains_config() + history = self.window.fx.show_history() + cap_gains = self.window.fx.get_history_capital_gains_config() set_visible(HistoryColumns.FIAT_VALUE, history) set_visible(HistoryColumns.FIAT_ACQ_PRICE, history and cap_gains) set_visible(HistoryColumns.FIAT_CAP_GAINS, history and cap_gains) @@ -366,8 +368,8 @@ def update_fiat(self, idx): txid = tx_item['txid'] fee = tx_item.get('fee') value = tx_item['value'].value - fiat_fields = self.parent.wallet.get_tx_item_fiat( - tx_hash=txid, amount_sat=value, fx=self.parent.fx, tx_fee=fee.value if fee else None) + fiat_fields = self.window.wallet.get_tx_item_fiat( + tx_hash=txid, amount_sat=value, fx=self.window.fx, tx_fee=fee.value if fee else None) tx_item.update(fiat_fields) self.dataChanged.emit(idx, idx, [Qt.DisplayRole, Qt.ForegroundRole]) @@ -377,7 +379,7 @@ def update_tx_mined_status(self, tx_hash: str, tx_mined_info: TxMinedInfo): tx_item = self.transactions[tx_hash] except KeyError: return - self.tx_status_cache[tx_hash] = self.parent.wallet.get_tx_status(tx_hash, tx_mined_info) + self.tx_status_cache[tx_hash] = self.window.wallet.get_tx_status(tx_hash, tx_mined_info) tx_item.update({ 'confirmations': tx_mined_info.conf, 'timestamp': tx_mined_info.timestamp, @@ -402,7 +404,7 @@ def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDat assert orientation == Qt.Horizontal if role != Qt.DisplayRole: return None - fx = self.parent.fx + fx = self.window.fx fiat_title = 'n/a fiat value' fiat_acq_title = 'n/a fiat acquisition price' fiat_cg_title = 'n/a fiat capital gains' diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index fa81f236f..1dd40d5cd 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1060,9 +1060,9 @@ def create_history_tab(self): l.show_toolbar(toolbar_shown) return tab - def show_address(self, addr): + def show_address(self, addr: str, *, parent: QWidget = None): from . import address_dialog - d = address_dialog.AddressDialog(self, addr) + d = address_dialog.AddressDialog(self, addr, parent=parent) d.exec_() def show_channel_details(self, chan): From 7d42676785e0d807c43f2b156a8371b5668abd26 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 2 Feb 2023 18:20:02 +0000 Subject: [PATCH 0062/1143] qt tx dialog: make scid and addr texts clickable in IO fields based on: https://github.com/Electron-Cash/Electron-Cash/commit/7eea0b6dae3d8ebc3823ceb7abf6093119b915c0 https://github.com/Electron-Cash/Electron-Cash/commit/52d845017c99d5c9a02df6b9af7825d2111b8def --- electrum/address_synchronizer.py | 2 +- electrum/gui/qt/main_window.py | 34 ++++++--- electrum/gui/qt/transaction_dialog.py | 103 +++++++++++++++++++------- 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index efbe3552c..62339bbdb 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -242,7 +242,7 @@ def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=Fa conflicting_txns -= {tx_hash} return conflicting_txns - def get_transaction(self, txid: str) -> Transaction: + def get_transaction(self, txid: str) -> Optional[Transaction]: tx = self.db.get_transaction(txid) if tx: # add verified tx info diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 1dd40d5cd..8c00648a9 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2190,30 +2190,44 @@ def do_process_from_file(self): if tx: self.show_transaction(tx) - def do_process_from_txid(self): + def do_process_from_txid(self, *, parent: QWidget = None, txid: str = None): + if parent is None: + parent = self from electrum import transaction - txid, ok = QInputDialog.getText(self, _('Lookup transaction'), _('Transaction ID') + ':') - if ok and txid: - txid = str(txid).strip() - raw_tx = self._fetch_tx_from_network(txid) + if txid is None: + txid, ok = QInputDialog.getText(parent, _('Lookup transaction'), _('Transaction ID') + ':') + if not ok: + txid = None + if not txid: + return + txid = str(txid).strip() + tx = self.wallet.adb.get_transaction(txid) + if tx is None: + raw_tx = self._fetch_tx_from_network(txid, parent=parent) if not raw_tx: return tx = transaction.Transaction(raw_tx) - self.show_transaction(tx) + self.show_transaction(tx) - def _fetch_tx_from_network(self, txid: str) -> Optional[str]: + def _fetch_tx_from_network(self, txid: str, *, parent: QWidget = None) -> Optional[str]: if not self.network: - self.show_message(_("You are offline.")) + self.show_message(_("You are offline."), parent=parent) return try: raw_tx = self.network.run_from_another_thread( self.network.get_transaction(txid, timeout=10)) except UntrustedServerReturnedError as e: self.logger.info(f"Error getting transaction from network: {repr(e)}") - self.show_message(_("Error getting transaction from network") + ":\n" + e.get_message_for_gui()) + self.show_message( + _("Error getting transaction from network") + ":\n" + e.get_message_for_gui(), + parent=parent, + ) return except Exception as e: - self.show_message(_("Error getting transaction from network") + ":\n" + repr(e)) + self.show_message( + _("Error getting transaction from network") + ":\n" + repr(e), + parent=parent, + ) return return raw_tx diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index c7381757b..18907b632 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -28,19 +28,20 @@ import datetime import traceback import time -from typing import TYPE_CHECKING, Callable, Optional, List, Union +from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple from functools import partial from decimal import Decimal -from PyQt5.QtCore import QSize, Qt +from PyQt5.QtCore import QSize, Qt, QUrl from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QGridLayout, - QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox) + QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox, QTextBrowser) import qrcode from qrcode import exceptions from electrum.simple_config import SimpleConfig from electrum.util import quantize_feerate +from electrum import bitcoin from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX from electrum.i18n import _ from electrum.plugin import run_hook @@ -74,7 +75,7 @@ class TxFiatLabel(QLabel): def setAmount(self, fiat_fee): self.setText(('≈ %s' % fiat_fee) if fiat_fee else '') -class QTextEditWithDefaultSize(QTextEdit): +class QTextBrowserWithDefaultSize(QTextBrowser): def sizeHint(self): return QSize(0, 100) @@ -86,7 +87,11 @@ def __init__(self, main_window, wallet): self.wallet = wallet self.main_window = main_window self.inputs_header = QLabel() - self.inputs_textedit = QTextEditWithDefaultSize() + self.inputs_textedit = QTextBrowserWithDefaultSize() + self.inputs_textedit.setOpenLinks(False) # disable automatic link opening + self.inputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler + self.inputs_textedit.setTextInteractionFlags( + self.inputs_textedit.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) self.txo_color_recv = TxOutputColoring( legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address")) self.txo_color_change = TxOutputColoring( @@ -94,7 +99,11 @@ def __init__(self, main_window, wallet): self.txo_color_2fa = TxOutputColoring( legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) self.outputs_header = QLabel() - self.outputs_textedit = QTextEditWithDefaultSize() + self.outputs_textedit = QTextBrowserWithDefaultSize() + self.outputs_textedit.setOpenLinks(False) # disable automatic link opening + self.outputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler + self.outputs_textedit.setTextInteractionFlags( + self.outputs_textedit.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) self.inputs_textedit.setMinimumWidth(950) self.outputs_textedit.setMinimumWidth(950) @@ -125,43 +134,59 @@ def update(self, tx): self.inputs_header.setText(inputs_header_text) - ext = QTextCharFormat() + ext = QTextCharFormat() # "external" + lnk = QTextCharFormat() + lnk.setToolTip(_('Click to open')) + lnk.setAnchor(True) + lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) tf_used_recv, tf_used_change, tf_used_2fa = False, False, False - def text_format(addr): + def addr_text_format(addr): nonlocal tf_used_recv, tf_used_change, tf_used_2fa if self.wallet.is_mine(addr): if self.wallet.is_change(addr): tf_used_change = True - return self.txo_color_change.text_char_format + fmt = QTextCharFormat(self.txo_color_change.text_char_format) else: tf_used_recv = True - return self.txo_color_recv.text_char_format + fmt = QTextCharFormat(self.txo_color_recv.text_char_format) + fmt.setAnchorHref(addr) + fmt.setToolTip(_('Click to open')) + fmt.setAnchor(True) + fmt.setUnderlineStyle(QTextCharFormat.SingleUnderline) + return fmt elif self.wallet.is_billing_address(addr): tf_used_2fa = True return self.txo_color_2fa.text_char_format return ext - def insert_tx_io(cursor, is_coinbase, short_id, address, value): - if is_coinbase: - cursor.insertText('coinbase') - else: - address_str = address or '
' - value_str = self.main_window.format_amount(value, whitespaces=True) - cursor.insertText("%-15s\t"%str(short_id), ext) - cursor.insertText("%-62s"%address_str, text_format(address)) - cursor.insertText('\t', ext) - cursor.insertText(value_str, ext) - cursor.insertBlock() - i_text = self.inputs_textedit i_text.clear() i_text.setFont(QFont(MONOSPACE_FONT)) i_text.setReadOnly(True) cursor = i_text.textCursor() - for txin in self.tx.inputs(): + for txin_idx, txin in enumerate(self.tx.inputs()): addr = self.wallet.adb.get_txin_address(txin) txin_value = self.wallet.adb.get_txin_value(txin) - insert_tx_io(cursor, txin.is_coinbase_input(), txin.short_id, addr, txin_value) + if txin.is_coinbase_input(): + cursor.insertText('coinbase') + else: + # short_id + a_name = f"tx input {txin_idx}" + lnk2 = QTextCharFormat(lnk) + lnk2.setAnchorHref(txin.prevout.txid.hex()) + lnk2.setAnchorNames([a_name]) + cursor.insertText(str(txin.short_id), lnk2) + cursor.insertText(" " * max(0, 15 - len(str(txin.short_id))), ext) # padding + cursor.insertText('\t', ext) + # addr + address_str = addr or '
' + cursor.insertText(address_str, addr_text_format(addr)) + cursor.insertText(" " * max(0, 62 - len(address_str)), ext) # padding + cursor.insertText('\t', ext) + # value + value_str = self.main_window.format_amount(txin_value, whitespaces=True) + cursor.insertText(value_str, ext) + cursor.insertBlock() self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) o_text = self.outputs_textedit @@ -176,14 +201,39 @@ def insert_tx_io(cursor, is_coinbase, short_id, address, value): short_id = ShortID.from_components(tx_height, tx_pos, index) else: short_id = TxOutpoint(tx_hash, index).short_name() - addr, value = o.get_ui_address_str(), o.value - insert_tx_io(cursor, False, short_id, addr, value) + # short id + cursor.insertText(str(short_id), ext) + cursor.insertText(" " * max(0, 15 - len(str(short_id))), ext) # padding + cursor.insertText('\t', ext) + # addr + address_str = addr or '
' + cursor.insertText(address_str, addr_text_format(addr)) + cursor.insertText(" " * max(0, 62 - len(address_str)), ext) # padding + cursor.insertText('\t', ext) + # value + value_str = self.main_window.format_amount(value, whitespaces=True) + cursor.insertText(value_str, ext) + cursor.insertBlock() self.txo_color_recv.legend_label.setVisible(tf_used_recv) self.txo_color_change.legend_label.setVisible(tf_used_change) self.txo_color_2fa.legend_label.setVisible(tf_used_2fa) + def _open_internal_link(self, target): + """Accepts either a str txid, str address, or a QUrl which should be + of the bare form "txid" and/or "address" -- used by the clickable + links in the inputs/outputs QTextBrowsers""" + if isinstance(target, QUrl): + target = target.toString(QUrl.None_) + assert target + if bitcoin.is_address(target): + # target was an address, open address dialog + self.main_window.show_address(target, parent=self) + else: + # target was a txid, open new tx dialog + self.main_window.do_process_from_txid(txid=target, parent=self) + _logger = get_logger(__name__) dialogs = [] # Otherwise python randomly garbage collects the dialogs... @@ -652,7 +702,6 @@ def update(self): run_hook('transaction_dialog_update', self) - def add_tx_stats(self, vbox): hbox_stats = QHBoxLayout() From c2c02391a23c7bc9d062e475c585f20a462b2409 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 14:42:31 +0000 Subject: [PATCH 0063/1143] qt tx dialog: add context menus to IO fields based on: https://github.com/Electron-Cash/Electron-Cash/commit/46df4190c8a32fbada777d82e8663b01d062b739 --- electrum/gui/qt/main_window.py | 2 +- electrum/gui/qt/transaction_dialog.py | 165 +++++++++++++++++++++----- 2 files changed, 136 insertions(+), 31 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8c00648a9..418776bd2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1085,7 +1085,7 @@ def create_receive_tab(self): def do_copy(self, content: str, *, title: str = None) -> None: self.app.clipboard().setText(content) if title is None: - tooltip_text = _("Text copied to clipboard").format(title) + tooltip_text = _("Text copied to clipboard") else: tooltip_text = _("{} copied to clipboard").format(title) QToolTip.showText(QCursor.pos(), tooltip_text, self) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 18907b632..2bdd3d307 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -32,10 +32,11 @@ from functools import partial from decimal import Decimal -from PyQt5.QtCore import QSize, Qt, QUrl -from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap +from PyQt5.QtCore import QSize, Qt, QUrl, QPoint +from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap, QCursor from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QGridLayout, - QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox, QTextBrowser) + QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox, QTextBrowser, QToolTip, + QApplication) import qrcode from qrcode import exceptions @@ -65,6 +66,11 @@ if TYPE_CHECKING: from .main_window import ElectrumWindow + from electrum.wallet import Abstract_Wallet + + +_logger = get_logger(__name__) +dialogs = [] # Otherwise python randomly garbage collects the dialogs... class TxSizeLabel(QLabel): @@ -81,17 +87,20 @@ def sizeHint(self): class TxInOutWidget(QWidget): - def __init__(self, main_window, wallet): + def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): QWidget.__init__(self) self.wallet = wallet self.main_window = main_window + self.tx = None # type: Optional[Transaction] self.inputs_header = QLabel() self.inputs_textedit = QTextBrowserWithDefaultSize() self.inputs_textedit.setOpenLinks(False) # disable automatic link opening self.inputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler self.inputs_textedit.setTextInteractionFlags( self.inputs_textedit.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) + self.inputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu) + self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs) self.txo_color_recv = TxOutputColoring( legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address")) self.txo_color_change = TxOutputColoring( @@ -104,6 +113,8 @@ def __init__(self, main_window, wallet): self.outputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler self.outputs_textedit.setTextInteractionFlags( self.outputs_textedit.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) + self.outputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu) + self.outputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_outputs) self.inputs_textedit.setMinimumWidth(950) self.outputs_textedit.setMinimumWidth(950) @@ -136,7 +147,7 @@ def update(self, tx): ext = QTextCharFormat() # "external" lnk = QTextCharFormat() - lnk.setToolTip(_('Click to open')) + lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setAnchor(True) lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) tf_used_recv, tf_used_change, tf_used_2fa = False, False, False @@ -150,7 +161,7 @@ def addr_text_format(addr): tf_used_recv = True fmt = QTextCharFormat(self.txo_color_recv.text_char_format) fmt.setAnchorHref(addr) - fmt.setToolTip(_('Click to open')) + fmt.setToolTip(_('Click to open, right-click for menu')) fmt.setAnchor(True) fmt.setUnderlineStyle(QTextCharFormat.SingleUnderline) return fmt @@ -167,25 +178,30 @@ def addr_text_format(addr): for txin_idx, txin in enumerate(self.tx.inputs()): addr = self.wallet.adb.get_txin_address(txin) txin_value = self.wallet.adb.get_txin_value(txin) + # prepare text char formats + a_name = f"input {txin_idx}" + tcf_ext = QTextCharFormat(ext) + tcf_shortid = QTextCharFormat(lnk) + tcf_shortid.setAnchorHref(txin.prevout.txid.hex()) + tcf_addr = addr_text_format(addr) + for tcf in (tcf_ext, tcf_shortid, tcf_addr): # used by context menu creation + tcf.setAnchorNames([a_name]) + # insert text if txin.is_coinbase_input(): - cursor.insertText('coinbase') + cursor.insertText('coinbase', tcf_ext) else: # short_id - a_name = f"tx input {txin_idx}" - lnk2 = QTextCharFormat(lnk) - lnk2.setAnchorHref(txin.prevout.txid.hex()) - lnk2.setAnchorNames([a_name]) - cursor.insertText(str(txin.short_id), lnk2) - cursor.insertText(" " * max(0, 15 - len(str(txin.short_id))), ext) # padding - cursor.insertText('\t', ext) + cursor.insertText(str(txin.short_id), tcf_shortid) + cursor.insertText(" " * max(0, 15 - len(str(txin.short_id))), tcf_ext) # padding + cursor.insertText('\t', tcf_ext) # addr address_str = addr or '
' - cursor.insertText(address_str, addr_text_format(addr)) - cursor.insertText(" " * max(0, 62 - len(address_str)), ext) # padding - cursor.insertText('\t', ext) + cursor.insertText(address_str, tcf_addr) + cursor.insertText(" " * max(0, 62 - len(address_str)), tcf_ext) # padding + cursor.insertText('\t', tcf_ext) # value value_str = self.main_window.format_amount(txin_value, whitespaces=True) - cursor.insertText(value_str, ext) + cursor.insertText(value_str, tcf_ext) cursor.insertBlock() self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) @@ -196,24 +212,30 @@ def addr_text_format(addr): tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) tx_hash = bytes.fromhex(self.tx.txid()) cursor = o_text.textCursor() - for index, o in enumerate(self.tx.outputs()): + for txout_idx, o in enumerate(self.tx.outputs()): if tx_pos is not None and tx_pos >= 0: - short_id = ShortID.from_components(tx_height, tx_pos, index) + short_id = ShortID.from_components(tx_height, tx_pos, txout_idx) else: - short_id = TxOutpoint(tx_hash, index).short_name() + short_id = TxOutpoint(tx_hash, txout_idx).short_name() addr, value = o.get_ui_address_str(), o.value + # prepare text char formats + a_name = f"output {txout_idx}" + tcf_ext = QTextCharFormat(ext) + tcf_addr = addr_text_format(addr) + for tcf in (tcf_ext, tcf_addr): # used by context menu creation + tcf.setAnchorNames([a_name]) # short id - cursor.insertText(str(short_id), ext) - cursor.insertText(" " * max(0, 15 - len(str(short_id))), ext) # padding - cursor.insertText('\t', ext) + cursor.insertText(str(short_id), tcf_ext) + cursor.insertText(" " * max(0, 15 - len(str(short_id))), tcf_ext) # padding + cursor.insertText('\t', tcf_ext) # addr address_str = addr or '
' - cursor.insertText(address_str, addr_text_format(addr)) - cursor.insertText(" " * max(0, 62 - len(address_str)), ext) # padding - cursor.insertText('\t', ext) + cursor.insertText(address_str, tcf_addr) + cursor.insertText(" " * max(0, 62 - len(address_str)), tcf_ext) # padding + cursor.insertText('\t', tcf_ext) # value value_str = self.main_window.format_amount(value, whitespaces=True) - cursor.insertText(value_str, ext) + cursor.insertText(value_str, tcf_ext) cursor.insertBlock() self.txo_color_recv.legend_label.setVisible(tf_used_recv) @@ -234,9 +256,92 @@ def _open_internal_link(self, target): # target was a txid, open new tx dialog self.main_window.do_process_from_txid(txid=target, parent=self) + def on_context_menu_for_inputs(self, pos: QPoint): + i_text = self.inputs_textedit + global_pos = i_text.viewport().mapToGlobal(pos) + + cursor = i_text.cursorForPosition(pos) + charFormat = cursor.charFormat() + name = charFormat.anchorNames() and charFormat.anchorNames()[0] + if not name: + menu = i_text.createStandardContextMenu() + menu.exec_(global_pos) + return -_logger = get_logger(__name__) -dialogs = [] # Otherwise python randomly garbage collects the dialogs... + menu = QMenu() + show_list = [] + copy_list = [] + # figure out which input they right-clicked on. input lines have an anchor named "input N" + txin_idx = int(name.split()[1]) # split "input N", translate N -> int + txin = self.tx.inputs()[txin_idx] + + menu.addAction(f"Tx Input #{txin_idx}").setDisabled(True) + menu.addSeparator() + if txin.is_coinbase_input(): + menu.addAction(_("Coinbase Input")).setDisabled(True) + else: + show_list += [(_("Show Prev Tx"), lambda: self._open_internal_link(txin.prevout.txid.hex()))] + copy_list += [(_("Copy Prevout"), lambda: self.main_window.do_copy(txin.prevout.to_str()))] + addr = self.wallet.adb.get_txin_address(txin) + if addr: + if self.wallet.is_mine(addr): + show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))] + copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))] + txin_value = self.wallet.adb.get_txin_value(txin) + if txin_value: + value_str = self.main_window.format_amount(txin_value) + copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))] + + for item in show_list: + menu.addAction(*item) + if show_list and copy_list: + menu.addSeparator() + for item in copy_list: + menu.addAction(*item) + + menu.addSeparator() + std_menu = i_text.createStandardContextMenu() + menu.addActions(std_menu.actions()) + menu.exec_(global_pos) + + def on_context_menu_for_outputs(self, pos: QPoint): + o_text = self.outputs_textedit + global_pos = o_text.viewport().mapToGlobal(pos) + + cursor = o_text.cursorForPosition(pos) + charFormat = cursor.charFormat() + name = charFormat.anchorNames() and charFormat.anchorNames()[0] + if not name: + menu = o_text.createStandardContextMenu() + menu.exec_(global_pos) + return + + menu = QMenu() + show_list = [] + copy_list = [] + # figure out which output they right-clicked on. output lines have an anchor named "output N" + txout_idx = int(name.split()[1]) # split "output N", translate N -> int + menu.addAction(f"Tx Output #{txout_idx}").setDisabled(True) + menu.addSeparator() + if addr := self.tx.outputs()[txout_idx].address: + if self.wallet.is_mine(addr): + show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))] + copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))] + txout_value = self.tx.outputs()[txout_idx].value + value_str = self.main_window.format_amount(txout_value) + copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))] + + for item in show_list: + menu.addAction(*item) + if show_list and copy_list: + menu.addSeparator() + for item in copy_list: + menu.addAction(*item) + + menu.addSeparator() + std_menu = o_text.createStandardContextMenu() + menu.addActions(std_menu.actions()) + menu.exec_(global_pos) def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False): From 04a5aaeddc07eb0c51a39f84e1f6f3d00da5bda2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 2 Feb 2023 19:16:38 +0000 Subject: [PATCH 0064/1143] transaction: (trivial) add comment about is_cb_input vs is_cb_output follow-up d6febb5c1243f3f80d5a79af9aa39312c8166c91, 96ac199f5cb27b6f3158c922afbb1322c5660121 --- electrum/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 763d003d0..bcf44e8be 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -254,7 +254,7 @@ def is_coinbase_input(self) -> bool: def is_coinbase_output(self) -> bool: """Whether the coin being spent is an output of a coinbase tx. - This matters for coin maturity. + This matters for coin maturity (and pretty much only for that!). """ return self._is_coinbase_output From 6ae105ca99c0fe865c8d2c894e651bffac96e528 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 15:17:42 +0000 Subject: [PATCH 0065/1143] qt tx dialog: fix size of IO textedits when resizing textedit.setMinimumWidth(950) (from 5af399d19639b0c141398db964270c4974f124acdoes) does not play well with the tx_dlg.setMinimumWidth(640) when resizing. Make 950 only affect the default sizing of the textedit. --- electrum/gui/qt/transaction_dialog.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 2bdd3d307..189a7fb73 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -82,8 +82,12 @@ def setAmount(self, fiat_fee): self.setText(('≈ %s' % fiat_fee) if fiat_fee else '') class QTextBrowserWithDefaultSize(QTextBrowser): + def __init__(self, width: int = 0, height: int = 0): + self._width = width + self._height = height + QTextBrowser.__init__(self) def sizeHint(self): - return QSize(0, 100) + return QSize(self._width, self._height) class TxInOutWidget(QWidget): @@ -94,7 +98,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.main_window = main_window self.tx = None # type: Optional[Transaction] self.inputs_header = QLabel() - self.inputs_textedit = QTextBrowserWithDefaultSize() + self.inputs_textedit = QTextBrowserWithDefaultSize(950, 100) self.inputs_textedit.setOpenLinks(False) # disable automatic link opening self.inputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler self.inputs_textedit.setTextInteractionFlags( @@ -108,7 +112,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.txo_color_2fa = TxOutputColoring( legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) self.outputs_header = QLabel() - self.outputs_textedit = QTextBrowserWithDefaultSize() + self.outputs_textedit = QTextBrowserWithDefaultSize(950, 100) self.outputs_textedit.setOpenLinks(False) # disable automatic link opening self.outputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler self.outputs_textedit.setTextInteractionFlags( @@ -116,9 +120,6 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.outputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu) self.outputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_outputs) - self.inputs_textedit.setMinimumWidth(950) - self.outputs_textedit.setMinimumWidth(950) - outheader_hbox = QHBoxLayout() outheader_hbox.setContentsMargins(0, 0, 0, 0) outheader_hbox.addWidget(self.outputs_header) From dc1441d129f249cc0339f9af9a35ae6083b028ea Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 16:26:25 +0100 Subject: [PATCH 0066/1143] qml: make sure to keep ref to instance --- .../gui/qml/components/ChannelBackups.qml | 2 +- electrum/gui/qml/components/Channels.qml | 2 +- electrum/gui/qml/qechannellistmodel.py | 19 ++++++++++++++----- electrum/gui/qml/qemodelfilter.py | 3 +-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qml/components/ChannelBackups.qml b/electrum/gui/qml/components/ChannelBackups.qml index 491976d9f..496509bee 100644 --- a/electrum/gui/qml/components/ChannelBackups.qml +++ b/electrum/gui/qml/components/ChannelBackups.qml @@ -54,7 +54,7 @@ Pane { Layout.preferredWidth: parent.width Layout.fillHeight: true clip: true - model: Daemon.currentWallet.channelModel.filterModel('is_backup', true) + model: Daemon.currentWallet.channelModel.filterModelBackups() delegate: ChannelDelegate { onClicked: { diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 3e04d9cc4..ea299d329 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -78,7 +78,7 @@ Pane { Layout.preferredWidth: parent.width Layout.fillHeight: true clip: true - model: Daemon.currentWallet.channelModel.filterModel('is_backup', false) + model: Daemon.currentWallet.channelModel.filterModelNoBackups() delegate: ChannelDelegate { onClicked: { diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 6b33db057..0f03937de 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -169,11 +169,20 @@ def remove_channel(self, cid): return i = i + 1 - @pyqtSlot(str, 'QVariant', result=QEFilterProxyModel) def filterModel(self, role, match): - self._filterModel = QEFilterProxyModel(self, self) + _filterModel = QEFilterProxyModel(self, self) assert role in self._ROLE_RMAP - self._filterModel.setFilterRole(self._ROLE_RMAP[role]) - self._filterModel.setFilterValue(match) - return self._filterModel + _filterModel.setFilterRole(self._ROLE_RMAP[role]) + _filterModel.setFilterValue(match) + return _filterModel + + @pyqtSlot(result=QEFilterProxyModel) + def filterModelBackups(self): + self._fm_backups = self.filterModel('is_backup', True) + return self._fm_backups + + @pyqtSlot(result=QEFilterProxyModel) + def filterModelNoBackups(self): + self._fm_nobackups = self.filterModel('is_backup', False) + return self._fm_nobackups diff --git a/electrum/gui/qml/qemodelfilter.py b/electrum/gui/qml/qemodelfilter.py index f024bea4a..1c098009d 100644 --- a/electrum/gui/qml/qemodelfilter.py +++ b/electrum/gui/qml/qemodelfilter.py @@ -5,10 +5,9 @@ class QEFilterProxyModel(QSortFilterProxyModel): _logger = get_logger(__name__) - _filter_value = None - def __init__(self, parent_model, parent=None): super().__init__(parent) + self._filter_value = None self.setSourceModel(parent_model) countChanged = pyqtSignal() From f05ab403fb445de2613d1a875b951b6770ebeee7 Mon Sep 17 00:00:00 2001 From: Roman Zeyde Date: Fri, 3 Feb 2023 17:33:47 +0200 Subject: [PATCH 0067/1143] Support latest Debian libsecp256k1 package https://packages.debian.org/source/testing/libsecp256k1 --- electrum/ecc_fast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index d95234aa0..3059eda54 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -47,7 +47,7 @@ def load_library(): library_paths = ('libsecp256k1.so',) else: # desktop Linux and similar library_paths = (os.path.join(os.path.dirname(__file__), 'libsecp256k1.so.0'), - 'libsecp256k1.so.0') + 'libsecp256k1.so.0', 'libsecp256k1.so.1') exceptions = [] secp256k1 = None From 3dadfadcabdc2f9acb0d57c93e6a46739720b0c8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 15:50:16 +0000 Subject: [PATCH 0068/1143] qt tx dialog: readd "insert_tx_io" code-factorisation recently rm-ed follow-up 7d42676785e0d807c43f2b156a8371b5668abd26 --- electrum/gui/qt/transaction_dialog.py | 85 ++++++++++++++------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 189a7fb73..81b0dd8ce 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -152,7 +152,7 @@ def update(self, tx): lnk.setAnchor(True) lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) tf_used_recv, tf_used_change, tf_used_2fa = False, False, False - def addr_text_format(addr): + def addr_text_format(addr: str) -> QTextCharFormat: nonlocal tf_used_recv, tf_used_change, tf_used_2fa if self.wallet.is_mine(addr): if self.wallet.is_change(addr): @@ -171,29 +171,29 @@ def addr_text_format(addr): return self.txo_color_2fa.text_char_format return ext - i_text = self.inputs_textedit - i_text.clear() - i_text.setFont(QFont(MONOSPACE_FONT)) - i_text.setReadOnly(True) - cursor = i_text.textCursor() - for txin_idx, txin in enumerate(self.tx.inputs()): - addr = self.wallet.adb.get_txin_address(txin) - txin_value = self.wallet.adb.get_txin_value(txin) - # prepare text char formats - a_name = f"input {txin_idx}" + def insert_tx_io( + *, + cursor: QCursor, + txio_idx: int, + is_coinbase: bool, + tcf_shortid: QTextCharFormat = None, + short_id: str, + addr: Optional[str], + value: Optional[int], + ): tcf_ext = QTextCharFormat(ext) - tcf_shortid = QTextCharFormat(lnk) - tcf_shortid.setAnchorHref(txin.prevout.txid.hex()) tcf_addr = addr_text_format(addr) + if tcf_shortid is None: + tcf_shortid = tcf_ext + a_name = f"txio_idx {txio_idx}" for tcf in (tcf_ext, tcf_shortid, tcf_addr): # used by context menu creation tcf.setAnchorNames([a_name]) - # insert text - if txin.is_coinbase_input(): + if is_coinbase: cursor.insertText('coinbase', tcf_ext) else: # short_id - cursor.insertText(str(txin.short_id), tcf_shortid) - cursor.insertText(" " * max(0, 15 - len(str(txin.short_id))), tcf_ext) # padding + cursor.insertText(short_id, tcf_shortid) + cursor.insertText(" " * max(0, 15 - len(short_id)), tcf_ext) # padding cursor.insertText('\t', tcf_ext) # addr address_str = addr or '
' @@ -201,10 +201,26 @@ def addr_text_format(addr): cursor.insertText(" " * max(0, 62 - len(address_str)), tcf_ext) # padding cursor.insertText('\t', tcf_ext) # value - value_str = self.main_window.format_amount(txin_value, whitespaces=True) + value_str = self.main_window.format_amount(value, whitespaces=True) cursor.insertText(value_str, tcf_ext) cursor.insertBlock() + i_text = self.inputs_textedit + i_text.clear() + i_text.setFont(QFont(MONOSPACE_FONT)) + i_text.setReadOnly(True) + cursor = i_text.textCursor() + for txin_idx, txin in enumerate(self.tx.inputs()): + addr = self.wallet.adb.get_txin_address(txin) + txin_value = self.wallet.adb.get_txin_value(txin) + tcf_shortid = QTextCharFormat(lnk) + tcf_shortid.setAnchorHref(txin.prevout.txid.hex()) + insert_tx_io( + cursor=cursor, is_coinbase=txin.is_coinbase_input(), txio_idx=txin_idx, + tcf_shortid=tcf_shortid, + short_id=str(txin.short_id), addr=addr, value=txin_value, + ) + self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) o_text = self.outputs_textedit o_text.clear() @@ -218,26 +234,11 @@ def addr_text_format(addr): short_id = ShortID.from_components(tx_height, tx_pos, txout_idx) else: short_id = TxOutpoint(tx_hash, txout_idx).short_name() - addr, value = o.get_ui_address_str(), o.value - # prepare text char formats - a_name = f"output {txout_idx}" - tcf_ext = QTextCharFormat(ext) - tcf_addr = addr_text_format(addr) - for tcf in (tcf_ext, tcf_addr): # used by context menu creation - tcf.setAnchorNames([a_name]) - # short id - cursor.insertText(str(short_id), tcf_ext) - cursor.insertText(" " * max(0, 15 - len(str(short_id))), tcf_ext) # padding - cursor.insertText('\t', tcf_ext) - # addr - address_str = addr or '
' - cursor.insertText(address_str, tcf_addr) - cursor.insertText(" " * max(0, 62 - len(address_str)), tcf_ext) # padding - cursor.insertText('\t', tcf_ext) - # value - value_str = self.main_window.format_amount(value, whitespaces=True) - cursor.insertText(value_str, tcf_ext) - cursor.insertBlock() + addr = o.get_ui_address_str() + insert_tx_io( + cursor=cursor, is_coinbase=False, txio_idx=txout_idx, + short_id=str(short_id), addr=addr, value=o.value, + ) self.txo_color_recv.legend_label.setVisible(tf_used_recv) self.txo_color_change.legend_label.setVisible(tf_used_change) @@ -272,8 +273,8 @@ def on_context_menu_for_inputs(self, pos: QPoint): menu = QMenu() show_list = [] copy_list = [] - # figure out which input they right-clicked on. input lines have an anchor named "input N" - txin_idx = int(name.split()[1]) # split "input N", translate N -> int + # figure out which input they right-clicked on. input lines have an anchor named "txio_idx N" + txin_idx = int(name.split()[1]) # split "txio_idx N", translate N -> int txin = self.tx.inputs()[txin_idx] menu.addAction(f"Tx Input #{txin_idx}").setDisabled(True) @@ -320,8 +321,8 @@ def on_context_menu_for_outputs(self, pos: QPoint): menu = QMenu() show_list = [] copy_list = [] - # figure out which output they right-clicked on. output lines have an anchor named "output N" - txout_idx = int(name.split()[1]) # split "output N", translate N -> int + # figure out which output they right-clicked on. output lines have an anchor named "txio_idx N" + txout_idx = int(name.split()[1]) # split "txio_idx N", translate N -> int menu.addAction(f"Tx Output #{txout_idx}").setDisabled(True) menu.addSeparator() if addr := self.tx.outputs()[txout_idx].address: From ee5dec0c59db97b732a8bdf1185e4b7289f48925 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 17:07:31 +0100 Subject: [PATCH 0069/1143] qml: correctly update channel list after channel backup import, and correctly delete channel backup --- electrum/gui/qml/qechanneldetails.py | 5 ++++- electrum/gui/qml/qechannellistmodel.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 60c556c53..e759ee9aa 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -199,7 +199,10 @@ async def do_close(closetype, channel_id): @pyqtSlot() def deleteChannel(self): - self._wallet.wallet.lnworker.remove_channel(self._channel.channel_id) + if self.isBackup: + self._wallet.wallet.lnworker.remove_channel_backup(self._channel.channel_id) + else: + self._wallet.wallet.lnworker.remove_channel(self._channel.channel_id) @pyqtSlot(result=str) def channelBackup(self): diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 0f03937de..c25f1dd4d 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -41,6 +41,11 @@ def on_event_channel(self, wallet, channel): if wallet == self.wallet: self.on_channel_updated(channel) + @qt_event_listener + def on_event_channels_updated(self, wallet): + if wallet == self.wallet: + self.init_model() + def on_destroy(self): self.unregister_callbacks() From b9f4758853d5c0de8b344347977cd926ed9e5a75 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 16:00:16 +0000 Subject: [PATCH 0070/1143] qt tx dialog: add "Copy Outpoint" to both IO ctx menus --- electrum/gui/qt/transaction_dialog.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 81b0dd8ce..bb1374e89 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -283,7 +283,7 @@ def on_context_menu_for_inputs(self, pos: QPoint): menu.addAction(_("Coinbase Input")).setDisabled(True) else: show_list += [(_("Show Prev Tx"), lambda: self._open_internal_link(txin.prevout.txid.hex()))] - copy_list += [(_("Copy Prevout"), lambda: self.main_window.do_copy(txin.prevout.to_str()))] + copy_list += [(_("Copy") + " " + _("Outpoint"), lambda: self.main_window.do_copy(txin.prevout.to_str()))] addr = self.wallet.adb.get_txin_address(txin) if addr: if self.wallet.is_mine(addr): @@ -325,6 +325,9 @@ def on_context_menu_for_outputs(self, pos: QPoint): txout_idx = int(name.split()[1]) # split "txio_idx N", translate N -> int menu.addAction(f"Tx Output #{txout_idx}").setDisabled(True) menu.addSeparator() + if tx_hash := self.tx.txid(): + outpoint = TxOutpoint(bytes.fromhex(tx_hash), txout_idx) + copy_list += [(_("Copy") + " " + _("Outpoint"), lambda: self.main_window.do_copy(outpoint.to_str()))] if addr := self.tx.outputs()[txout_idx].address: if self.wallet.is_mine(addr): show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))] From b4d2c902c4a3bbd812442049d71897f67a171297 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 16:09:06 +0000 Subject: [PATCH 0071/1143] qt tx dialog: fix for pre-segwit legacy wallets ``` Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/invoice_list.py", line 169, in menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) File "/home/user/wspace/electrum/electrum/gui/qt/send_tab.py", line 585, in do_pay_invoice self.pay_onchain_dialog(self.window.get_coins(), invoice.outputs) File "/home/user/wspace/electrum/electrum/gui/qt/send_tab.py", line 271, in pay_onchain_dialog preview_dlg = PreviewTxDialog( File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 962, in __init__ self.update() File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 658, in update self.io_widget.update(self.tx) File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 212, in update tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) File "/home/user/wspace/electrum/electrum/address_synchronizer.py", line 483, in get_txpos verified_tx_mined_info = self.db.get_verified_tx(tx_hash) File "/home/user/wspace/electrum/electrum/json_db.py", line 44, in wrapper return func(self, *args, **kwargs) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 1256, in get_verified_tx assert isinstance(txid, str) AssertionError ``` --- electrum/gui/qt/transaction_dialog.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index bb1374e89..4abde8c4f 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -226,14 +226,18 @@ def insert_tx_io( o_text.clear() o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) - tx_height, tx_pos = self.wallet.adb.get_txpos(self.tx.txid()) - tx_hash = bytes.fromhex(self.tx.txid()) + tx_height, tx_pos = None, None + tx_hash = self.tx.txid() + if tx_hash: + tx_height, tx_pos = self.wallet.adb.get_txpos(tx_hash) cursor = o_text.textCursor() for txout_idx, o in enumerate(self.tx.outputs()): - if tx_pos is not None and tx_pos >= 0: + if tx_height is not None and tx_pos is not None and tx_pos >= 0: short_id = ShortID.from_components(tx_height, tx_pos, txout_idx) + elif tx_hash: + short_id = TxOutpoint(bytes.fromhex(tx_hash), txout_idx).short_name() else: - short_id = TxOutpoint(tx_hash, txout_idx).short_name() + short_id = f"unknown:{txout_idx}" addr = o.get_ui_address_str() insert_tx_io( cursor=cursor, is_coinbase=False, txio_idx=txout_idx, From 3cd32129217f45e8ac35b039ad960fec721ac142 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 17:13:11 +0100 Subject: [PATCH 0072/1143] qml: show correct delete confirm text for channel backups --- electrum/gui/qml/components/ChannelDetails.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 243f033a5..22d5b75f3 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -242,7 +242,9 @@ Pane { visible: channeldetails.canDelete onClicked: { var dialog = app.messageDialog.createObject(root, { - text: qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), + text: channeldetails.isBackup + ? qsTr('Are you sure you want to delete this channel backup?') + : qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), yesno: true }) dialog.yesClicked.connect(function() { From f8e43b3149cf82b0cd5dcfc49cb9a2de3dcfc99a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Feb 2023 17:27:12 +0100 Subject: [PATCH 0073/1143] qml: styling MessageDialog --- electrum/gui/qml/components/MessageDialog.qml | 45 +++++++++++++------ .../qml/components/controls/FlatButton.qml | 4 +- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 0d6a5e77f..51fd9da56 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -28,35 +28,54 @@ ElDialog { color: "#aa000000" } + padding: 0 + ColumnLayout { - TextArea { - id: message - Layout.preferredWidth: Overlay.overlay.width *2/3 - readOnly: true - wrapMode: TextInput.WordWrap - textFormat: richText ? TextEdit.RichText : TextEdit.PlainText - background: Rectangle { - color: 'transparent' + ColumnLayout { + Layout.margins: constants.paddingMedium + Layout.alignment: Qt.AlignHCenter + TextArea { + id: message + Layout.preferredWidth: Overlay.overlay.width *2/3 + readOnly: true + wrapMode: TextInput.WordWrap + textFormat: richText ? TextEdit.RichText : TextEdit.PlainText + background: Rectangle { + color: 'transparent' + } } } - RowLayout { - Layout.alignment: Qt.AlignHCenter - Button { + ButtonContainer { + Layout.fillWidth: true + + FlatButton { + Layout.fillWidth: true + textUnderIcon: false text: qsTr('Ok') + icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: !yesno onClicked: dialog.close() } - Button { + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + textUnderIcon: false text: qsTr('Yes') + icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: yesno onClicked: { yesClicked() dialog.close() } } - Button { + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + textUnderIcon: false text: qsTr('No') + icon.source: Qt.resolvedUrl('../../icons/closebutton.png') visible: yesno onClicked: { reject() diff --git a/electrum/gui/qml/components/controls/FlatButton.qml b/electrum/gui/qml/components/controls/FlatButton.qml index df336eaed..703e385b6 100644 --- a/electrum/gui/qml/components/controls/FlatButton.qml +++ b/electrum/gui/qml/components/controls/FlatButton.qml @@ -8,8 +8,10 @@ TabButton { id: control checkable: false + property bool textUnderIcon: true + font.pixelSize: constants.fontSizeSmall - display: IconLabel.TextUnderIcon + display: textUnderIcon ? IconLabel.TextUnderIcon : IconLabel.TextBesideIcon contentItem: IconLabel { spacing: control.spacing From dd141c7fa017deaeffabd9b3b6a340b1da1ac487 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 17:09:51 +0000 Subject: [PATCH 0074/1143] libsecp256k1: add runtime support for both 0.2.x and <0.2.0 lib vers related: https://github.com/spesmilo/electrum/pull/8185 https://github.com/bitcoin-core/secp256k1/pull/1055 --- electrum/ecc_fast.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index 3059eda54..fe6e1090e 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -38,16 +38,18 @@ class LibModuleMissing(Exception): pass def load_library(): if sys.platform == 'darwin': - library_paths = (os.path.join(os.path.dirname(__file__), 'libsecp256k1.0.dylib'), - 'libsecp256k1.0.dylib') + libnames = ['libsecp256k1.1.dylib', 'libsecp256k1.0.dylib', ] elif sys.platform in ('windows', 'win32'): - library_paths = (os.path.join(os.path.dirname(__file__), 'libsecp256k1-0.dll'), - 'libsecp256k1-0.dll') + libnames = ['libsecp256k1-1.dll', 'libsecp256k1-0.dll', ] elif 'ANDROID_DATA' in os.environ: - library_paths = ('libsecp256k1.so',) + libnames = ['libsecp256k1.so', ] else: # desktop Linux and similar - library_paths = (os.path.join(os.path.dirname(__file__), 'libsecp256k1.so.0'), - 'libsecp256k1.so.0', 'libsecp256k1.so.1') + libnames = ['libsecp256k1.so.1', 'libsecp256k1.so.0', ] + library_paths = [] + for libname in libnames: # try local files in repo dir first + library_paths.append(os.path.join(os.path.dirname(__file__), libname)) + for libname in libnames: + library_paths.append(libname) exceptions = [] secp256k1 = None From 7d83335e34d7ea4752145b47e086a9fbe45742ad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 17:11:16 +0000 Subject: [PATCH 0075/1143] bump libsecp256k1 version now there are tags :O --- contrib/android/p4a_recipes/libsecp256k1/__init__.py | 4 ++-- contrib/make_libsecp256k1.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contrib/android/p4a_recipes/libsecp256k1/__init__.py b/contrib/android/p4a_recipes/libsecp256k1/__init__.py index 177a1ac53..a1b969f7d 100644 --- a/contrib/android/p4a_recipes/libsecp256k1/__init__.py +++ b/contrib/android/p4a_recipes/libsecp256k1/__init__.py @@ -6,9 +6,9 @@ class LibSecp256k1RecipePinned(LibSecp256k1Recipe): - version = "1253a27756540d2ca526b2061d98d54868e9177c" + version = "21ffe4b22a9683cf24ae0763359e401d1284cc7a" url = "https://github.com/bitcoin-core/secp256k1/archive/{version}.zip" - sha512sum = "92232cdefba54fce5573e8b4a542dcd307e56380e9b72841da00da1d1d48bfa6f4c0d157e5c294be5342e500237761376aee5e29adde70b2bf7be413cbd77571" + sha512sum = "51832bfc6825690d5b71a5426aacce8981163ca1a56a235394aa86e742d105f5e2b331971433a21b8842ee338cbd7877dcbae5605fa01a9e6f4a73171b93f3e9" recipe = LibSecp256k1RecipePinned() diff --git a/contrib/make_libsecp256k1.sh b/contrib/make_libsecp256k1.sh index ba43b28ae..46e091b4c 100755 --- a/contrib/make_libsecp256k1.sh +++ b/contrib/make_libsecp256k1.sh @@ -14,7 +14,8 @@ # sudo apt-get install gcc-multilib g++-multilib # $ AUTOCONF_FLAGS="--host=i686-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32" ./contrib/make_libsecp256k1.sh -LIBSECP_VERSION="1253a27756540d2ca526b2061d98d54868e9177c" +LIBSECP_VERSION="21ffe4b22a9683cf24ae0763359e401d1284cc7a" +# ^ tag "v0.2.0" set -e From c66411f47e4cb7344b55d6ee7720bf763472e181 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Feb 2023 17:36:46 +0000 Subject: [PATCH 0076/1143] contrib/make_libsecp256k1.sh: rm Makefile patch unneeded since https://github.com/bitcoin-core/secp256k1/commit/c0cd7de6d4e497c0e678f7098079727188e81de8 and in fact buggy since https://github.com/bitcoin-core/secp256k1/commit/0bd3e4243caa3c000e6afe3ea5533b97565557c4 related https://github.com/spesmilo/electrum/pull/8185#issuecomment-1416171287 --- contrib/make_libsecp256k1.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/contrib/make_libsecp256k1.sh b/contrib/make_libsecp256k1.sh index 46e091b4c..7400e9313 100755 --- a/contrib/make_libsecp256k1.sh +++ b/contrib/make_libsecp256k1.sh @@ -43,7 +43,6 @@ info "Building $pkgname..." git checkout "${LIBSECP_VERSION}^{commit}" if ! [ -x configure ] ; then - echo "libsecp256k1_la_LDFLAGS = -no-undefined" >> Makefile.am echo "LDFLAGS = -no-undefined" >> Makefile.am ./autogen.sh || fail "Could not run autogen for $pkgname. Please make sure you have automake and libtool installed, and try again." fi From 0eea47c78dd53b44863f8e88e9bed133edc83f6e Mon Sep 17 00:00:00 2001 From: ghost43 Date: Sat, 4 Feb 2023 01:36:19 +0000 Subject: [PATCH 0077/1143] libsecp256k1: update hardcoded .so lib name in binaries (#8186) follow-up 7d83335e34d7ea4752145b47e086a9fbe45742ad --- contrib/build-linux/appimage/make_appimage.sh | 4 ++-- contrib/build-wine/deterministic.spec | 2 +- contrib/build-wine/make_win.sh | 2 +- contrib/build-wine/prepare-wine.sh | 2 +- contrib/osx/make_osx.sh | 4 ++-- contrib/osx/osx.spec | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/build-linux/appimage/make_appimage.sh b/contrib/build-linux/appimage/make_appimage.sh index a9173b232..6d3dca24d 100755 --- a/contrib/build-linux/appimage/make_appimage.sh +++ b/contrib/build-linux/appimage/make_appimage.sh @@ -76,12 +76,12 @@ info "installing python." ) -if [ -f "$DLL_TARGET_DIR/libsecp256k1.so.0" ]; then +if [ -f "$DLL_TARGET_DIR/libsecp256k1.so.1" ]; then info "libsecp256k1 already built, skipping" else "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" fi -cp -f "$DLL_TARGET_DIR/libsecp256k1.so.0" "$APPDIR/usr/lib/libsecp256k1.so.0" || fail "Could not copy libsecp to its destination" +cp -f "$DLL_TARGET_DIR"/libsecp256k1.so.* "$APPDIR/usr/lib/" || fail "Could not copy libsecp to its destination" # note: libxcb-util1 is not available in debian 10 (buster), only libxcb-util0. So we build it ourselves. diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index dabb4524d..e62c38414 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -31,7 +31,7 @@ binaries = [] # Workaround for "Retro Look": binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]] -binaries += [('C:/tmp/libsecp256k1-0.dll', '.')] +binaries += [('C:/tmp/libsecp256k1-1.dll', '.')] binaries += [('C:/tmp/libusb-1.0.dll', '.')] binaries += [('C:/tmp/libzbar-0.dll', '.')] diff --git a/contrib/build-wine/make_win.sh b/contrib/build-wine/make_win.sh index 92a712cc8..51ddaae54 100755 --- a/contrib/build-wine/make_win.sh +++ b/contrib/build-wine/make_win.sh @@ -41,7 +41,7 @@ rm "$here"/dist/* -rf mkdir -p "$CACHEDIR" "$DLL_TARGET_DIR" "$PIP_CACHE_DIR" -if [ -f "$DLL_TARGET_DIR/libsecp256k1-0.dll" ]; then +if [ -f "$DLL_TARGET_DIR/libsecp256k1-1.dll" ]; then info "libsecp256k1 already built, skipping" else "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index 5312cf77d..b8f24a435 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -53,7 +53,7 @@ $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary : # copy already built DLLs -cp "$DLL_TARGET_DIR/libsecp256k1-0.dll" $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libsecp to its destination" +cp "$DLL_TARGET_DIR"/libsecp256k1-*.dll $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libsecp to its destination" cp "$DLL_TARGET_DIR/libzbar-0.dll" $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libzbar to its destination" cp "$DLL_TARGET_DIR/libusb-1.0.dll" $WINEPREFIX/drive_c/tmp/ || fail "Could not copy libusb to its destination" diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index 185efbdf9..af205c663 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -178,13 +178,13 @@ info "generating locale" ) || fail "failed generating locale" -if [ ! -f "$DLL_TARGET_DIR/libsecp256k1.0.dylib" ]; then +if [ ! -f "$DLL_TARGET_DIR/libsecp256k1.1.dylib" ]; then info "Building libsecp256k1 dylib..." "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" else info "Skipping libsecp256k1 build: reusing already built dylib." fi -cp -f "$DLL_TARGET_DIR/libsecp256k1.0.dylib" "$PROJECT_ROOT/electrum/" || fail "Could not copy libsecp256k1 dylib" +cp -f "$DLL_TARGET_DIR"/libsecp256k1.*.dylib "$PROJECT_ROOT/electrum/" || fail "Could not copy libsecp256k1 dylib" if [ ! -f "$DLL_TARGET_DIR/libzbar.0.dylib" ]; then info "Building ZBar dylib..." diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index 59f53ab99..7b983eea6 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -50,7 +50,7 @@ datas += collect_data_files('bitbox02') # Add libusb so Trezor and Safe-T mini will work binaries = [(electrum + "electrum/libusb-1.0.dylib", ".")] -binaries += [(electrum + "electrum/libsecp256k1.0.dylib", ".")] +binaries += [(electrum + "electrum/libsecp256k1.1.dylib", ".")] binaries += [(electrum + "electrum/libzbar.0.dylib", ".")] # Workaround for "Retro Look": From d4a6768b40dddc47b1b11b2efd378bf0285b5102 Mon Sep 17 00:00:00 2001 From: /dev/fd0 Date: Sun, 5 Feb 2023 02:09:25 +0530 Subject: [PATCH 0078/1143] remove duplicate import --- electrum/network.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 2f25327cf..5e531208d 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -31,7 +31,6 @@ import socket import json import sys -import asyncio from typing import NamedTuple, Optional, Sequence, List, Dict, Tuple, TYPE_CHECKING, Iterable, Set, Any, TypeVar import traceback import concurrent From 9ef9b9b76817dd905327224fa8772a17335bd529 Mon Sep 17 00:00:00 2001 From: Jan Sarenik Date: Sun, 5 Feb 2023 10:30:09 +0100 Subject: [PATCH 0079/1143] Add ex.signet.bublina.. option for signet explorer --- electrum/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/util.py b/electrum/util.py index 622538074..0ad3a8cab 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -897,6 +897,8 @@ def time_difference(distance_in_time, include_seconds): {'tx': 'tx/', 'addr': 'address/'}), 'wakiyamap.dev': ('https://signet-explorer.wakiyamap.dev/', {'tx': 'tx/', 'addr': 'address/'}), + 'ex.signet.bublina.eu.org': ('https://ex.signet.bublina.eu.org/', + {'tx': 'tx/', 'addr': 'address/'}), 'system default': ('blockchain:/', {'tx': 'tx/', 'addr': 'address/'}), } From c7a7c54ccd00610c92c0163e94ac6c2333f9fe94 Mon Sep 17 00:00:00 2001 From: Jan Sarenik Date: Sun, 5 Feb 2023 10:32:11 +0100 Subject: [PATCH 0080/1143] Add another signet electrum server Running electrs v0.9.11 currently. TLS via nginx. Always up-to-date info at http://ln.uk.ms/signet.txt --- electrum/servers_signet.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/electrum/servers_signet.json b/electrum/servers_signet.json index 134e2682a..aacd9dce6 100644 --- a/electrum/servers_signet.json +++ b/electrum/servers_signet.json @@ -10,5 +10,11 @@ "s": "50002", "t": "50001", "version": "1.4" + }, + "bublina.eu.org": { + "pruning": "-", + "s": "60003", + "t": "50001", + "version": "1.4" } } From 046609c5d2ef16701fc9d5b8153cb57070dbb808 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 5 Feb 2023 22:49:12 +0000 Subject: [PATCH 0081/1143] lnpeer: add note about thread-safety, and some checks I was calling methods from the Qt console (e.g. peer.pay()) and seeing weird behaviour... htlc_switch() (running on asyncio thread) was racing with pay() (running on GUI thread). --- electrum/lnpeer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index d1151441b..24e4c3de4 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -63,6 +63,8 @@ class Peer(Logger): + # note: in general this class is NOT thread-safe. Most methods are assumed to be running on asyncio thread. + LOGGING_SHORTCUT = 'P' ORDERED_MESSAGES = ( @@ -120,6 +122,7 @@ def __init__( self.downstream_htlc_resolved_event = asyncio.Event() def send_message(self, message_name: str, **kwargs): + assert util.get_running_loop() == util.get_asyncio_loop(), f"this must be run on the asyncio thread!" assert type(message_name) is str if message_name not in self.SPAMMY_MESSAGES: self.logger.debug(f"Sending {message_name.upper()}") @@ -1421,6 +1424,7 @@ def on_update_fail_htlc(self, chan: Channel, payload): self.maybe_send_commitment(chan) def maybe_send_commitment(self, chan: Channel) -> bool: + assert util.get_running_loop() == util.get_asyncio_loop(), f"this must be run on the asyncio thread!" # REMOTE should revoke first before we can sign a new ctx if chan.hm.is_revack_pending(REMOTE): return False From c6b464f1cbcd62439083fefa941ba0ae95d796e0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Feb 2023 11:31:56 +0000 Subject: [PATCH 0082/1143] gitignore: add .so.* (.so libs regardless of version) to cover `electrum/libsecp256k1.so.1` --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7b716aaf8..739ef9783 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,7 @@ contrib/.venv_make_packages/ # shared objects electrum/*.so -electrum/*.so.0 +electrum/*.so.* electrum/*.dll electrum/*.dylib contrib/osx/*.dylib From 036a5997c7d26fc0b5da10b5e94e673159feb89c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 12:43:54 +0100 Subject: [PATCH 0083/1143] qml: fix confusion when dialogs not on top of activeDialog stack are closed --- electrum/gui/qml/components/controls/ElDialog.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 9db30fcc7..dc4e8bc72 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -16,7 +16,12 @@ Dialog { if (opened) { app.activeDialogs.push(abstractdialog) } else { - app.activeDialogs.pop() + if (app.activeDialogs.indexOf(abstractdialog) < 0) { + console.log('dialog should exist in activeDialogs!') + app.activeDialogs.pop() + return + } + app.activeDialogs.splice(app.activeDialogs.indexOf(abstractdialog),1) } } From 7adc8b1fbbef801ae30f48a1c3d37c8e38bc2351 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 13:12:14 +0100 Subject: [PATCH 0084/1143] qml: fix issues with close channel --- .../gui/qml/components/CloseChannelDialog.qml | 47 ++++++++++++------- electrum/gui/qml/qechanneldetails.py | 17 ++++--- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index c80e673c3..ec1bbfe71 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -9,6 +9,7 @@ import "controls" ElDialog { id: dialog + width: parent.width height: parent.height @@ -22,7 +23,7 @@ ElDialog { Overlay.modal: Rectangle { color: "#aa000000" } - property bool closing: false + property bool _closing: false closePolicy: Popup.NoAutoClose @@ -97,7 +98,7 @@ ElDialog { InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true - text: qsTr(channeldetails.message_force_close) + text: channeldetails.message_force_close } Label { @@ -115,41 +116,47 @@ ElDialog { } RadioButton { + id: closetypeCoop ButtonGroup.group: closetypegroup property string closetype: 'cooperative' - checked: true - enabled: !closing && channeldetails.canCoopClose + enabled: !_closing && channeldetails.canCoopClose text: qsTr('Cooperative close') } RadioButton { + id: closetypeRemoteForce ButtonGroup.group: closetypegroup property string closetype: 'remote_force' - enabled: !closing && channeldetails.canForceClose + enabled: !_closing && channeldetails.canForceClose text: qsTr('Request Force-close') } RadioButton { + id: closetypeLocalForce ButtonGroup.group: closetypegroup property string closetype: 'local_force' - enabled: !closing && channeldetails.canForceClose && !channeldetails.isBackup + enabled: !_closing && channeldetails.canForceClose && !channeldetails.isBackup text: qsTr('Local Force-close') } } ColumnLayout { Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width + Label { id: errorText - visible: !closing && errorText + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width + visible: !_closing && errorText wrapMode: Text.Wrap - Layout.preferredWidth: layout.width } Label { + Layout.alignment: Qt.AlignHCenter text: qsTr('Closing...') - visible: closing + visible: _closing } BusyIndicator { - visible: closing + Layout.alignment: Qt.AlignHCenter + visible: _closing } } } @@ -160,10 +167,10 @@ ElDialog { Layout.fillWidth: true text: qsTr('Close channel') icon.source: '../../icons/closebutton.png' - enabled: !closing + enabled: !_closing onClicked: { - closing = true - channeldetails.close_channel(closetypegroup.checkedButton.closetype) + _closing = true + channeldetails.closeChannel(closetypegroup.checkedButton.closetype) } } @@ -175,13 +182,21 @@ ElDialog { wallet: Daemon.currentWallet channelid: dialog.channelid + onChannelChanged : { + // init default choice + if (channeldetails.canCoopClose) + closetypeCoop.checked = true + else + closetypeRemoteForce.checked = true + } + onChannelCloseSuccess: { - closing = false + _closing = false dialog.close() } onChannelCloseFailed: { - closing = false + _closing = false errorText.text = message } } diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index e759ee9aa..61f39479a 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -1,4 +1,5 @@ import asyncio +import threading from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS @@ -177,25 +178,23 @@ def freezeForReceiving(self): else: self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP) - # this method assumes the qobject is not destroyed before the close either fails or succeeds @pyqtSlot(str) - def close_channel(self, closetype): - async def do_close(closetype, channel_id): + def closeChannel(self, closetype): + channel_id = self._channel.channel_id + def do_close(): try: if closetype == 'remote_force': - await self._wallet.wallet.lnworker.request_force_close(channel_id) + self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.request_force_close(channel_id)) elif closetype == 'local_force': - await self._wallet.wallet.lnworker.force_close_channel(channel_id) + self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.force_close_channel(channel_id)) else: - await self._wallet.wallet.lnworker.close_channel(channel_id) + self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.close_channel(channel_id)) self.channelCloseSuccess.emit() except Exception as e: self._logger.exception("Could not close channel: " + repr(e)) self.channelCloseFailed.emit(_('Could not close channel: ') + repr(e)) - loop = self._wallet.wallet.network.asyncio_loop - coro = do_close(closetype, self._channel.channel_id) - asyncio.run_coroutine_threadsafe(coro, loop) + threading.Thread(target=do_close).start() @pyqtSlot() def deleteChannel(self): From d3a10c422550ec863b5d9359e6286cfa48548474 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 6 Feb 2023 17:55:56 +0000 Subject: [PATCH 0085/1143] commands: allow cmd to launch new gui window for wallet e.g. imagine electrum Qt gui is already running, with some wallet (wallet_1) open, running `$ ./run_electrum --testnet -w ~/.electrum/testnet/wallets/wallet_2` should open a new window, for wallet_2. related https://github.com/spesmilo/electrum/issues/8188#issuecomment-1419444675 --- electrum/daemon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 148f63988..b0d04f40c 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -305,9 +305,12 @@ async def ping(self): return True async def gui(self, config_options): + # note: "config_options" is coming from the short-lived CLI-invocation, + # while self.config is the config of the long-lived daemon process. + # "config_options" should have priority. if self.daemon.gui_object: if hasattr(self.daemon.gui_object, 'new_window'): - path = self.config.get_wallet_path(use_gui_last_wallet=True) + path = config_options.get('wallet_path') or self.config.get_wallet_path(use_gui_last_wallet=True) self.daemon.gui_object.new_window(path, config_options.get('url')) response = "ok" else: From ad26d809a4bdaa9fd552ead33d48d035b80407f3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 21:39:41 +0100 Subject: [PATCH 0086/1143] qml: update history in more cases when saving/removing tx --- electrum/gui/qml/qetxdetails.py | 2 ++ electrum/gui/qml/qewallet.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index f3520a2f3..3a0d125c7 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -354,6 +354,7 @@ def removeLocalTx(self, confirm = False): self._wallet.wallet.adb.remove_transaction(txid) self._wallet.wallet.save_db() + self._wallet.historyModel.init_model(True) @pyqtSlot() def save(self): @@ -367,6 +368,7 @@ def save(self): return self._wallet.wallet.save_db() self.saveTxSuccess.emit() + self._wallet.historyModel.init_model(True) except AddTransactionException as e: self.saveTxError.emit('error', str(e)) finally: diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 563393d15..e6e3dcd59 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -513,10 +513,13 @@ def do_sign(self, tx, broadcast): if not tx.is_complete(): self._logger.debug('tx not complete') - return + broadcast = False if broadcast: self.broadcast(tx) + else: + # not broadcasted, so add to history now + self.historyModel.init_model(True) # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok def on_sign_complete(self, broadcast, tx): From 049a59f57a510394c3f4186811aa910350c621e7 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 22:47:15 +0100 Subject: [PATCH 0087/1143] qml: move fee bump button below fee amount --- electrum/gui/qml/components/TxDetails.qml | 45 ++++++++++++----------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index afb85f2ab..d76e6a000 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -105,23 +105,27 @@ Pane { FormattedAmount { Layout.fillWidth: true amount: txdetails.fee - singleLine: !(txdetails.canBump || txdetails.canCpfp) } - FlatButton { - Layout.fillWidth: true - Layout.minimumWidth: implicitWidth - icon.source: '../../icons/warning.png' - icon.color: 'transparent' - text: qsTr('Bump fee') - visible: txdetails.canBump || txdetails.canCpfp - onClicked: { - if (txdetails.canBump) { - var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) - } else { - var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) - } - dialog.open() + } + + Item { + visible: feebumpButton.visible + Layout.preferredWidth: 1 ; Layout.preferredHeight: 1 + } + FlatButton { + id: feebumpButton + visible: txdetails.canBump || txdetails.canCpfp + textUnderIcon: false + icon.source: '../../icons/warning.png' + icon.color: 'transparent' + text: qsTr('Bump fee') + onClicked: { + if (txdetails.canBump) { + var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) + } else { + var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) } + dialog.open() } } @@ -385,27 +389,24 @@ Pane { rawtx: root.rawtx onLabelChanged: root.detailsChanged() onConfirmRemoveLocalTx: { - var dialog = app.messageDialog.createObject(app, {'text': message, 'yesno': true}) + var dialog = app.messageDialog.createObject(app, { text: message, yesno: true }) dialog.yesClicked.connect(function() { dialog.close() txdetails.removeLocalTx(true) - txdetails.wallet.historyModel.init_model(true) root.close() }) dialog.open() } onSaveTxSuccess: { var dialog = app.messageDialog.createObject(app, { - 'text': qsTr('Transaction added to wallet history.') + '\n\n' + - qsTr('Note: this is an offline transaction, if you want the network to see it, you need to broadcast it.') + text: qsTr('Transaction added to wallet history.') + '\n\n' + + qsTr('Note: this is an offline transaction, if you want the network to see it, you need to broadcast it.') }) dialog.open() root.close() } onSaveTxError: { - var dialog = app.messageDialog.createObject(app, { - 'text': message - }) + var dialog = app.messageDialog.createObject(app, { text: message }) dialog.open() } } From a84450386112b45a56717ec3a178d0329e113e1f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 22:47:41 +0100 Subject: [PATCH 0088/1143] qml: move separator visible property binding to its component so our master index ref is stable --- .../components/controls/ButtonContainer.qml | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/components/controls/ButtonContainer.qml b/electrum/gui/qml/components/controls/ButtonContainer.qml index 98e5e89cc..ae1553731 100644 --- a/electrum/gui/qml/components/controls/ButtonContainer.qml +++ b/electrum/gui/qml/components/controls/ButtonContainer.qml @@ -24,16 +24,11 @@ Container { contentRoot.children.push(verticalSeparator.createObject(_layout, { pheight: rowheight * 2/3, - visible: Qt.binding(function() { - let anybefore_visible = false - for (let j = i-1; j >= 0; j--) { - anybefore_visible = anybefore_visible || root.itemAt(j).visible - } - return button.visible && anybefore_visible - }) + master_idx: i })) contentRoot.children.push(button) + console.log('push ' + i + ', v=' + button.visible + ',len=' + contentRoot.children.length) } contentItem = contentRoot @@ -52,11 +47,24 @@ Container { id: verticalSeparator Rectangle { required property int pheight + required property int master_idx Layout.fillWidth: false Layout.preferredWidth: 2 Layout.preferredHeight: pheight + Layout.leftMargin: 2 + Layout.rightMargin: 2 Layout.alignment: Qt.AlignVCenter color: constants.darkerBackground + Component.onCompleted: { + // create binding here, we need to be able to have stable ref master_idx + visible = Qt.binding(function() { + let anybefore_visible = false + for (let j = master_idx-1; j >= 0; j--) { + anybefore_visible = anybefore_visible || root.itemAt(j).visible + } + return root.itemAt(master_idx).visible && anybefore_visible + }) + } } } From 99133a65dd98564b64b9f149e3dacf0935b32ebe Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 22:52:38 +0100 Subject: [PATCH 0089/1143] followup a84450386112b45a56717ec3a178d0329e113e1f --- electrum/gui/qml/components/controls/ButtonContainer.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/electrum/gui/qml/components/controls/ButtonContainer.qml b/electrum/gui/qml/components/controls/ButtonContainer.qml index ae1553731..644d40088 100644 --- a/electrum/gui/qml/components/controls/ButtonContainer.qml +++ b/electrum/gui/qml/components/controls/ButtonContainer.qml @@ -28,7 +28,6 @@ Container { })) contentRoot.children.push(button) - console.log('push ' + i + ', v=' + button.visible + ',len=' + contentRoot.children.length) } contentItem = contentRoot @@ -51,8 +50,6 @@ Container { Layout.fillWidth: false Layout.preferredWidth: 2 Layout.preferredHeight: pheight - Layout.leftMargin: 2 - Layout.rightMargin: 2 Layout.alignment: Qt.AlignVCenter color: constants.darkerBackground Component.onCompleted: { From 2cfcf5035d8db57eeccbf03370ebde0a829d884f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Feb 2023 23:57:44 +0100 Subject: [PATCH 0090/1143] qml: add rocket.png for fee bump icon --- electrum/gui/icons/rocket.png | Bin 0 -> 3869 bytes electrum/gui/qml/components/RbfBumpFeeDialog.qml | 1 + 2 files changed, 1 insertion(+) create mode 100644 electrum/gui/icons/rocket.png diff --git a/electrum/gui/icons/rocket.png b/electrum/gui/icons/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..a80a1e9552358796d89f11a87bdb3fbf973ecaf9 GIT binary patch literal 3869 zcmV+&5908NP)j^f(#Z>Qb8V3XoHk$OlrisG!E%XQIlw6+Eq1a zS3{z)%hhHXjhdh)(G^#0rNuhNQ~`~`L!p92QeefHUB|Bg^f@rRDr9)I#2CS;j^!^BBv$=|&Qt^&IJSE}`Rl%p@z}#8bcd5HA1V6Q(NPpg zE71IzBH+d5W-F2LZZd^-2XHb#TW1>I_bJ#m!L#QPzhnjV8=s~h+i{g3;|NqNvFS5I zz~&vhm&XzJDZz~1?XKt1k;)Q_#+WoVLGFrcX%~X}4eRi#V*T}iv|Re3^3QQ-POqJBTX2}!`VZQBf6kN#L8 zzEw+Zng+2*1R-iWc{F4(zCOmjFI`#Y3BH21q69#V9s4SrfCOxMXYa{q;io`dP$d+C za4<+H80c3=wqVoJmcns8SaBI0zE9nT|D?Sr@Ki~UipC!R5GPV9yk%$8T+ib&c?*T6l{mWy5G?L?DP1xgBm{}wcuC3j04DHGxd>U)Al`o?76I`hVh1> zQx}g9*?53pz`!zfIxqM$oUh#omW3KO;kRA#6M}#@b~G(Ts8Pu~;N9Yc}4sUw1vB|Da1g>M@}4?f1VRB+mg>r7Rc-5DA9>FiagoGY~@I z`wH7}@H~G&{Yoh^nJi=CQD&Srk#{t)Y3^6P_->bcR0-I;W6ya?s&&*bJ`@ZP4uz0X z5{-n=HLXk@3^LV~bM=DFxzQbgS&ON>5qZS0;*s*JlP}-}t+&&x% z5()+pLXe0>ky7w>YnGP}bkI@sII}Lm<71@ie~%`qIuC7Djq%fzNdXP-G6 z=ysh^@qpLfX*yj;d#RT3k#LA$ptQ|}f&ruy{C0l_5AAH}KGW@N&+>XpihCDNC1eFC z7HxdL{6#9|lg?y%8;^i{=w&>NLJ-`tbLTi+iQfUIRLa7kV7KuAmSJFfK9BDHxavUr zan51g2Wl+YHY$)`B{tQWgpZ2nB;Z4MHHK z;9$n4=++K)?|0^bQd-;_*?b<`cFNk6V)c?GOWaBw$CZGh7WoOVxUxMMC$Z!A+Izfq>P$)m6fET@seY z!tv26nG}MHCM5u{k5+*^&nKBlQxVrafp!BGYrk91(99SfHt*PT9;h{y?WSoG359EQ z0U%!}MfaQkXd(^cW8J5^n5px(^G;!AT?of@aa|9k6sb&_8oSy9=bwGX+kK5O2 z{TnAK!}<=Mx=?y*8cGF(rwk>9tE9+bh}{_4uV?uBnxC(l@4Wfu?lG<(OzAKO-d5+_@L8UUDF8#EObLd2!ZGO z6m6SA-bSHFXR;KERk?uc3%Pjl-055TwQV2*mOj|{ec`yb3CHjM3|a$o##vlBH&H&I z5Y)wE7`j0qU}72uQVM*fa2%IHp;)%veRMAQ6pX8V0dw1StgyXe+qXSsDS$#Ig)BnH;uLwPRHw zlg-sM9(cN8?##OfR6q>^mOi*E0?C)PoU;HxnHpYDN0}PRlqgNoyT&9OA8C8Yyo<~^ zbY(1;&WaH+%VTUL62dSIV$m>C3SQdZ!Q)LwNIM>eCi&8odajy2mPjN-XR@@m`$9IC zBVYR*uv;r=*FlYs0Rs`R^kY)dd_ zT>1GXV3`Ch3)3`^QnKaaG!N`->6Sa5&)U7MNC97+aWcVxMK)Kpp97G|<;dr28h=PC zwPL}7Gg7sLwa{(d(w}X)S2*s=ZXy`ACq?k;tO1R$f+q0lgDjg>?)ti}5eY{CF!U1V z|Mh?EHB~&nuLEE8{I55ZI=bd-8{g^s;^Kw#X6_rxly!g$A8CA21@$ZZF&03_QJDqQ z&QSWn{Fr21pocgq1+i$9uHCL{+iNY@HT!2>kG$s*l9l9_qJ7b>*)OAnzhdz@vvv%1 zzWS2KwtP*6tXGxB-LXhy_X{h|=#DCjp4wHfsp7dxh>Jj738Jc8Ri;b|E;^|^b&Z6> zXuXW_eGkx?77OxGu4>)o<}mB#q&IpsWhJF)tbs~<$B8- z=Fi?gBm*ii?21ssQ`^5M;Lfh+*%6KFCz{>zL@bI?$;?~If}MJ zDxIz>bd>tD)po>+CG%&r4r}tgFzmz78!PACg%CgNdOkPWb0!a1X5YpGC`BflBbm;U zFBB=-4uzskI+N{gOQuq_jfelRb#LjhFdjx&6BHmC)^2}N7vhT5(*nv62q6UZi8!(< ziVqB>AXmtftG)kwN~nKqm^<^I8Dx?(7>wvRtUwt$H%twPj}1}E4@1G=@Qn8rsdTz8 z<5Nm-+0pd&m{xH|TovAQ@x`Z!$y3&;zrN`lX`0m6)%ANhp%z@%C7G_B`YOdcLf0-^ zbk-SrM>NqsFd`4wwrts{y6>;o_U%1Kd*mSkFl6Hkg#w+abd~X{^rG)^64ruG%Xk7$HVl z5(@#uO~r(ePq3IF_M`7WF57RXLeZw9BUx3!Q3^JFnB}LN+u7G)qvf32#8YT}s5vGW z5drJr_LNke<3l~}8!Tps-F0hC5?t4#Gnpct$>3MK^`^APgN<#xxF>^ibo&#w7rY?3 z`Rb9(aUcwvfX^>mcIt~RKffzBJmC~5+@j^o{Mo=qj#e6;RV`2>2X06yu-nztq)?fDYg} zw!iAg9yQ&|YX`Fu=ctYOIcGoflMAn&f28PFeGjaJmT}PAdXgcfYxeqYH&lH-KUU~} z2|Rk{m(kswAa2G|aesLlHKzc9l4X-mNwNBy&*;9B_W*>{`+CDZYR6Gn$<` zQaISWhio=`;3K8}0(cF0Bod4L<&q5>+{UXHq+Y*b-gOH8f&S9jiZYxMVsMGYFj0F6 ztX`@!xn&nHy{z5z@mHiFSiOETYwP^W9zX5cjHx}__`(bQ79|(2-F^p(+BJIx*8ZOE ze#m%b!#B?V!XRuJ7HWIIgu@9Cn}I`RbDL_H8{frUt4_>SAH4kXOHT}BJizOh&%3+N zC#vZBcX&xO*l!nd@)AFj?Jjo+ul?!K{<0d|*e zc!)*pZ~AjY_t&E~H2-o|ZSC^{!a(qJ)qQ(I6g`CN;|H|njW;fQc(^);g1+wwuG-Z? zUj?2=XFiWm8d83j>-X#%$pp;$j;)!-pZ@#G zl{LSEeOegx>+%SHrcAqzZ)kU{ziGic$9wou_W6 Date: Tue, 7 Feb 2023 00:03:48 +0100 Subject: [PATCH 0091/1143] qml: silence undefined property errors when not really used --- electrum/gui/qml/components/History.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 17b7a2247..6a1f758b8 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -131,7 +131,9 @@ Pane { Label { id: postext anchors.centerIn: parent - text: listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date + text: vdragscroll.drag.active + ? listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date + : '' font.pixelSize: constants.fontSizeLarge } } From fa72da57fd2239f4a5807f61dd449cbd3655adc3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 00:06:05 +0100 Subject: [PATCH 0092/1143] Revert "qml: silence undefined property errors when not really used" This reverts commit ba51cef0d5e9b3cdf71199940ef02a3cb9f5dc1d. --- electrum/gui/qml/components/History.qml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 6a1f758b8..17b7a2247 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -131,9 +131,7 @@ Pane { Label { id: postext anchors.centerIn: parent - text: vdragscroll.drag.active - ? listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date - : '' + text: listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date font.pixelSize: constants.fontSizeLarge } } From b2d4a2a81f417c463ba47bdf314e338e5600d485 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 00:13:03 +0100 Subject: [PATCH 0093/1143] qml: fix show mempool depth when unconfirmed --- electrum/gui/qml/components/TxDetails.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index d76e6a000..fd09bd724 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -143,12 +143,12 @@ Pane { Label { text: qsTr('Mempool depth') color: Material.accentColor - visible: !txdetails.isMined && txdetails.canBroadcast + visible: !txdetails.isMined && !txdetails.canBroadcast } Label { text: txdetails.mempoolDepth - visible: !txdetails.isMined && txdetails.canBroadcast + visible: !txdetails.isMined && !txdetails.canBroadcast } Label { From b9b0ada15defae7c8c290d1889cb711e6ecf1978 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 00:21:21 +0100 Subject: [PATCH 0094/1143] qml: better fix --- electrum/gui/qml/components/TxDetails.qml | 4 ++-- electrum/gui/qml/qetxdetails.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index fd09bd724..15edf83f1 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -143,12 +143,12 @@ Pane { Label { text: qsTr('Mempool depth') color: Material.accentColor - visible: !txdetails.isMined && !txdetails.canBroadcast + visible: txdetails.mempoolDepth } Label { text: txdetails.mempoolDepth - visible: !txdetails.isMined && !txdetails.canBroadcast + visible: txdetails.mempoolDepth } Label { diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 3a0d125c7..7457b6a01 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -255,7 +255,7 @@ def update(self): self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height > 0 if self._is_mined: self.update_mined_status(txinfo.tx_mined_status) - else: + elif txinfo.tx_mined_status.height == 0: self._mempool_depth = self._wallet.wallet.config.depth_tooltip(txinfo.mempool_depth_bytes) if self._wallet.wallet.lnworker: From db34efd333e4d454d38eb908e21e4ddf1f4ee27f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 10:35:34 +0100 Subject: [PATCH 0095/1143] qml: silence undefined property errors when not really used --- electrum/gui/qml/components/History.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 17b7a2247..f8c6d0f8f 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -131,7 +131,9 @@ Pane { Label { id: postext anchors.centerIn: parent - text: listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date + text: dragb.opacity + ? listview.itemAt(0,listview.contentY + (dragb.y + dragb.height/2)).delegateModel.date + : '' font.pixelSize: constants.fontSizeLarge } } From 9d4e00d582532d2330c39db433c167c2172d3c39 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 11:10:28 +0100 Subject: [PATCH 0096/1143] qml: styling ImportAddressesKeysDialog --- .../components/ImportAddressesKeysDialog.qml | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml index 58e4b0606..9320cbffe 100644 --- a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml +++ b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml @@ -9,6 +9,10 @@ import "controls" ElDialog { id: root + title: Daemon.currentWallet.isWatchOnly + ? qsTr('Import additional addresses') + : qsTr('Import additional keys') + property bool valid: false modal: true @@ -16,12 +20,11 @@ ElDialog { Overlay.modal: Rectangle { color: "#aa000000" } + width: parent.width height: parent.height - title: Daemon.currentWallet.isWatchOnly - ? qsTr('Import additional addresses') - : qsTr('Import additional keys') + padding: 0 function verify(text) { if (Daemon.currentWallet.isWatchOnly) @@ -38,61 +41,68 @@ ElDialog { } ColumnLayout { - width: parent.width - height: parent.height + anchors.fill: parent + spacing: 0 - Label { - text: Daemon.currentWallet.isWatchOnly - ? qsTr('Import additional addresses') - : qsTr('Import additional keys') - } + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: constants.paddingLarge + Layout.rightMargin: constants.paddingLarge - RowLayout { - TextArea { - id: import_ta - Layout.fillWidth: true - Layout.minimumHeight: 80 - focus: true - wrapMode: TextEdit.WrapAnywhere - onTextChanged: valid = verify(text) + Label { + text: Daemon.currentWallet.isWatchOnly + ? qsTr('Import additional addresses') + : qsTr('Import additional keys') } - ColumnLayout { - Layout.alignment: Qt.AlignTop - ToolButton { - icon.source: '../../icons/paste.png' - icon.height: constants.iconSizeMedium - icon.width: constants.iconSizeMedium - onClicked: { - if (verify(AppController.clipboardToText())) { - if (import_ta.text != '') - import_ta.text = import_ta.text + '\n' - import_ta.text = import_ta.text + AppController.clipboardToText() - } - } + + RowLayout { + TextArea { + id: import_ta + Layout.fillWidth: true + Layout.minimumHeight: 80 + focus: true + wrapMode: TextEdit.WrapAnywhere + onTextChanged: valid = verify(text) } - ToolButton { - icon.source: '../../icons/qrcode.png' - icon.height: constants.iconSizeMedium - icon.width: constants.iconSizeMedium - scale: 1.2 - onClicked: { - var scan = qrscan.createObject(root.contentItem) // can't use dialog as parent? - scan.onFound.connect(function() { - if (verify(scan.scanData)) { + ColumnLayout { + Layout.alignment: Qt.AlignTop + ToolButton { + icon.source: '../../icons/paste.png' + icon.height: constants.iconSizeMedium + icon.width: constants.iconSizeMedium + onClicked: { + if (verify(AppController.clipboardToText())) { if (import_ta.text != '') - import_ta.text = import_ta.text + ',\n' - import_ta.text = import_ta.text + scan.scanData + import_ta.text = import_ta.text + '\n' + import_ta.text = import_ta.text + AppController.clipboardToText() } - scan.destroy() - }) + } + } + ToolButton { + icon.source: '../../icons/qrcode.png' + icon.height: constants.iconSizeMedium + icon.width: constants.iconSizeMedium + scale: 1.2 + onClicked: { + var scan = qrscan.createObject(root.contentItem) // can't use dialog as parent? + scan.onFound.connect(function() { + if (verify(scan.scanData)) { + if (import_ta.text != '') + import_ta.text = import_ta.text + ',\n' + import_ta.text = import_ta.text + scan.scanData + } + scan.destroy() + }) + } } } } - } - Item { - Layout.preferredWidth: 1 - Layout.fillHeight: true + Item { + Layout.preferredWidth: 1 + Layout.fillHeight: true + } } FlatButton { From 2dff6a10cacc7fcaab2f81f94a6829a694e8e829 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 11:18:27 +0100 Subject: [PATCH 0097/1143] qml: fix issue with number of addresses in model --- electrum/gui/qml/qeaddresslistmodel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py index 42d621109..2dd1c357b 100644 --- a/electrum/gui/qml/qeaddresslistmodel.py +++ b/electrum/gui/qml/qeaddresslistmodel.py @@ -73,7 +73,7 @@ def init_model(self): r_addresses = self.wallet.get_receiving_addresses() c_addresses = self.wallet.get_change_addresses() - n_addresses = len(r_addresses) + len(c_addresses) if self.wallet.use_change else 0 + n_addresses = len(r_addresses) + (len(c_addresses) if self.wallet.use_change else 0) def insert_row(atype, alist, address, iaddr): item = self.addr_to_model(address) From 9d02f6ee4c5b5651c509c756c86c857c0abf7ab0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 11:40:31 +0100 Subject: [PATCH 0098/1143] qml: shorten text for adding addresses/keys and add add.png icon --- electrum/gui/icons/add.png | Bin 0 -> 687 bytes electrum/gui/qml/components/WalletDetails.qml | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 electrum/gui/icons/add.png diff --git a/electrum/gui/icons/add.png b/electrum/gui/icons/add.png new file mode 100644 index 0000000000000000000000000000000000000000..2de908118dea674bdffe2d136d7e3596cfc7ebbb GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9F>>p_??`B~p#prB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&3=B**o-U3d6?5Lsw(S-U6lt4p zT)rvgrjx+guDP4UlzuR?+|0?={_#WoFtjG5w#e8KR-$;_KOrelG(y(mA0vl zl_7w2uFSo8UvvL2``o>NdjnrWFD}COIqQ~xGShsywyn0-NS|c2*30ossKvucch<bl4ciNEP6}>jV90d{e86;K?~}SirVomuBX>8rd9$qe z{xHE@cM8+wi*stZ?%Q)MoPLct;prMB*AC6z`5Z4LUd{hMN3(0$JiAGq`ybu?>3V@R zf_=lX&rFZR4{qSmUj0&*|677hy5ZaTPa@7S1-bCwyv^!!W9bq#ov$3;o;w*dsy39j zFmP_D(fa!}T_Zl}R21urYgf+~?X~j1aO7}2+mg!ebCwpH&w8<*y}KpOVgEmYsanfC zwzpXy@bi+pxx$N0aK6vO*>-m;reAuU|Kg9*3#K*95sz^Zmn#{6=1<{yTJq_$dQ$Q7 z6g$SliHCn@xmbv5T$}FXS`w#Y^8Tgso>tE5u U!d?4(fQgO4)78&qol`;+0RHJNdH?_b literal 0 HcmV?d00001 diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index ba908f70f..074ed9af3 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -468,8 +468,9 @@ Pane { Layout.preferredWidth: 1 visible: Daemon.currentWallet.walletType == 'imported' text: Daemon.currentWallet.isWatchOnly - ? qsTr('Import additional addresses') - : qsTr('Import additional keys') + ? qsTr('Add addresses') + : qsTr('Add keys') + icon.source: '../../icons/add.png' onClicked: rootItem.importAddressesKeys() } FlatButton { From 393dcde7ae12d87e39e52c7970ac7d075a2a9328 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 11:49:57 +0100 Subject: [PATCH 0099/1143] qml: set a minimum button size --- electrum/gui/qml/components/GenericShareDialog.qml | 2 ++ electrum/gui/qml/components/ReceiveDialog.qml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index 1cc8bbd6b..1702f433d 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -76,6 +76,7 @@ ElDialog { Layout.alignment: Qt.AlignHCenter FlatButton { + Layout.minimumWidth: dialog.width * 1/4 text: qsTr('Copy') icon.source: '../../icons/copy_bw.png' onClicked: { @@ -84,6 +85,7 @@ ElDialog { } } FlatButton { + Layout.minimumWidth: dialog.width * 1/4 text: qsTr('Share') icon.source: '../../icons/share.png' onClicked: { diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 54bfa0477..e96291f07 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -245,6 +245,7 @@ ElDialog { id: buttons Layout.alignment: Qt.AlignHCenter FlatButton { + Layout.minimumWidth: dialog.width * 1/4 icon.source: '../../icons/copy_bw.png' icon.color: 'transparent' text: 'Copy' @@ -259,6 +260,7 @@ ElDialog { } } FlatButton { + Layout.minimumWidth: dialog.width * 1/4 icon.source: '../../icons/share.png' text: 'Share' onClicked: { @@ -274,6 +276,7 @@ ElDialog { } } FlatButton { + Layout.minimumWidth: dialog.width * 1/4 Layout.alignment: Qt.AlignHCenter icon.source: '../../icons/pen.png' text: qsTr('Edit') From 73004b4993162a9a94da55f7cffd5ebd22f9c93a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 11:54:10 +0100 Subject: [PATCH 0100/1143] qml: button min size --- electrum/gui/qml/components/ExportTxDialog.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index e745b81ba..4e4d3e35b 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -69,6 +69,7 @@ ElDialog { Layout.alignment: Qt.AlignHCenter FlatButton { + Layout.minimumWidth: dialog.width * 1/4 text: qsTr('Copy') icon.source: '../../icons/copy_bw.png' onClicked: { @@ -77,6 +78,7 @@ ElDialog { } } FlatButton { + Layout.minimumWidth: dialog.width * 1/4 text: qsTr('Share') icon.source: '../../icons/share.png' onClicked: { From 78d68d00e0640ae58f88875e2683cfc00f88e1a8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 12:23:14 +0100 Subject: [PATCH 0101/1143] qml: override default Material styling for toolbar, use grays --- electrum/gui/qml/components/main.qml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index ec31d5720..822da4ac2 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -2,6 +2,7 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.3 import QtQuick.Controls.Material 2.0 +import QtQuick.Controls.Material.impl 2.12 import QtQml 2.6 import QtMultimedia 5.6 @@ -35,6 +36,17 @@ ApplicationWindow header: ToolBar { id: toolbar + background: Rectangle { + implicitHeight: 48 + color: Material.dialogColor + + layer.enabled: true + layer.effect: ElevationEffect { + elevation: 4 + fullWidth: true + } + } + ColumnLayout { spacing: 0 width: parent.width @@ -52,6 +64,12 @@ ApplicationWindow Layout.preferredHeight: 1 } + Image { + source: '../../icons/wallet.png' + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + } + Label { Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height) text: stack.currentItem.title From c3c308ec0b5f107bf402e670a56bad5e8380f96b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 13:50:26 +0100 Subject: [PATCH 0102/1143] qml: qewallet logger add wallet name --- electrum/gui/qml/qewallet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index e6e3dcd59..7e4961b1f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -79,6 +79,8 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): super().__init__(parent) self.wallet = wallet + self._logger = get_logger(f'{__name__}.[{wallet}]') + self._isUpToDate = False self._synchronizing = False self._synchronizing_progress = '' @@ -195,7 +197,7 @@ def on_event_new_transaction(self, wallet, tx): @qt_event_listener def on_event_wallet_updated(self, wallet): if wallet == self.wallet: - self._logger.debug('wallet %s updated' % str(wallet)) + self._logger.debug('wallet_updated') self.balanceChanged.emit() self.synchronizing = not wallet.is_up_to_date() From 5eb7bcebef9c36300db9b2edd6e1e9e9c695dea8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 13:51:26 +0100 Subject: [PATCH 0103/1143] qml: remove ugly notification popup, add wallet name to notifications --- electrum/gui/qml/components/Constants.qml | 8 +- .../gui/qml/components/NotificationPopup.qml | 108 +++++++++++++----- electrum/gui/qml/components/main.qml | 9 +- electrum/gui/qml/qeapp.py | 7 +- 4 files changed, 95 insertions(+), 37 deletions(-) diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 812a71900..94d17d4cf 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -27,11 +27,14 @@ Item { readonly property int fingerWidth: 64 // TODO: determine finger width from screen dimensions and resolution - property color colorCredit: "#ff80ff80" - property color colorDebit: "#ffff8080" property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2) property color darkerBackground: Qt.darker(Material.background, 1.20) property color lighterBackground: Qt.lighter(Material.background, 1.10) + property color notificationBackground: Qt.lighter(Material.background, 1.5) + + property color colorCredit: "#ff80ff80" + property color colorDebit: "#ffff8080" + property color colorMine: "yellow" property color colorError: '#ffff8080' property color colorLightningLocal: "blue" @@ -41,7 +44,6 @@ Item { property color colorPiechartOnchain: Qt.darker(Material.accentColor, 1.50) property color colorPiechartFrozen: 'gray' property color colorPiechartLightning: 'orange' //Qt.darker(Material.accentColor, 1.20) - property color colorPiechartParticipant: 'gray' property color colorPiechartSignature: 'yellow' diff --git a/electrum/gui/qml/components/NotificationPopup.qml b/electrum/gui/qml/components/NotificationPopup.qml index adff81c09..3700d77be 100644 --- a/electrum/gui/qml/components/NotificationPopup.qml +++ b/electrum/gui/qml/components/NotificationPopup.qml @@ -2,60 +2,114 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.3 import QtQuick.Controls.Material 2.0 +import QtQuick.Controls.Material.impl 2.12 -Rectangle { +Item { id: root - property alias text: textItem.text + property string message + property string wallet_name + property bool _hide: true - property bool hide: true + clip:true - color: Qt.lighter(Material.background, 1.5) - radius: constants.paddingXLarge - - width: root.parent.width * 2/3 - height: layout.height - x: (root.parent.width - width) / 2 - y: -height + layer.enabled: height > 0 + layer.effect: ElevationEffect { + elevation: constants.paddingXLarge + fullWidth: true + } states: [ State { - name: 'expanded'; when: !hide - PropertyChanges { target: root; y: 100 } + name: 'expanded'; when: !_hide + PropertyChanges { target: root; height: layout.implicitHeight } } ] transitions: [ Transition { from: ''; to: 'expanded'; reversible: true - NumberAnimation { properties: 'y'; duration: 300; easing.type: Easing.InOutQuad } + NumberAnimation { target: root; properties: 'height'; duration: 300; easing.type: Easing.OutQuad } } ] - function show(message) { - root.text = message - root.hide = false + function show(wallet_name, message) { + root.wallet_name = wallet_name + root.message = message + root._hide = false closetimer.start() } - RowLayout { - id: layout - width: parent.width - Text { - id: textItem - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - font.pixelSize: constants.fontSizeLarge - color: Material.foreground - wrapMode: Text.Wrap + Rectangle { + id: rect + width: root.width + height: layout.height + color: constants.colorAlpha(Material.dialogColor, 0.8) + anchors.bottom: root.bottom + + ColumnLayout { + id: layout + width: parent.width + spacing: 0 + + RowLayout { + Layout.margins: constants.paddingLarge + spacing: constants.paddingSizeSmall + + Image { + source: '../../icons/info.png' + Layout.preferredWidth: constants.iconSizeLarge + Layout.preferredHeight: constants.iconSizeLarge + } + + Label { + id: messageLabel + Layout.fillWidth: true + font.pixelSize: constants.fontSizeLarge + color: Material.foreground + wrapMode: Text.Wrap + text: root.message + } + } + Rectangle { + Layout.preferredHeight: 2 + Layout.fillWidth: true + color: Material.accentColor + } } + + RowLayout { + visible: root.wallet_name && root.wallet_name != Daemon.currentWallet.name + anchors.right: rect.right + anchors.bottom: rect.bottom + + RowLayout { + Layout.margins: constants.paddingSmall + Image { + source: '../../icons/wallet.png' + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + } + + Label { + font.pixelSize: constants.fontSizeSmall + color: Material.accentColor + text: root.wallet_name + } + } + } + } + + MouseArea { + // capture all clicks + anchors.fill: parent } Timer { id: closetimer interval: 5000 repeat: false - onTriggered: hide = true + onTriggered: _hide = true } } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 822da4ac2..7957d381b 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -262,6 +262,7 @@ ApplicationWindow NotificationPopup { id: notificationPopup + width: parent.width } Component { @@ -360,8 +361,8 @@ ApplicationWindow Connections { target: AppController - function onUserNotify(message) { - notificationPopup.show(message) + function onUserNotify(wallet_name, message) { + notificationPopup.show(wallet_name, message) } function onShowException() { var dialog = crashDialog.createObject(app, { @@ -378,10 +379,10 @@ ApplicationWindow } // TODO: add to notification queue instead of barging through function onPaymentSucceeded(key) { - notificationPopup.show(qsTr('Payment Succeeded')) + notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment Succeeded')) } function onPaymentFailed(key, reason) { - notificationPopup.show(qsTr('Payment Failed') + ': ' + reason) + notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment Failed') + ': ' + reason) } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 1cebd04d8..ae6af94c7 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -49,7 +49,7 @@ class QEAppController(BaseCrashReporter, QObject): _dummy = pyqtSignal() - userNotify = pyqtSignal(str) + userNotify = pyqtSignal(str, str) uriReceived = pyqtSignal(str) showException = pyqtSignal() sendingBugreport = pyqtSignal() @@ -94,7 +94,7 @@ def on_wallet_loaded(self): def on_wallet_usernotify(self, wallet, message): self.logger.debug(message) - self.user_notification_queue.put(message) + self.user_notification_queue.put((wallet,message)) if not self.notification_timer.isActive(): self.logger.debug('starting app notification timer') self.notification_timer.start() @@ -111,7 +111,8 @@ def on_notification_timer(self): self.user_notification_last_time = now self.logger.info("Notifying GUI about new user notifications") try: - self.userNotify.emit(self.user_notification_queue.get_nowait()) + wallet, message = self.user_notification_queue.get_nowait() + self.userNotify.emit(str(wallet), message) except queue.Empty: pass From bc3946d2f4e00abdfc8bcd2980ea4a467a2e9756 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 14 Dec 2022 17:16:15 +0100 Subject: [PATCH 0104/1143] Qt: new onchain tx creation flow: - transaction_dialog is read-only - ConfirmTxDialog and RBF dialogs inherit from TxEditor - TxEditors are configurable --- electrum/gui/qt/confirm_tx_dialog.py | 593 ++++++++++++++++++++------ electrum/gui/qt/fee_slider.py | 22 +- electrum/gui/qt/main_window.py | 15 +- electrum/gui/qt/rbf_dialog.py | 152 +++---- electrum/gui/qt/send_tab.py | 50 +-- electrum/gui/qt/settings_dialog.py | 8 - electrum/gui/qt/transaction_dialog.py | 269 +----------- electrum/wallet.py | 2 +- 8 files changed, 583 insertions(+), 528 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index d06723742..ab240050c 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -24,31 +24,45 @@ # SOFTWARE. from decimal import Decimal +from functools import partial from typing import TYPE_CHECKING, Optional, Union from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QVBoxLayout, QLabel, QGridLayout, QPushButton, QLineEdit +from PyQt5.QtGui import QIcon + +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QLineEdit, QToolButton, QMenu from electrum.i18n import _ from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates +from electrum.util import quantize_feerate from electrum.plugin import run_hook from electrum.transaction import Transaction, PartialTransaction from electrum.wallet import InternalAddressCorruption +from electrum.simple_config import SimpleConfig from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, - BlockingWaitingDialog, PasswordLineEdit, WWLabel) + BlockingWaitingDialog, PasswordLineEdit, WWLabel, read_QIcon) from .fee_slider import FeeSlider, FeeComboBox if TYPE_CHECKING: from .main_window import ElectrumWindow +from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget +from .fee_slider import FeeSlider, FeeComboBox +from .amountedit import FeerateEdit, BTCAmountEdit +from .locktimeedit import LockTimeEdit + +class TxEditor(WindowModalDialog): -class TxEditor: + def __init__(self, *, title='', + window: 'ElectrumWindow', + make_tx, + output_value: Union[int, str] = None, + allow_preview=True): - def __init__(self, *, window: 'ElectrumWindow', make_tx, - output_value: Union[int, str] = None, is_sweep: bool): + WindowModalDialog.__init__(self, window, title=title) self.main_window = window self.make_tx = make_tx self.output_value = output_value @@ -58,9 +72,40 @@ def __init__(self, *, window: 'ElectrumWindow', make_tx, self.not_enough_funds = False self.no_dynfee_estimates = False self.needs_update = False - self.password_required = self.wallet.has_keystore_encryption() and not is_sweep + # preview is disabled for lightning channel funding + self.allow_preview = allow_preview + self.is_preview = False + + self.locktime_e = LockTimeEdit(self) + self.locktime_label = QLabel(_("LockTime") + ": ") + self.io_widget = TxInOutWidget(self.main_window, self.wallet) + self.create_fee_controls() + + vbox = QVBoxLayout() + self.setLayout(vbox) + + top = self.create_top_bar(self.help_text) + grid = self.create_grid() + + vbox.addLayout(top) + vbox.addLayout(grid) + self.message_label = WWLabel('\n') + vbox.addWidget(self.message_label) + vbox.addWidget(self.io_widget) + buttons = self.create_buttons_bar() + vbox.addStretch(1) + vbox.addLayout(buttons) + + self.set_io_visible(self.config.get('show_tx_io', False)) + self.set_fee_edit_visible(self.config.get('show_fee_details', False)) + self.set_locktime_visible(self.config.get('show_locktime', False)) + self.set_preview_visible(self.config.get('show_preview_button', False)) + self.update_fee_target() + self.resize(self.layout().sizeHint()) + self.main_window.gui_object.timer.timeout.connect(self.timer_actions) + def timer_actions(self): if self.needs_update: self.update_tx() @@ -70,7 +115,7 @@ def timer_actions(self): def stop_editor_updates(self): self.main_window.gui_object.timer.timeout.disconnect(self.timer_actions) - def fee_slider_callback(self, dyn, pos, fee_rate): + def set_fee_config(self, dyn, pos, fee_rate): if dyn: if self.config.use_mempool_fees(): self.config.set_key('depth_level', pos, False) @@ -78,10 +123,399 @@ def fee_slider_callback(self, dyn, pos, fee_rate): self.config.set_key('fee_level', pos, False) else: self.config.set_key('fee_per_kb', fee_rate, False) + + def update_tx(self, *, fallback_to_zero_fee: bool = False): + raise NotImplementedError() + + def update_fee_target(self): + text = self.fee_slider.get_dynfee_target() + self.fee_target.setText(text) + self.fee_target.setVisible(bool(text)) # hide in static mode + + def update_feerate_label(self): + self.feerate_label.setText(self.feerate_e.text() + ' ' + self.feerate_e.base_unit()) + + def create_fee_controls(self): + + self.fee_label = QLabel('') + self.fee_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + + self.size_label = TxSizeLabel() + self.size_label.setAlignment(Qt.AlignCenter) + self.size_label.setAmount(0) + self.size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) + + self.feerate_label = QLabel('') + self.feerate_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + + self.fiat_fee_label = TxFiatLabel() + self.fiat_fee_label.setAlignment(Qt.AlignCenter) + self.fiat_fee_label.setAmount(0) + self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) + + self.feerate_e = FeerateEdit(lambda: 0) + self.feerate_e.setAmount(self.config.fee_per_byte()) + self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False)) + self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True)) + self.update_feerate_label() + + self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point) + self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False)) + self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True)) + + self.feerate_e.setFixedWidth(150) + self.fee_e.setFixedWidth(150) + + self.fee_e.textChanged.connect(self.entry_changed) + self.feerate_e.textChanged.connect(self.entry_changed) + + self.fee_target = QLabel('') + self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback) + self.fee_combo = FeeComboBox(self.fee_slider) + + def feerounding_onclick(): + text = (self.feerounding_text + '\n\n' + + _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + + _('At most 100 satoshis might be lost due to this rounding.') + ' ' + + _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' + + _('Also, dust is not kept as change, but added to the fee.') + '\n' + + _('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.')) + self.show_message(title=_('Fee rounding'), msg=text) + + self.feerounding_icon = QToolButton() + self.feerounding_icon.setIcon(QIcon()) + self.feerounding_icon.setAutoRaise(True) + self.feerounding_icon.clicked.connect(feerounding_onclick) + + self.fee_hbox = fee_hbox = QHBoxLayout() + fee_hbox.addWidget(self.feerate_e) + fee_hbox.addWidget(self.feerate_label) + fee_hbox.addWidget(self.size_label) + fee_hbox.addWidget(self.fee_e) + fee_hbox.addWidget(self.fee_label) + fee_hbox.addWidget(self.fiat_fee_label) + fee_hbox.addWidget(self.feerounding_icon) + fee_hbox.addStretch() + + self.fee_target_hbox = fee_target_hbox = QHBoxLayout() + fee_target_hbox.addWidget(self.fee_target) + fee_target_hbox.addWidget(self.fee_slider) + fee_target_hbox.addWidget(self.fee_combo) + fee_target_hbox.addStretch() + + # set feerate_label to same size as feerate_e + self.feerate_label.setFixedSize(self.feerate_e.sizeHint()) + self.fee_label.setFixedSize(self.fee_e.sizeHint()) + self.fee_slider.setFixedWidth(200) + self.fee_target.setFixedSize(self.feerate_e.sizeHint()) + + def _trigger_update(self): + # set tx to None so that the ok button is disabled while we compute the new tx + self.tx = None + self.update() self.needs_update = True + def fee_slider_callback(self, dyn, pos, fee_rate): + self.set_fee_config(dyn, pos, fee_rate) + self.fee_slider.activate() + if fee_rate: + fee_rate = Decimal(fee_rate) + self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000)) + else: + self.feerate_e.setAmount(None) + self.fee_e.setModified(False) + self.update_fee_target() + self.update_feerate_label() + self._trigger_update() + + def on_fee_or_feerate(self, edit_changed, editing_finished): + edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e + if editing_finished: + if edit_changed.get_amount() is None: + # This is so that when the user blanks the fee and moves on, + # we go back to auto-calculate mode and put a fee back. + edit_changed.setModified(False) + else: + # edit_changed was edited just now, so make sure we will + # freeze the correct fee setting (this) + edit_other.setModified(False) + self.fee_slider.deactivate() + self._trigger_update() + + def is_send_fee_frozen(self): + return self.fee_e.isVisible() and self.fee_e.isModified() \ + and (self.fee_e.text() or self.fee_e.hasFocus()) + + def is_send_feerate_frozen(self): + return self.feerate_e.isVisible() and self.feerate_e.isModified() \ + and (self.feerate_e.text() or self.feerate_e.hasFocus()) + + def set_feerounding_text(self, num_satoshis_added): + self.feerounding_text = (_('Additional {} satoshis are going to be added.') + .format(num_satoshis_added)) + def get_fee_estimator(self): - return None + if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None: + fee_estimator = self.fee_e.get_amount() + elif self.is_send_feerate_frozen() and self.feerate_e.get_amount() is not None: + amount = self.feerate_e.get_amount() # sat/byte feerate + amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate + fee_estimator = partial( + SimpleConfig.estimate_fee_for_feerate, amount) + else: + fee_estimator = None + return fee_estimator + + def entry_changed(self): + # blue color denotes auto-filled values + text = "" + fee_color = ColorScheme.DEFAULT + feerate_color = ColorScheme.DEFAULT + if self.not_enough_funds: + fee_color = ColorScheme.RED + feerate_color = ColorScheme.RED + elif self.fee_e.isModified(): + feerate_color = ColorScheme.BLUE + elif self.feerate_e.isModified(): + fee_color = ColorScheme.BLUE + else: + fee_color = ColorScheme.BLUE + feerate_color = ColorScheme.BLUE + self.fee_e.setStyleSheet(fee_color.as_stylesheet()) + self.feerate_e.setStyleSheet(feerate_color.as_stylesheet()) + # + self.needs_update = True + + def update_fee_fields(self): + freeze_fee = self.is_send_fee_frozen() + freeze_feerate = self.is_send_feerate_frozen() + tx = self.tx + if self.no_dynfee_estimates and tx: + size = tx.estimated_size() + self.size_label.setAmount(size) + #self.size_e.setAmount(size) + if self.not_enough_funds or self.no_dynfee_estimates: + if not freeze_fee: + self.fee_e.setAmount(None) + if not freeze_feerate: + self.feerate_e.setAmount(None) + self.feerounding_icon.setIcon(QIcon()) + return + + assert tx is not None + size = tx.estimated_size() + fee = tx.get_fee() + + #self.size_e.setAmount(size) + self.size_label.setAmount(size) + fiat_fee = self.main_window.format_fiat_and_units(fee) + self.fiat_fee_label.setAmount(fiat_fee) + + # Displayed fee/fee_rate values are set according to user input. + # Due to rounding or dropping dust in CoinChooser, + # actual fees often differ somewhat. + if freeze_feerate or self.fee_slider.is_active(): + displayed_feerate = self.feerate_e.get_amount() + if displayed_feerate is not None: + displayed_feerate = quantize_feerate(displayed_feerate) + elif self.fee_slider.is_active(): + # fallback to actual fee + displayed_feerate = quantize_feerate(fee / size) if fee is not None else None + self.feerate_e.setAmount(displayed_feerate) + displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None + self.fee_e.setAmount(displayed_fee) + else: + if freeze_fee: + displayed_fee = self.fee_e.get_amount() + else: + # fallback to actual fee if nothing is frozen + displayed_fee = fee + self.fee_e.setAmount(displayed_fee) + displayed_fee = displayed_fee if displayed_fee else 0 + displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None + self.feerate_e.setAmount(displayed_feerate) + + # set fee rounding icon to empty if there is no rounding + feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0 + self.set_feerounding_text(int(feerounding)) + self.feerounding_icon.setToolTip(self.feerounding_text) + self.feerounding_icon.setIcon(read_QIcon('info.png') if abs(feerounding) >= 1 else QIcon()) + + + def create_buttons_bar(self): + self.preview_button = QPushButton(_('Preview')) + self.preview_button.clicked.connect(self.on_preview) + self.ok_button = QPushButton(_('OK')) + self.ok_button.clicked.connect(self.on_send) + self.ok_button.setDefault(True) + buttons = Buttons(CancelButton(self), self.preview_button, self.ok_button) + return buttons + + def create_top_bar(self, text): + self.pref_menu = QMenu() + self.m1 = self.pref_menu.addAction('Show inputs/outputs', self.toggle_io_visibility) + self.m1.setCheckable(True) + self.m2 = self.pref_menu.addAction('Edit fees', self.toggle_fee_details) + self.m2.setCheckable(True) + self.m3 = self.pref_menu.addAction('Edit Locktime', self.toggle_locktime) + self.m3.setCheckable(True) + self.m4 = self.pref_menu.addAction('Show Preview Button', self.toggle_preview_button) + self.m4.setCheckable(True) + self.m4.setEnabled(self.allow_preview) + self.pref_button = QToolButton() + self.pref_button.setIcon(read_QIcon("preferences.png")) + self.pref_button.setMenu(self.pref_menu) + self.pref_button.setPopupMode(QToolButton.InstantPopup) + hbox = QHBoxLayout() + hbox.addWidget(QLabel(text)) + hbox.addStretch() + hbox.addWidget(self.pref_button) + return hbox + + def toggle_io_visibility(self): + b = not self.config.get('show_tx_io', False) + self.config.set_key('show_tx_io', b) + self.set_io_visible(b) + #self.resize(self.layout().sizeHint()) + self.setFixedSize(self.layout().sizeHint()) + + def toggle_fee_details(self): + b = not self.config.get('show_fee_details', False) + self.config.set_key('show_fee_details', b) + self.set_fee_edit_visible(b) + self.setFixedSize(self.layout().sizeHint()) + + def toggle_locktime(self): + b = not self.config.get('show_locktime', False) + self.config.set_key('show_locktime', b) + self.set_locktime_visible(b) + self.setFixedSize(self.layout().sizeHint()) + + def toggle_preview_button(self): + b = not self.config.get('show_preview_button', False) + self.config.set_key('show_preview_button', b) + self.set_preview_visible(b) + + def set_preview_visible(self, b): + b = b and self.allow_preview + self.preview_button.setVisible(b) + self.m4.setChecked(b) + + def set_io_visible(self, b): + self.io_widget.setVisible(b) + self.m1.setChecked(b) + + def set_fee_edit_visible(self, b): + detailed = [self.feerounding_icon, self.feerate_e, self.fee_e] + basic = [self.fee_label, self.feerate_label] + # first hide, then show + for w in (basic if b else detailed): + w.hide() + for w in (detailed if b else basic): + w.show() + self.m2.setChecked(b) + + def set_locktime_visible(self, b): + for w in [ + self.locktime_e, + self.locktime_label]: + w.setVisible(b) + self.m3.setChecked(b) + + def run(self): + cancelled = not self.exec_() + self.stop_editor_updates() + self.deleteLater() # see #3956 + return self.tx if not cancelled else None + + def on_send(self): + self.accept() + + def on_preview(self): + self.is_preview = True + self.accept() + + def toggle_send_button(self, enable: bool, *, message: str = None): + if message is None: + self.message_label.setStyleSheet(None) + self.message_label.setText(' ') + else: + self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) + self.message_label.setText(message) + + self.setFixedSize(self.layout().sizeHint()) + self.preview_button.setEnabled(enable) + self.ok_button.setEnabled(enable) + + def update(self): + tx = self.tx + self._update_amount_label() + if self.not_enough_funds: + text = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen() + self.toggle_send_button(False, message=text) + return + if not tx: + self.toggle_send_button(False) + return + self.update_fee_fields() + if self.locktime_e.get_locktime() is None: + self.locktime_e.set_locktime(self.tx.locktime) + self.io_widget.update(tx) + fee = tx.get_fee() + assert fee is not None + self.fee_label.setText(self.main_window.config.format_amount_and_units(fee)) + + fee_rate = fee // tx.estimated_size() + #self.feerate_label.setText(self.main_window.format_amount(fee_rate)) + + # extra fee + x_fee = run_hook('get_tx_extra_fee', self.wallet, tx) + if x_fee: + x_fee_address, x_fee_amount = x_fee + self.extra_fee_label.setVisible(True) + self.extra_fee_value.setVisible(True) + self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount)) + amount = tx.output_value() if self.output_value == '!' else self.output_value + tx_size = tx.estimated_size() + fee_warning_tuple = self.wallet.get_tx_fee_warning( + invoice_amt=amount, tx_size=tx_size, fee=fee) + if fee_warning_tuple: + allow_send, long_warning, short_warning = fee_warning_tuple + self.toggle_send_button(allow_send, message=long_warning) + else: + self.toggle_send_button(True) + + def _update_amount_label(self): + pass + +class ConfirmTxDialog(TxEditor): + help_text = ''#_('Set the mining fee of your transaction') + + def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], allow_preview=True): + + TxEditor.__init__( + self, + window=window, + make_tx=make_tx, + output_value=output_value, + title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps + allow_preview=allow_preview) + + BlockingWaitingDialog(window, _("Preparing transaction..."), self.update_tx) + self.update() + + def _update_amount_label(self): + tx = self.tx + if self.output_value == '!': + if tx: + amount = tx.output_value() + amount_str = self.main_window.format_amount_and_units(amount) + else: + amount_str = "max" + else: + amount = self.output_value + amount_str = self.main_window.format_amount_and_units(amount) + self.amount_label.setText(amount_str) def update_tx(self, *, fallback_to_zero_fee: bool = False): fee_estimator = self.get_fee_estimator() @@ -116,6 +550,7 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): self.tx.set_rbf(True) def have_enough_funds_assuming_zero_fees(self) -> bool: + # called in send_tab.py try: tx = self.make_tx(0) except NotEnoughFunds: @@ -123,147 +558,39 @@ def have_enough_funds_assuming_zero_fees(self) -> bool: else: return True - - - -class ConfirmTxDialog(TxEditor, WindowModalDialog): - # set fee and return password (after pw check) - - def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], is_sweep: bool): - - TxEditor.__init__(self, window=window, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep) - WindowModalDialog.__init__(self, window, _("Confirm Transaction")) - vbox = QVBoxLayout() - self.setLayout(vbox) + def create_grid(self): grid = QGridLayout() - vbox.addLayout(grid) - msg = (_('The amount to be received by the recipient.') + ' ' + _('Fees are paid by the sender.')) self.amount_label = QLabel('') self.amount_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + grid.addWidget(HelpLabel(_("Amount to be sent") + ": ", msg), 0, 0) grid.addWidget(self.amount_label, 0, 1) msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\ + _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\ + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.') - self.fee_label = QLabel('') - self.fee_label.setTextInteractionFlags(Qt.TextSelectableByMouse) - grid.addWidget(HelpLabel(_("Mining fee") + ": ", msg), 1, 0) - grid.addWidget(self.fee_label, 1, 1) + grid.addWidget(HelpLabel(_("Mining Fee") + ": ", msg), 1, 0) + grid.addLayout(self.fee_hbox, 1, 1, 1, 3) + + grid.addWidget(HelpLabel(_("Fee target") + ": ", self.fee_combo.help_msg), 3, 0) + grid.addLayout(self.fee_target_hbox, 3, 1, 1, 3) + + grid.setColumnStretch(4, 1) + + # extra fee self.extra_fee_label = QLabel(_("Additional fees") + ": ") self.extra_fee_label.setVisible(False) self.extra_fee_value = QLabel('') self.extra_fee_value.setTextInteractionFlags(Qt.TextSelectableByMouse) self.extra_fee_value.setVisible(False) - grid.addWidget(self.extra_fee_label, 2, 0) - grid.addWidget(self.extra_fee_value, 2, 1) - - self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback) - self.fee_combo = FeeComboBox(self.fee_slider) - grid.addWidget(HelpLabel(_("Fee rate") + ": ", self.fee_combo.help_msg), 5, 0) - grid.addWidget(self.fee_slider, 5, 1) - grid.addWidget(self.fee_combo, 5, 2) - - self.message_label = WWLabel(self.default_message()) - grid.addWidget(self.message_label, 6, 0, 1, -1) - self.pw_label = QLabel(_('Password')) - self.pw_label.setVisible(self.password_required) - self.pw = PasswordLineEdit() - self.pw.setVisible(self.password_required) - grid.addWidget(self.pw_label, 8, 0) - grid.addWidget(self.pw, 8, 1, 1, -1) - self.preview_button = QPushButton(_('Advanced')) - self.preview_button.clicked.connect(self.on_preview) - grid.addWidget(self.preview_button, 0, 2) - self.send_button = QPushButton(_('Send')) - self.send_button.clicked.connect(self.on_send) - self.send_button.setDefault(True) - vbox.addLayout(Buttons(CancelButton(self), self.send_button)) - BlockingWaitingDialog(window, _("Preparing transaction..."), self.update_tx) - self.update() - self.is_send = False + grid.addWidget(self.extra_fee_label, 5, 0) + grid.addWidget(self.extra_fee_value, 5, 1) - def default_message(self): - return _('Enter your password to proceed') if self.password_required else _('Click Send to proceed') + # locktime editor + grid.addWidget(self.locktime_label, 6, 0) + grid.addWidget(self.locktime_e, 6, 1, 1, 2) - def on_preview(self): - self.accept() - - def run(self): - cancelled = not self.exec_() - password = self.pw.text() or None - self.stop_editor_updates() - self.deleteLater() # see #3956 - return cancelled, self.is_send, password, self.tx - - def on_send(self): - password = self.pw.text() or None - if self.password_required: - if password is None: - self.main_window.show_error(_("Password required"), parent=self) - return - try: - self.wallet.check_password(password) - except Exception as e: - self.main_window.show_error(str(e), parent=self) - return - self.is_send = True - self.accept() - - def toggle_send_button(self, enable: bool, *, message: str = None): - if message is None: - self.message_label.setStyleSheet(None) - self.message_label.setText(self.default_message()) - else: - self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) - self.message_label.setText(message) - self.pw.setEnabled(enable) - self.send_button.setEnabled(enable) - - def _update_amount_label(self): - tx = self.tx - if self.output_value == '!': - if tx: - amount = tx.output_value() - amount_str = self.main_window.format_amount_and_units(amount) - else: - amount_str = "max" - else: - amount = self.output_value - amount_str = self.main_window.format_amount_and_units(amount) - self.amount_label.setText(amount_str) - - def update(self): - tx = self.tx - self._update_amount_label() - - if self.not_enough_funds: - text = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen() - self.toggle_send_button(False, message=text) - return - - if not tx: - return - - fee = tx.get_fee() - assert fee is not None - self.fee_label.setText(self.main_window.format_amount_and_units(fee)) - x_fee = run_hook('get_tx_extra_fee', self.wallet, tx) - if x_fee: - x_fee_address, x_fee_amount = x_fee - self.extra_fee_label.setVisible(True) - self.extra_fee_value.setVisible(True) - self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount)) - - amount = tx.output_value() if self.output_value == '!' else self.output_value - tx_size = tx.estimated_size() - fee_warning_tuple = self.wallet.get_tx_fee_warning( - invoice_amt=amount, tx_size=tx_size, fee=fee) - if fee_warning_tuple: - allow_send, long_warning, short_warning = fee_warning_tuple - self.toggle_send_button(allow_send, message=long_warning) - else: - self.toggle_send_button(True) + return grid diff --git a/electrum/gui/qt/fee_slider.py b/electrum/gui/qt/fee_slider.py index 293a3c6c4..b73c4b49b 100644 --- a/electrum/gui/qt/fee_slider.py +++ b/electrum/gui/qt/fee_slider.py @@ -40,13 +40,18 @@ def __init__(self, window, config, callback): self.update() self.valueChanged.connect(self.moved) self._active = True + self.setFocusPolicy(Qt.NoFocus) + + def get_fee_rate(self, pos): + if self.dyn: + fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos) + else: + fee_rate = self.config.static_fee(pos) + return fee_rate def moved(self, pos): with self.lock: - if self.dyn: - fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos) - else: - fee_rate = self.config.static_fee(pos) + fee_rate = self.get_fee_rate(pos) tooltip = self.get_tooltip(pos, fee_rate) QToolTip.showText(QCursor.pos(), tooltip, self) self.setToolTip(tooltip) @@ -60,6 +65,15 @@ def get_tooltip(self, pos, fee_rate): else: return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate + def get_dynfee_target(self): + if not self.dyn: + return '' + pos = self.value() + fee_rate = self.get_fee_rate(pos) + mempool = self.config.use_mempool_fees() + target, estimate = self.config.get_fee_text(pos, True, mempool, fee_rate) + return target + def update(self): with self.lock: self.dyn = self.config.is_dynfee() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 418776bd2..dd5fee6b1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1258,17 +1258,16 @@ def open_channel(self, connect_str, funding_sat, push_amt): msg = messages.MGS_CONFLICTING_BACKUP_INSTANCE if not self.question(msg): return - # use ConfirmTxDialog # we need to know the fee before we broadcast, because the txid is required make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id) - d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, is_sweep=False) - # disable preview button because the user must not broadcast tx before establishment_flow - d.preview_button.setEnabled(False) - cancelled, is_send, password, funding_tx = d.run() - if not is_send: - return - if cancelled: + d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, allow_preview=False) + funding_tx = d.run() + if not funding_tx: return + self._open_channel(connect_str, funding_sat, push_amt, funding_tx) + + @protected + def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password): # read funding_sat from tx; converts '!' to int value funding_sat = funding_tx.output_value_for_address(ln_dummy_address()) def task(): diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index f803d2910..e99a477e4 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QCheckBox, QLabel, QVBoxLayout, QGridLayout, QWidget, QPushButton, QHBoxLayout, QComboBox) @@ -20,7 +21,9 @@ from .main_window import ElectrumWindow -class _BaseRBFDialog(WindowModalDialog): +from .confirm_tx_dialog import ConfirmTxDialog, TxEditor, TxSizeLabel, HelpLabel + +class _BaseRBFDialog(TxEditor): def __init__( self, @@ -30,125 +33,110 @@ def __init__( txid: str, title: str): - WindowModalDialog.__init__(self, main_window, title=title) - self.window = main_window self.wallet = main_window.wallet - self.tx = tx - self.new_tx = None + self.old_tx = tx assert txid - self.txid = txid + self.old_txid = txid self.message = '' - fee = tx.get_fee() - assert fee is not None - tx_size = tx.estimated_size() - self.old_fee_rate = old_fee_rate = fee / tx_size # sat/vbyte - vbox = QVBoxLayout(self) - vbox.addWidget(WWLabel(self.help_text)) - vbox.addStretch(1) - - self.ok_button = OkButton(self) - self.message_label = QLabel('') - self.feerate_e = FeerateEdit(lambda: 0) - self.feerate_e.setAmount(max(old_fee_rate * 1.5, old_fee_rate + 1)) - self.feerate_e.textChanged.connect(self.update) - - def on_slider(dyn, pos, fee_rate): - fee_slider.activate() - if fee_rate is not None: - self.feerate_e.setAmount(fee_rate / 1000) - - fee_slider = FeeSlider(self.window, self.window.config, on_slider) - fee_combo = FeeComboBox(fee_slider) - fee_slider.deactivate() - self.feerate_e.textEdited.connect(fee_slider.deactivate) - - grid = QGridLayout() + self.old_fee = self.old_tx.get_fee() + self.old_tx_size = tx.estimated_size() + self.old_fee_rate = old_fee_rate = self.old_fee / self.old_tx_size # sat/vbyte - self.method_label = QLabel(_('Method') + ':') - self.method_combo = QComboBox() - self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')]) - self.method_combo.currentIndexChanged.connect(self.update) - grid.addWidget(self.method_label, 0, 0) - grid.addWidget(self.method_combo, 0, 1) + TxEditor.__init__( + self, + window=main_window, + title=title, + make_tx=self.rbf_func) - grid.addWidget(QLabel(_('Current fee') + ':'), 1, 0) - grid.addWidget(QLabel(self.window.format_amount_and_units(fee)), 1, 1) - grid.addWidget(QLabel(_('Current fee rate') + ':'), 2, 0) - grid.addWidget(QLabel(self.window.format_fee_rate(1000 * old_fee_rate)), 2, 1) - - grid.addWidget(QLabel(_('New fee rate') + ':'), 3, 0) - grid.addWidget(self.feerate_e, 3, 1) - grid.addWidget(fee_slider, 3, 2) - grid.addWidget(fee_combo, 3, 3) - grid.addWidget(self.message_label, 5, 0, 1, 3) - - vbox.addLayout(grid) - vbox.addStretch(1) - btns_hbox = QHBoxLayout() - btns_hbox.addStretch(1) - btns_hbox.addWidget(CancelButton(self)) - btns_hbox.addWidget(self.ok_button) - vbox.addLayout(btns_hbox) - - new_fee_rate = old_fee_rate + max(1, old_fee_rate // 20) + new_fee_rate = self.old_fee_rate + max(1, self.old_fee_rate // 20) self.feerate_e.setAmount(new_fee_rate) self._update_tx(new_fee_rate) self._update_message() - # give focus to fee slider - fee_slider.activate() - fee_slider.setFocus() + self.fee_slider.activate() # are we paying max? invoices = self.wallet.get_relevant_invoices_for_tx(txid) if len(invoices) == 1 and len(invoices[0].outputs) == 1: if invoices[0].outputs[0].value == '!': self.set_decrease_payment() + def create_grid(self): + self.method_label = QLabel(_('Method') + ':') + self.method_combo = QComboBox() + self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')]) + self.method_combo.currentIndexChanged.connect(self.update) + old_size_label = TxSizeLabel() + old_size_label.setAlignment(Qt.AlignCenter) + old_size_label.setAmount(self.old_tx_size) + old_size_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) + current_fee_hbox = QHBoxLayout() + current_fee_hbox.addWidget(QLabel(self.main_window.format_fee_rate(1000 * self.old_fee_rate))) + current_fee_hbox.addWidget(old_size_label) + current_fee_hbox.addWidget(QLabel(self.main_window.format_amount_and_units(self.old_fee))) + current_fee_hbox.addStretch() + grid = QGridLayout() + grid.addWidget(self.method_label, 0, 0) + grid.addWidget(self.method_combo, 0, 1) + grid.addWidget(QLabel(_('Current fee') + ':'), 1, 0) + grid.addLayout(current_fee_hbox, 1, 1, 1, 3) + grid.addWidget(QLabel(_('New fee') + ':'), 2, 0) + grid.addLayout(self.fee_hbox, 2, 1, 1, 3) + grid.addWidget(HelpLabel(_("Fee target") + ": ", self.fee_combo.help_msg), 4, 0) + grid.addLayout(self.fee_target_hbox, 4, 1, 1, 3) + grid.setColumnStretch(4, 1) + # locktime + grid.addWidget(self.locktime_label, 5, 0) + grid.addWidget(self.locktime_e, 5, 1, 1, 2) + return grid + def is_decrease_payment(self): return self.method_combo.currentIndex() == 1 def set_decrease_payment(self): self.method_combo.setCurrentIndex(1) - def rbf_func(self, fee_rate) -> PartialTransaction: - raise NotImplementedError() # implemented by subclasses - def run(self) -> None: if not self.exec_(): return - self.new_tx.set_rbf(True) - tx_label = self.wallet.get_label_for_txid(self.txid) - self.window.show_transaction(self.new_tx, tx_desc=tx_label) - # TODO maybe save tx_label as label for new tx?? - - def update(self): + if self.is_preview: + self.main_window.show_transaction(self.tx) + return + def sign_done(success): + if success: + self.main_window.broadcast_or_show(self.tx) + self.main_window.sign_tx( + self.tx, + callback=sign_done, + external_keypairs={}) + + def update_tx(self): fee_rate = self.feerate_e.get_amount() self._update_tx(fee_rate) self._update_message() def _update_tx(self, fee_rate): if fee_rate is None: - self.new_tx = None + self.tx = None self.message = '' elif fee_rate <= self.old_fee_rate: - self.new_tx = None + self.tx = None self.message = _("The new fee rate needs to be higher than the old fee rate.") else: try: - self.new_tx = self.rbf_func(fee_rate) + self.tx = self.make_tx(fee_rate) except CannotBumpFee as e: - self.new_tx = None + self.tx = None self.message = str(e) - if not self.new_tx: + if not self.tx: return - delta = self.new_tx.get_fee() - self.tx.get_fee() + delta = self.tx.get_fee() - self.old_tx.get_fee() if not self.is_decrease_payment(): - self.message = _("You will pay {} more.").format(self.window.format_amount_and_units(delta)) + self.message = _("You will pay {} more.").format(self.main_window.format_amount_and_units(delta)) else: - self.message = _("The recipient will receive {} less.").format(self.window.format_amount_and_units(delta)) + self.message = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta)) def _update_message(self): - enabled = bool(self.new_tx) + enabled = bool(self.tx) self.ok_button.setEnabled(enabled) if enabled: style = ColorScheme.BLUE.as_stylesheet() @@ -177,10 +165,10 @@ def __init__( def rbf_func(self, fee_rate): return self.wallet.bump_fee( - tx=self.tx, - txid=self.txid, + tx=self.old_tx, + txid=self.old_txid, new_fee_rate=fee_rate, - coins=self.window.get_coins(), + coins=self.main_window.get_coins(), decrease_payment=self.is_decrease_payment()) @@ -206,4 +194,4 @@ def __init__( self.method_combo.setVisible(False) def rbf_func(self, fee_rate): - return self.wallet.dscancel(tx=self.tx, new_fee_rate=fee_rate) + return self.wallet.dscancel(tx=self.old_tx, new_fee_rate=fee_rate) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 6294d1d50..2bb64ef51 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -28,7 +28,6 @@ from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit from .confirm_tx_dialog import ConfirmTxDialog -from .transaction_dialog import PreviewTxDialog if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -234,7 +233,7 @@ def pay_onchain_dialog( output_value = '!' else: output_value = sum(output_values) - conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep) + conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value) if conf_dlg.not_enough_funds: # Check if we had enough funds excluding fees, # if so, still provide opportunity to set lower fees. @@ -243,37 +242,26 @@ def pay_onchain_dialog( self.show_message(text) return - # shortcut to advanced preview (after "enough funds" check!) - if self.config.get('advanced_preview'): - preview_dlg = PreviewTxDialog( - window=self.window, - make_tx=make_tx, - external_keypairs=external_keypairs, - output_value=output_value) - preview_dlg.show() + tx = conf_dlg.run() + if tx is None: + # user cancelled return - - cancelled, is_send, password, tx = conf_dlg.run() - if cancelled: + is_preview = conf_dlg.is_preview + if is_preview: + self.window.show_transaction(tx) return - if is_send: - self.save_pending_invoice() - def sign_done(success): - if success: - self.window.broadcast_or_show(tx) - self.window.sign_tx_with_password( - tx, - callback=sign_done, - password=password, - external_keypairs=external_keypairs, - ) - else: - preview_dlg = PreviewTxDialog( - window=self.window, - make_tx=make_tx, - external_keypairs=external_keypairs, - output_value=output_value) - preview_dlg.show() + + self.save_pending_invoice() + def sign_done(success): + if success: + self.window.broadcast_or_show(tx) + else: + raise + + self.window.sign_tx( + tx, + callback=sign_done, + external_keypairs=external_keypairs) def get_text_not_enough_funds_mentioning_frozen(self) -> str: text = _("Not enough funds") diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 5155a1414..e247e3bcb 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -276,13 +276,6 @@ def on_set_filelogging(v): filelogging_cb.stateChanged.connect(on_set_filelogging) filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.')) - preview_cb = QCheckBox(_('Advanced preview')) - preview_cb.setChecked(bool(self.config.get('advanced_preview', False))) - preview_cb.setToolTip(_("Open advanced transaction preview dialog when 'Pay' is clicked.")) - def on_preview(x): - self.config.set_key('advanced_preview', x == Qt.Checked) - preview_cb.stateChanged.connect(on_preview) - usechange_cb = QCheckBox(_('Use change addresses')) usechange_cb.setChecked(self.wallet.use_change) if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False) @@ -494,7 +487,6 @@ def on_fiat_address(checked): tx_widgets = [] tx_widgets.append((usechange_cb, None)) tx_widgets.append((batch_rbf_cb, None)) - tx_widgets.append((preview_cb, None)) tx_widgets.append((unconf_cb, None)) tx_widgets.append((multiple_cb, None)) tx_widgets.append((outrounding_cb, None)) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 4abde8c4f..30e8ac6b9 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -43,6 +43,7 @@ from electrum.simple_config import SimpleConfig from electrum.util import quantize_feerate from electrum import bitcoin + from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX from electrum.i18n import _ from electrum.plugin import run_hook @@ -59,10 +60,6 @@ BlockingWaitingDialog, getSaveFileName, ColorSchemeItem, get_iconname_qrcode) -from .fee_slider import FeeSlider, FeeComboBox -from .confirm_tx_dialog import TxEditor -from .amountedit import FeerateEdit, BTCAmountEdit -from .locktimeedit import LockTimeEdit if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -353,6 +350,7 @@ def on_context_menu_for_outputs(self, pos: QPoint): menu.exec_(global_pos) + def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False): try: d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved) @@ -428,9 +426,6 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.export_actions_button.setMenu(export_actions_menu) self.export_actions_button.setPopupMode(QToolButton.InstantPopup) - self.finalize_button = QPushButton(_('Finalize')) - self.finalize_button.clicked.connect(self.on_finalize) - partial_tx_actions_menu = QMenu() ptx_merge_sigs_action = QAction(_("Merge signatures from"), self) ptx_merge_sigs_action.triggered.connect(self.merge_sigs) @@ -447,11 +442,11 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz # Action buttons self.buttons = [self.partial_tx_actions_button, self.sign_button, self.broadcast_button, self.cancel_button] # Transaction sharing buttons - self.sharing_buttons = [self.finalize_button, self.export_actions_button, self.save_button] + self.sharing_buttons = [self.export_actions_button, self.save_button] run_hook('transaction_dialog', self) - if not self.finalized: - self.create_fee_controls() - vbox.addWidget(self.feecontrol_fields) + #if not self.finalized: + # self.create_fee_controls() + # vbox.addWidget(self.feecontrol_fields) self.hbox = hbox = QHBoxLayout() hbox.addLayout(Buttons(*self.sharing_buttons)) hbox.addStretch(1) @@ -464,8 +459,6 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz def set_buttons_visibility(self): for b in [self.export_actions_button, self.save_button, self.sign_button, self.broadcast_button, self.partial_tx_actions_button]: b.setVisible(self.finalized) - for b in [self.finalize_button]: - b.setVisible(not self.finalized) def set_tx(self, tx: 'Transaction'): # Take a copy; it might get updated in the main window by @@ -659,9 +652,6 @@ def join_tx_with_another(self): self.update() def update(self): - if not self.finalized: - self.update_fee_fields() - self.finalize_button.setEnabled(self.can_finalize()) if self.tx is None: return self.io_widget.update(self.tx) @@ -723,8 +713,7 @@ def update(self): else: locktime_final_str = f"LockTime: {self.tx.locktime} ({datetime.datetime.fromtimestamp(self.tx.locktime)})" self.locktime_final_label.setText(locktime_final_str) - if self.locktime_e.get_locktime() is None: - self.locktime_e.set_locktime(self.tx.locktime) + self.rbf_label.setText(_('Replace by fee') + f": {not self.tx.is_final()}") if tx_mined_status.header_hash: @@ -768,10 +757,7 @@ def update(self): fee_rate = Decimal(fee) / size # sat/byte fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000) if isinstance(self.tx, PartialTransaction): - if isinstance(self, PreviewTxDialog): - invoice_amt = self.tx.output_value() if self.output_value == '!' else self.output_value - else: - invoice_amt = amount + invoice_amt = amount fee_warning_tuple = self.wallet.get_tx_fee_warning( invoice_amt=invoice_amt, tx_size=size, fee=fee) if fee_warning_tuple: @@ -865,19 +851,6 @@ def add_tx_stats(self, vbox): self.locktime_final_label = TxDetailLabel() vbox_right.addWidget(self.locktime_final_label) - locktime_setter_hbox = QHBoxLayout() - locktime_setter_hbox.setContentsMargins(0, 0, 0, 0) - locktime_setter_hbox.setSpacing(0) - locktime_setter_label = TxDetailLabel() - locktime_setter_label.setText("LockTime: ") - self.locktime_e = LockTimeEdit(self) - locktime_setter_hbox.addWidget(locktime_setter_label) - locktime_setter_hbox.addWidget(self.locktime_e) - locktime_setter_hbox.addStretch(1) - self.locktime_setter_widget = QWidget() - self.locktime_setter_widget.setLayout(locktime_setter_hbox) - vbox_right.addWidget(self.locktime_setter_widget) - self.block_height_label = TxDetailLabel() vbox_right.addWidget(self.block_height_label) vbox_right.addStretch(1) @@ -892,7 +865,6 @@ def add_tx_stats(self, vbox): # set visibility after parenting can be determined by Qt self.rbf_label.setVisible(self.finalized) self.locktime_final_label.setVisible(self.finalized) - self.locktime_setter_widget.setVisible(not self.finalized) def set_title(self): self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction")) @@ -947,228 +919,3 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if self.update() -class PreviewTxDialog(BaseTxDialog, TxEditor): - - def __init__( - self, - *, - make_tx, - external_keypairs, - window: 'ElectrumWindow', - output_value: Union[int, str], - ): - TxEditor.__init__( - self, - window=window, - make_tx=make_tx, - is_sweep=bool(external_keypairs), - output_value=output_value, - ) - BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False, - finalized=False, external_keypairs=external_keypairs) - BlockingWaitingDialog(window, _("Preparing transaction..."), - lambda: self.update_tx(fallback_to_zero_fee=True)) - self.update() - - def create_fee_controls(self): - - self.size_e = TxSizeLabel() - self.size_e.setAlignment(Qt.AlignCenter) - self.size_e.setAmount(0) - self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) - - self.fiat_fee_label = TxFiatLabel() - self.fiat_fee_label.setAlignment(Qt.AlignCenter) - self.fiat_fee_label.setAmount(0) - self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) - - self.feerate_e = FeerateEdit(lambda: 0) - self.feerate_e.setAmount(self.config.fee_per_byte()) - self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False)) - self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True)) - - self.fee_e = BTCAmountEdit(self.main_window.get_decimal_point) - self.fee_e.textEdited.connect(partial(self.on_fee_or_feerate, self.fee_e, False)) - self.fee_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.fee_e, True)) - - self.fee_e.textChanged.connect(self.entry_changed) - self.feerate_e.textChanged.connect(self.entry_changed) - - self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback) - self.fee_combo = FeeComboBox(self.fee_slider) - self.fee_slider.setFixedWidth(self.fee_e.width()) - - def feerounding_onclick(): - text = (self.feerounding_text + '\n\n' + - _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + - _('At most 100 satoshis might be lost due to this rounding.') + ' ' + - _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' + - _('Also, dust is not kept as change, but added to the fee.') + '\n' + - _('Also, when batching RBF transactions, BIP 125 imposes a lower bound on the fee.')) - self.show_message(title=_('Fee rounding'), msg=text) - - self.feerounding_icon = QToolButton() - self.feerounding_icon.setIcon(read_QIcon('info.png')) - self.feerounding_icon.setAutoRaise(True) - self.feerounding_icon.clicked.connect(feerounding_onclick) - self.feerounding_icon.setVisible(False) - - self.feecontrol_fields = QWidget() - hbox = QHBoxLayout(self.feecontrol_fields) - hbox.setContentsMargins(0, 0, 0, 0) - grid = QGridLayout() - grid.addWidget(QLabel(_("Target fee:")), 0, 0) - grid.addWidget(self.feerate_e, 0, 1) - grid.addWidget(self.size_e, 0, 2) - grid.addWidget(self.fee_e, 0, 3) - grid.addWidget(self.feerounding_icon, 0, 4) - grid.addWidget(self.fiat_fee_label, 0, 5) - grid.addWidget(self.fee_slider, 1, 1) - grid.addWidget(self.fee_combo, 1, 2) - hbox.addLayout(grid) - hbox.addStretch(1) - - def fee_slider_callback(self, dyn, pos, fee_rate): - super().fee_slider_callback(dyn, pos, fee_rate) - self.fee_slider.activate() - if fee_rate: - fee_rate = Decimal(fee_rate) - self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000)) - else: - self.feerate_e.setAmount(None) - self.fee_e.setModified(False) - - def on_fee_or_feerate(self, edit_changed, editing_finished): - edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e - if editing_finished: - if edit_changed.get_amount() is None: - # This is so that when the user blanks the fee and moves on, - # we go back to auto-calculate mode and put a fee back. - edit_changed.setModified(False) - else: - # edit_changed was edited just now, so make sure we will - # freeze the correct fee setting (this) - edit_other.setModified(False) - self.fee_slider.deactivate() - self.update() - - def is_send_fee_frozen(self): - return self.fee_e.isVisible() and self.fee_e.isModified() \ - and (self.fee_e.text() or self.fee_e.hasFocus()) - - def is_send_feerate_frozen(self): - return self.feerate_e.isVisible() and self.feerate_e.isModified() \ - and (self.feerate_e.text() or self.feerate_e.hasFocus()) - - def set_feerounding_text(self, num_satoshis_added): - self.feerounding_text = (_('Additional {} satoshis are going to be added.') - .format(num_satoshis_added)) - - def get_fee_estimator(self): - if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None: - fee_estimator = self.fee_e.get_amount() - elif self.is_send_feerate_frozen() and self.feerate_e.get_amount() is not None: - amount = self.feerate_e.get_amount() # sat/byte feerate - amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate - fee_estimator = partial( - SimpleConfig.estimate_fee_for_feerate, amount) - else: - fee_estimator = None - return fee_estimator - - def entry_changed(self): - # blue color denotes auto-filled values - text = "" - fee_color = ColorScheme.DEFAULT - feerate_color = ColorScheme.DEFAULT - if self.not_enough_funds: - fee_color = ColorScheme.RED - feerate_color = ColorScheme.RED - elif self.fee_e.isModified(): - feerate_color = ColorScheme.BLUE - elif self.feerate_e.isModified(): - fee_color = ColorScheme.BLUE - else: - fee_color = ColorScheme.BLUE - feerate_color = ColorScheme.BLUE - self.fee_e.setStyleSheet(fee_color.as_stylesheet()) - self.feerate_e.setStyleSheet(feerate_color.as_stylesheet()) - # - self.needs_update = True - - def update_fee_fields(self): - freeze_fee = self.is_send_fee_frozen() - freeze_feerate = self.is_send_feerate_frozen() - tx = self.tx - if self.no_dynfee_estimates and tx: - size = tx.estimated_size() - self.size_e.setAmount(size) - if self.not_enough_funds or self.no_dynfee_estimates: - if not freeze_fee: - self.fee_e.setAmount(None) - if not freeze_feerate: - self.feerate_e.setAmount(None) - self.feerounding_icon.setVisible(False) - return - - assert tx is not None - size = tx.estimated_size() - fee = tx.get_fee() - - self.size_e.setAmount(size) - fiat_fee = self.main_window.format_fiat_and_units(fee) - self.fiat_fee_label.setAmount(fiat_fee) - - # Displayed fee/fee_rate values are set according to user input. - # Due to rounding or dropping dust in CoinChooser, - # actual fees often differ somewhat. - if freeze_feerate or self.fee_slider.is_active(): - displayed_feerate = self.feerate_e.get_amount() - if displayed_feerate is not None: - displayed_feerate = quantize_feerate(displayed_feerate) - elif self.fee_slider.is_active(): - # fallback to actual fee - displayed_feerate = quantize_feerate(fee / size) if fee is not None else None - self.feerate_e.setAmount(displayed_feerate) - displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None - self.fee_e.setAmount(displayed_fee) - else: - if freeze_fee: - displayed_fee = self.fee_e.get_amount() - else: - # fallback to actual fee if nothing is frozen - displayed_fee = fee - self.fee_e.setAmount(displayed_fee) - displayed_fee = displayed_fee if displayed_fee else 0 - displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None - self.feerate_e.setAmount(displayed_feerate) - - # show/hide fee rounding icon - feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0 - self.set_feerounding_text(int(feerounding)) - self.feerounding_icon.setToolTip(self.feerounding_text) - self.feerounding_icon.setVisible(abs(feerounding) >= 1) - - def can_finalize(self): - return (self.tx is not None - and not self.not_enough_funds) - - def on_finalize(self): - if not self.can_finalize(): - return - assert self.tx - self.finalized = True - self.stop_editor_updates() - self.tx.set_rbf(True) - locktime = self.locktime_e.get_locktime() - if locktime is not None: - self.tx.locktime = locktime - for widget in [self.fee_slider, self.fee_combo, self.feecontrol_fields, - self.locktime_setter_widget, self.locktime_e]: - widget.setEnabled(False) - widget.setVisible(False) - for widget in [self.rbf_label, self.locktime_final_label]: - widget.setVisible(True) - self.set_title() - self.set_buttons_visibility() - self.update() diff --git a/electrum/wallet.py b/electrum/wallet.py index f6ef08580..5ea84a1fc 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2770,7 +2770,7 @@ def get_tx_fee_warning( fee: int) -> Optional[Tuple[bool, str, str]]: feerate = Decimal(fee) / tx_size # sat/byte - fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 1 + fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0 long_warning = None short_warning = None allow_send = True From 71697afabd1a378d810154535c15c5c3b422237e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 00:40:46 +0000 Subject: [PATCH 0105/1143] invoices: get_outputs to use .outputs field if available It is wasteful to create new PartialTxOutput objects if we already have an outputs field. Btw apparently lightning invoices too have an outputs field. --- electrum/invoices.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/electrum/invoices.py b/electrum/invoices.py index e7b6c46de..52fbeb68f 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -1,5 +1,5 @@ import time -from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any +from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any, Sequence from decimal import Decimal import attr @@ -126,16 +126,13 @@ def get_address(self) -> Optional[str]: address = self._lnaddr.get_fallback_address() or None return address - def get_outputs(self): - if self.is_lightning(): + def get_outputs(self) -> Sequence[PartialTxOutput]: + outputs = self.outputs or [] + if not outputs: address = self.get_address() amount = self.get_amount_sat() if address and amount is not None: outputs = [PartialTxOutput.from_address_and_value(address, int(amount))] - else: - outputs = [] - else: - outputs = self.outputs return outputs def can_be_paid_onchain(self) -> bool: From ede9b2b37256992c83e35949c09f12b3ee795efa Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Sat, 29 May 2021 20:44:15 +0200 Subject: [PATCH 0106/1143] transaction: cache address determination from output script In order to avoid repeatedly calling get_addr_from_output_script() on every read of the "TxOutput.address" property, determine and cache it only whenever the output script is created/changed. --- electrum/transaction.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index bcf44e8be..425ce9a80 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -148,9 +148,18 @@ def from_legacy_tuple(cls, _type: int, addr: str, val: Union[int, str]) -> Union return cls(scriptpubkey=bfh(addr), value=val) raise Exception(f"unexpected legacy address type: {_type}") + @property + def scriptpubkey(self) -> bytes: + return self._scriptpubkey + + @scriptpubkey.setter + def scriptpubkey(self, scriptpubkey: bytes): + self._scriptpubkey = scriptpubkey + self._address = get_address_from_output_script(scriptpubkey) + @property def address(self) -> Optional[str]: - return get_address_from_output_script(self.scriptpubkey) # TODO cache this? + return self._address def get_ui_address_str(self) -> str: addr = self.address From 1e3f9b942f66f990d96a135fc1bee707a5222823 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 00:45:18 +0000 Subject: [PATCH 0107/1143] qt: MyTreeView.refresh_all to use maybe_defer_update In particular, window.timer_actions() calls request_list.refresh_all() and invoice_list.refresh_all(), every 0.5 seconds. We avoid doing this at least when those lists are not visible anyway. --- electrum/gui/qt/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 8d1e54c13..fce6f4474 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -813,6 +813,8 @@ def find_row_by_key(self, key) -> Optional[int]: return row def refresh_all(self): + if self.maybe_defer_update(): + return for row in range(0, self.std_model.rowCount()): item = self.std_model.item(row, 0) key = item.data(self.key_role) From b20933de6d06fbfc0961eaf505ee898a671cf055 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 01:51:43 +0000 Subject: [PATCH 0108/1143] payserver: better handle running with --offline ``` 19.70 | E | plugin | Plugin error. plugin: payserver, hook: wallet_export_request Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/plugin.py", line 227, in run_hook r = f(*args) File "/home/user/wspace/electrum/electrum/plugins/payserver/payserver.py", line 69, in wallet_export_request d['view_url'] = self.view_url(key) File "/home/user/wspace/electrum/electrum/plugins/payserver/payserver.py", line 55, in view_url return self.server.base_url + self.server.root + '/pay?id=' + key AttributeError: 'NoneType' object has no attribute 'base_url' ``` ``` 23.22 | E | gui.qt.exception_window.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/plugins/payserver/qt.py", line 48, in settings_dialog url = self.server.base_url + self.server.root + '/create_invoice.html' AttributeError: 'NoneType' object has no attribute 'base_url' ``` --- electrum/plugins/payserver/payserver.py | 9 ++++++--- electrum/plugins/payserver/qt.py | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index 7ddbdfcc5..2e45401de 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -26,7 +26,7 @@ import os import asyncio from collections import defaultdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from aiohttp import web from aiorpcx import NetAddress @@ -50,7 +50,9 @@ def __init__(self, parent, config: 'SimpleConfig', name): self.config = config self.server = None - def view_url(self, key): + def view_url(self, key) -> Optional[str]: + if not self.server: + return None return self.server.base_url + self.server.root + '/pay?id=' + key @hook @@ -65,7 +67,8 @@ def daemon_wallet_loaded(self, daemon: 'Daemon', wallet: 'Abstract_Wallet'): @hook def wallet_export_request(self, d, key): - d['view_url'] = self.view_url(key) + if view_url := self.view_url(key): + d['view_url'] = view_url class PayServer(Logger, EventListener): diff --git a/electrum/plugins/payserver/qt.py b/electrum/plugins/payserver/qt.py index 7d43e6115..2e485e62a 100644 --- a/electrum/plugins/payserver/qt.py +++ b/electrum/plugins/payserver/qt.py @@ -42,9 +42,13 @@ def settings_widget(self, window: WindowModalDialog): partial(self.settings_dialog, window)) def settings_dialog(self, window: WindowModalDialog): + if self.config.get('offline'): + window.show_error(_("You are offline.")) + return d = WindowModalDialog(window, _("PayServer Settings")) form = QtWidgets.QFormLayout(None) addr = self.config.get('payserver_address', 'localhost:8080') + assert self.server url = self.server.base_url + self.server.root + '/create_invoice.html' self.help_button = QtWidgets.QPushButton('View sample invoice creation form') self.help_button.clicked.connect(lambda: webopen(url)) From f3b3a40ffeaecf0a069ece3a1768531356d401cf Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 7 Feb 2023 14:16:29 +0100 Subject: [PATCH 0109/1143] qml: add wallet button icon --- electrum/gui/qml/components/AddressDetails.qml | 2 +- electrum/gui/qml/components/Wallets.qml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index a1abc2ff3..20534c45c 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -157,7 +157,7 @@ Pane { } ToolButton { icon.source: '../../icons/share.png' - icon.color: 'transparent' + enabled: modelData onClicked: { var dialog = app.genericShareDialog.createObject(root, { title: qsTr('Public key'), text: modelData } diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 4796274da..39955ac9c 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -113,6 +113,7 @@ Pane { FlatButton { Layout.fillWidth: true text: 'Create Wallet' + icon.source: '../../icons/add.png' onClicked: rootItem.createWallet() } } From 6e8e8798c767299f93290d2c4a25c4eb284b0985 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 8 Feb 2023 11:22:02 +0100 Subject: [PATCH 0110/1143] feerounding_icon: use transparent background, and disable if not visible --- electrum/gui/qt/confirm_tx_dialog.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index ab240050c..ac0d92b56 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -183,9 +183,10 @@ def feerounding_onclick(): self.show_message(title=_('Fee rounding'), msg=text) self.feerounding_icon = QToolButton() - self.feerounding_icon.setIcon(QIcon()) + self.feerounding_icon.setStyleSheet("background-color: rgba(255, 255, 255, 0); ") self.feerounding_icon.setAutoRaise(True) self.feerounding_icon.clicked.connect(feerounding_onclick) + self.set_feerounding_visibility(False) self.fee_hbox = fee_hbox = QHBoxLayout() fee_hbox.addWidget(self.feerate_e) @@ -254,6 +255,11 @@ def set_feerounding_text(self, num_satoshis_added): self.feerounding_text = (_('Additional {} satoshis are going to be added.') .format(num_satoshis_added)) + def set_feerounding_visibility(self, b:bool): + # we do not use setVisible because it affects the layout + self.feerounding_icon.setIcon(read_QIcon('info.png') if b else QIcon()) + self.feerounding_icon.setEnabled(b) + def get_fee_estimator(self): if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None: fee_estimator = self.fee_e.get_amount() @@ -299,7 +305,7 @@ def update_fee_fields(self): self.fee_e.setAmount(None) if not freeze_feerate: self.feerate_e.setAmount(None) - self.feerounding_icon.setIcon(QIcon()) + self.set_feerounding_visibility(False) return assert tx is not None @@ -339,8 +345,7 @@ def update_fee_fields(self): feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0 self.set_feerounding_text(int(feerounding)) self.feerounding_icon.setToolTip(self.feerounding_text) - self.feerounding_icon.setIcon(read_QIcon('info.png') if abs(feerounding) >= 1 else QIcon()) - + self.set_feerounding_visibility(abs(feerounding) >= 1) def create_buttons_bar(self): self.preview_button = QPushButton(_('Preview')) @@ -456,6 +461,7 @@ def update(self): return if not tx: self.toggle_send_button(False) + self.set_feerounding_visibility(False) return self.update_fee_fields() if self.locktime_e.get_locktime() is None: From 53b7cc3f90e4037cb134ac1c5eb0d7684e673305 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 8 Feb 2023 11:34:48 +0100 Subject: [PATCH 0111/1143] use shared prefix for config keys related to tx editor --- electrum/gui/qt/confirm_tx_dialog.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index ac0d92b56..b7edd6aea 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -97,9 +97,9 @@ def __init__(self, *, title='', vbox.addLayout(buttons) self.set_io_visible(self.config.get('show_tx_io', False)) - self.set_fee_edit_visible(self.config.get('show_fee_details', False)) - self.set_locktime_visible(self.config.get('show_locktime', False)) - self.set_preview_visible(self.config.get('show_preview_button', False)) + self.set_fee_edit_visible(self.config.get('show_tx_fee_details', False)) + self.set_locktime_visible(self.config.get('show_tx_locktime', False)) + self.set_preview_visible(self.config.get('show_tx_preview_button', False)) self.update_fee_target() self.resize(self.layout().sizeHint()) @@ -385,20 +385,20 @@ def toggle_io_visibility(self): self.setFixedSize(self.layout().sizeHint()) def toggle_fee_details(self): - b = not self.config.get('show_fee_details', False) - self.config.set_key('show_fee_details', b) + b = not self.config.get('show_tx_fee_details', False) + self.config.set_key('show_tx_fee_details', b) self.set_fee_edit_visible(b) self.setFixedSize(self.layout().sizeHint()) def toggle_locktime(self): - b = not self.config.get('show_locktime', False) - self.config.set_key('show_locktime', b) + b = not self.config.get('show_tx_locktime', False) + self.config.set_key('show_tx_locktime', b) self.set_locktime_visible(b) self.setFixedSize(self.layout().sizeHint()) def toggle_preview_button(self): - b = not self.config.get('show_preview_button', False) - self.config.set_key('show_preview_button', b) + b = not self.config.get('show_tx_preview_button', False) + self.config.set_key('show_tx_preview_button', b) self.set_preview_visible(b) def set_preview_visible(self, b): From c5883c7cdb6c78fb909d804210544fcf0c0a484e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 8 Feb 2023 12:51:24 +0100 Subject: [PATCH 0112/1143] text guis low hanging fruit --- electrum/gui/stdio.py | 4 ++-- electrum/gui/text.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index 4f30faddb..ef77a8bf5 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -25,7 +25,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): def __init__(self, *, config, daemon, plugins): BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) self.network = daemon.network - storage = WalletStorage(config.get_wallet_path()) + storage = WalletStorage(config.get_wallet_path(use_gui_last_wallet=True)) if not storage.file_exists(): print("Wallet not found. try 'electrum create'") exit() @@ -68,7 +68,7 @@ def on_event_network_updated(self): self.updated() @event_listener - def on_event_banner(self): + def on_event_banner(self, *args): self.print_banner() def main_command(self): diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 4316fa20a..4f08d18fc 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -52,7 +52,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) self.network = daemon.network - storage = WalletStorage(config.get_wallet_path()) + storage = WalletStorage(config.get_wallet_path(use_gui_last_wallet=True)) if not storage.file_exists(): print("Wallet not found. try 'electrum create'") exit() From 1d00b56b64eef3107abb02f2441752c0eb757af9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 8 Feb 2023 13:28:03 +0100 Subject: [PATCH 0113/1143] make ConfirmTxDialog resizeable --- electrum/gui/qt/confirm_tx_dialog.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index b7edd6aea..164595f49 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -377,24 +377,29 @@ def create_top_bar(self, text): hbox.addWidget(self.pref_button) return hbox + def resize_to_fit_content(self): + # fixme: calling resize once is not enough... + size = self.layout().sizeHint() + self.resize(size) + self.resize(size) + def toggle_io_visibility(self): b = not self.config.get('show_tx_io', False) self.config.set_key('show_tx_io', b) self.set_io_visible(b) - #self.resize(self.layout().sizeHint()) - self.setFixedSize(self.layout().sizeHint()) + self.resize_to_fit_content() def toggle_fee_details(self): b = not self.config.get('show_tx_fee_details', False) self.config.set_key('show_tx_fee_details', b) self.set_fee_edit_visible(b) - self.setFixedSize(self.layout().sizeHint()) + self.resize_to_fit_content() def toggle_locktime(self): b = not self.config.get('show_tx_locktime', False) self.config.set_key('show_tx_locktime', b) self.set_locktime_visible(b) - self.setFixedSize(self.layout().sizeHint()) + self.resize_to_fit_content() def toggle_preview_button(self): b = not self.config.get('show_tx_preview_button', False) @@ -448,7 +453,6 @@ def toggle_send_button(self, enable: bool, *, message: str = None): self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) self.message_label.setText(message) - self.setFixedSize(self.layout().sizeHint()) self.preview_button.setEnabled(enable) self.ok_button.setEnabled(enable) From 6cb6531fd9849e319cfc27157a89c41484c1395f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 8 Feb 2023 17:12:20 +0100 Subject: [PATCH 0114/1143] qml: add swap progress dialog --- electrum/gui/qml/components/Channels.qml | 29 +++- electrum/gui/qml/components/SwapDialog.qml | 24 +--- .../gui/qml/components/SwapProgressDialog.qml | 124 ++++++++++++++++++ electrum/gui/qml/qeswaphelper.py | 19 ++- 4 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 electrum/gui/qml/components/SwapProgressDialog.qml diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index ea299d329..85b43ee7a 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -112,7 +112,7 @@ Pane { visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 icon.source: '../../icons/status_waiting.png' onClicked: { - var dialog = swapDialog.createObject(root) + var dialog = swapDialog.createObject(root, { swaphelper: swaphelper }) dialog.open() } } @@ -141,6 +141,26 @@ Pane { } + SwapHelper { + id: swaphelper + wallet: Daemon.currentWallet + onConfirm: { + var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) + dialog.yesClicked.connect(function() { + dialog.close() + swaphelper.executeSwap(true) + }) + dialog.open() + } + onAuthRequired: { + app.handleAuthRequired(swaphelper, method) + } + onSwapStarted: { + var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + dialog.open() + } + } + Component { id: swapDialog SwapDialog { @@ -148,6 +168,13 @@ Pane { } } + Component { + id: swapProgressDialog + SwapProgressDialog { + onClosed: destroy() + } + } + Component { id: openChannelDialog OpenChannelDialog { diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 32eeaed17..abebaa049 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -10,6 +10,8 @@ import "controls" ElDialog { id: root + required property QtObject swaphelper + width: parent.width height: parent.height @@ -194,24 +196,10 @@ ElDialog { } } - SwapHelper { - id: swaphelper - wallet: Daemon.currentWallet - onError: { - var dialog = app.messageDialog.createObject(app, {'text': message}) - dialog.open() - } - onConfirm: { - var dialog = app.messageDialog.createObject(app, {'text': message, 'yesno': true}) - dialog.yesClicked.connect(function() { - dialog.close() - swaphelper.executeSwap(true) - }) - dialog.open() - } - onAuthRequired: { - app.handleAuthRequired(swaphelper, method) + Connections { + target: swaphelper + function onSwapStarted() { + root.close() } - onSwapStarted: root.close() // TODO: show swap progress monitor } } diff --git a/electrum/gui/qml/components/SwapProgressDialog.qml b/electrum/gui/qml/components/SwapProgressDialog.qml new file mode 100644 index 000000000..5cc9feea3 --- /dev/null +++ b/electrum/gui/qml/components/SwapProgressDialog.qml @@ -0,0 +1,124 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.14 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +ElDialog { + id: dialog + + required property QtObject swaphelper + + width: parent.width + height: parent.height + + title: swaphelper.isReverse + ? qsTr('Reverse swap...') + : qsTr('Swap...') + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + Item { + id: s + state: '' + states: [ + State { + name: '' + }, + State { + name: 'success' + PropertyChanges { target: spinner; running: false } + PropertyChanges { target: helpText; text: qsTr('Success') } + PropertyChanges { target: icon; source: '../../icons/confirmed.png' } + }, + State { + name: 'failed' + PropertyChanges { target: spinner; running: false } + PropertyChanges { target: helpText; text: qsTr('Failed') } + PropertyChanges { target: errorText; visible: true } + PropertyChanges { target: icon; source: '../../icons/warning.png' } + } + ] + transitions: [ + Transition { + from: '' + to: 'success' + PropertyAnimation { target: helpText; properties: 'text'; duration: 0} + NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 200 } + NumberAnimation { target: icon; properties: 'scale'; from: 0; to: 1; duration: 500 + easing.type: Easing.OutBack + easing.overshoot: 10 + } + }, + Transition { + from: '' + to: 'failed' + PropertyAnimation { target: helpText; properties: 'text'; duration: 0} + NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 500 } + } + ] + } + + ColumnLayout { + id: content + anchors.centerIn: parent + width: parent.width + + Item { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: constants.iconSizeXXLarge + Layout.preferredHeight: constants.iconSizeXXLarge + + BusyIndicator { + id: spinner + visible: s.state == '' + width: constants.iconSizeXXLarge + height: constants.iconSizeXXLarge + } + + Image { + id: icon + width: constants.iconSizeXXLarge + height: constants.iconSizeXXLarge + } + } + + Label { + id: helpText + Layout.alignment: Qt.AlignHCenter + text: qsTr('Performing swap...') + font.pixelSize: constants.fontSizeXXLarge + } + + Label { + id: errorText + Layout.preferredWidth: parent.width + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + } + } + + Connections { + target: swaphelper + function onSwapSuccess() { + console.log('swap succeeded!') + s.state = 'success' + } + function onSwapFailed(message) { + console.log('swap failed: ' + message) + s.state = 'failed' + if (message) + errorText.text = message + } + } + +} diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 4c38e2c8a..2924809d6 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -18,9 +18,10 @@ class QESwapHelper(AuthMixin, QObject): _logger = get_logger(__name__) - error = pyqtSignal([str], arguments=['message']) confirm = pyqtSignal([str], arguments=['message']) swapStarted = pyqtSignal() + swapSuccess = pyqtSignal() + swapFailed = pyqtSignal([str], arguments=['message']) def __init__(self, parent=None): super().__init__(parent) @@ -302,10 +303,12 @@ def do_normal_swap(self, lightning_amount, onchain_amount): def swap_task(): try: fut = asyncio.run_coroutine_threadsafe(coro, loop) - result = fut.result() + self.swapStarted.emit() + txid = fut.result() + self.swapSuccess.emit() except Exception as e: self._logger.error(str(e)) - self.error.emit(str(e)) + self.swapFailed.emit(str(e)) threading.Thread(target=swap_task).start() @@ -322,10 +325,15 @@ def do_reverse_swap(self, lightning_amount, onchain_amount): def swap_task(): try: fut = asyncio.run_coroutine_threadsafe(coro, loop) - result = fut.result() + self.swapStarted.emit() + success = fut.result() + if success: + self.swapSuccess.emit() + else: + self.swapFailed.emit('') except Exception as e: self._logger.error(str(e)) - self.error.emit(str(e)) + self.swapFailed.emit(str(e)) threading.Thread(target=swap_task).start() @@ -356,4 +364,3 @@ def _do_execute_swap(self): lightning_amount = self._receive_amount onchain_amount = self._send_amount self.do_normal_swap(lightning_amount, onchain_amount) - self.swapStarted.emit() From 6b8f9f8fe24aaf6a4532411dfe6f64abfc6d6bad Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 8 Feb 2023 17:31:08 +0100 Subject: [PATCH 0115/1143] qml: allow ln node connect string without port, default to 9735 --- electrum/gui/qml/qechannelopener.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 5d0e71493..9b2947e7a 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -135,7 +135,11 @@ def validate_nodeid(self, nodeid): def nodeid_to_lnpeer(self, nodeid): node_pubkey, host_port = extract_nodeid(nodeid) - host, port = host_port.split(':',1) + if host_port.__contains__(':'): + host, port = host_port.split(':',1) + else: + host = host_port + port = 9735 return LNPeerAddr(host, int(port), node_pubkey) # FIXME "max" button in amount_dialog should enforce LN_MAX_FUNDING_SAT From 90f1279d9ae2739ae5bc22a294c5c9169fefc00f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 23:08:01 +0000 Subject: [PATCH 0116/1143] addr_sync: (trivial) don't use private utxo._is_coinbase_output --- electrum/address_synchronizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 62339bbdb..ccc9606dd 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -845,14 +845,14 @@ def get_balance(self, domain, *, excluded_addresses: Set[str] = None, c = u = x = 0 mempool_height = self.get_local_height() + 1 # height of next block - for utxo in coins.values(): + for utxo in coins.values(): # type: PartialTxInput if utxo.spent_height is not None: continue if utxo.prevout.to_str() in excluded_coins: continue v = utxo.value_sats() tx_height = utxo.block_height - is_cb = utxo._is_coinbase_output + is_cb = utxo.is_coinbase_output() if is_cb and tx_height + COINBASE_MATURITY > mempool_height: x += v elif tx_height > 0: From 8a53a3201c169d0d21b5eaac3b4c9e7c726b1bb2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 23:30:48 +0000 Subject: [PATCH 0117/1143] wallet.try_detecting_internal_addresses_corruption: check more addrs related https://github.com/spesmilo/electrum/issues/8202 For a HD wallet, instead of checking the first 10 addrs + 10 additional random ones, we now check the first 10 addrs + 10 random used addrs + 10 random unused addrs. Checking unused addresses is useful to prevent getting money sent there, and checking used addresses is useful to inform people of already lost money. --- electrum/wallet.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index f6ef08580..c90ace5b0 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -3085,9 +3085,14 @@ def get_txin_type(self, address): @profiler def try_detecting_internal_addresses_corruption(self): # we check only a random sample, for performance - addresses = self.get_addresses() - addresses = random.sample(addresses, min(len(addresses), 10)) - for addr_found in addresses: + addresses_all = self.get_addresses() + # some random *used* addresses (note: we likely have not synced yet) + addresses_used = [addr for addr in addresses_all if self.adb.is_used(addr)] + sample1 = random.sample(addresses_used, min(len(addresses_used), 10)) + # some random *unused* addresses + addresses_unused = [addr for addr in addresses_all if not self.adb.is_used(addr)] + sample2 = random.sample(addresses_unused, min(len(addresses_unused), 10)) + for addr_found in itertools.chain(sample1, sample2): self.check_address_for_corruption(addr_found) def check_address_for_corruption(self, addr): @@ -3167,12 +3172,16 @@ def get_change_addresses(self, *, slice_start=None, slice_stop=None): @profiler def try_detecting_internal_addresses_corruption(self): addresses_all = self.get_addresses() - # sample 1: first few - addresses_sample1 = addresses_all[:10] - # sample2: a few more randomly selected - addresses_rand = addresses_all[10:] - addresses_sample2 = random.sample(addresses_rand, min(len(addresses_rand), 10)) - for addr_found in itertools.chain(addresses_sample1, addresses_sample2): + # first few addresses + nfirst_few = 10 + sample1 = addresses_all[:nfirst_few] + # some random *used* addresses (note: we likely have not synced yet) + addresses_used = [addr for addr in addresses_all[nfirst_few:] if self.adb.is_used(addr)] + sample2 = random.sample(addresses_used, min(len(addresses_used), 10)) + # some random *unused* addresses + addresses_unused = [addr for addr in addresses_all[nfirst_few:] if not self.adb.is_used(addr)] + sample3 = random.sample(addresses_unused, min(len(addresses_unused), 10)) + for addr_found in itertools.chain(sample1, sample2, sample3): self.check_address_for_corruption(addr_found) def check_address_for_corruption(self, addr): From ad0b853cd98b040565c18f5403da365221f311bd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 23:36:36 +0000 Subject: [PATCH 0118/1143] invoices: improve perf by caching lnaddr even earlier During wallet-open, we load all invoices/payreqs. This involved decoding the lnaddrs twice. Now we only decode once. For a wallet with ~1000 payreqs, this noticeably sped up wallet-open: (before:) 8.83 | D | util.profiler | Daemon._load_wallet 6.4317 sec (after:) 5.69 | D | util.profiler | Daemon._load_wallet 3.4450 sec It is very expensive to parse all the lnaddrs... --- electrum/daemon.py | 1 + electrum/invoices.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index b0d04f40c..ccedd9248 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -463,6 +463,7 @@ def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstr return wallet @staticmethod + @profiler def _load_wallet( path, password, diff --git a/electrum/invoices.py b/electrum/invoices.py index 52fbeb68f..0933cba6c 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -196,7 +196,8 @@ def get_bip21_URI(self, *, include_lightning: bool = False) -> Optional[str]: @lightning_invoice.validator def _validate_invoice_str(self, attribute, value): if value is not None: - lndecode(value) # this checks the str can be decoded + lnaddr = lndecode(value) # this checks the str can be decoded + self.__lnaddr = lnaddr # save it, just to avoid having to recompute later @amount_msat.validator def _validate_amount(self, attribute, value): From 227ccc65d4cb8586a9699abbe0bdf1d920b1d536 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 23:37:11 +0000 Subject: [PATCH 0119/1143] perf: load matplotlib on-demand it takes ~1.7 seconds to import electrum.plot, slowing down app-startup considerably --- electrum/gui/qt/history_list.py | 15 +++++++-------- electrum/plot.py | 2 ++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 374bb84c4..38e3dbd3e 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -61,12 +61,6 @@ _logger = get_logger(__name__) -try: - from electrum.plot import plot_history, NothingToPlotException -except: - _logger.info("could not import electrum.plot. This feature needs matplotlib to be installed.") - plot_history = None - # note: this list needs to be kept in sync with another in kivy TX_ICONS = [ "unconfirmed.png", @@ -631,10 +625,15 @@ def show_summary(self): d.exec_() def plot_history_dialog(self): - if plot_history is None: + try: + from electrum.plot import plot_history, NothingToPlotException + except Exception as e: + _logger.error(f"could not import electrum.plot. This feature needs matplotlib to be installed. exc={e!r}") self.parent.show_message( _("Can't plot history.") + '\n' + - _("Perhaps some dependencies are missing...") + " (matplotlib?)") + _("Perhaps some dependencies are missing...") + " (matplotlib?)" + '\n' + + f"Error: {e!r}" + ) return try: plt = plot_history(list(self.hm.transactions.values())) diff --git a/electrum/plot.py b/electrum/plot.py index a22a1d9f8..1b27bf5d9 100644 --- a/electrum/plot.py +++ b/electrum/plot.py @@ -1,3 +1,5 @@ +# note: This module takes 1-2 seconds to import. It should be imported *on-demand*. + import datetime from collections import defaultdict From 72d750c51c1e4c0d0217454eaa93aa254ec2c8e2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 8 Feb 2023 23:37:51 +0000 Subject: [PATCH 0120/1143] plot history: also include lightning items ``` 6.15 | E | gui.qt.exception_window.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/history_list.py", line 637, in plot_history_dialog plt = plot_history(list(self.hm.transactions.values())) File "/home/user/wspace/electrum/electrum/plot.py", line 24, in plot_history if not item['confirmations']: KeyError: 'confirmations' ``` --- electrum/plot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/plot.py b/electrum/plot.py index 1b27bf5d9..42bdee0c9 100644 --- a/electrum/plot.py +++ b/electrum/plot.py @@ -23,7 +23,8 @@ def plot_history(history): hist_in = defaultdict(int) hist_out = defaultdict(int) for item in history: - if not item['confirmations']: + is_lightning = item.get("lightning", False) + if not is_lightning and not item['confirmations']: continue if item['timestamp'] is None: continue From 1e60cb740ff15ddad1a344b861e5a19706a14cbb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Feb 2023 01:13:05 +0100 Subject: [PATCH 0121/1143] qml: fixes TextHighlightPane --- .../gui/qml/components/AddressDetails.qml | 9 +- .../gui/qml/components/ChannelDetails.qml | 2 - .../gui/qml/components/CloseChannelDialog.qml | 2 - .../gui/qml/components/ConfirmTxDialog.qml | 8 +- electrum/gui/qml/components/Constants.qml | 1 + electrum/gui/qml/components/InvoiceDialog.qml | 97 +++++++++++++------ .../components/LightningPaymentDetails.qml | 8 -- .../gui/qml/components/RbfBumpFeeDialog.qml | 3 +- .../gui/qml/components/RbfCancelDialog.qml | 6 +- electrum/gui/qml/components/TxDetails.qml | 7 +- .../components/controls/TextHighlightPane.qml | 11 ++- .../components/wizard/WCCosignerKeystore.qml | 8 +- .../qml/components/wizard/WCHaveMasterKey.qml | 8 +- .../gui/qml/components/wizard/WCHaveSeed.qml | 5 +- .../components/wizard/WCShowMasterPubkey.qml | 2 - 15 files changed, 93 insertions(+), 84 deletions(-) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index 20534c45c..b07d4fd99 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -22,7 +22,7 @@ Pane { spacing: 0 Flickable { - Layout.preferredWidth: parent.width + Layout.fillWidth: true Layout.fillHeight: true leftMargin: constants.paddingLarge @@ -48,8 +48,6 @@ Pane { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -86,8 +84,6 @@ Pane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -144,8 +140,7 @@ Pane { delegate: TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + RowLayout { width: parent.width Label { diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 22d5b75f3..c9dc39778 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -175,8 +175,6 @@ Pane { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index ec1bbfe71..24631ef3c 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -71,8 +71,6 @@ ElDialog { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall Label { width: parent.width diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 5b66966dd..c4fce4b44 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -49,11 +49,12 @@ ElDialog { spacing: 0 GridLayout { - width: parent.width - columns: 2 + Layout.fillWidth: true Layout.leftMargin: constants.paddingLarge Layout.rightMargin: constants.paddingLarge + columns: 2 + Label { id: amountLabel text: qsTr('Amount to send') @@ -181,8 +182,7 @@ ElDialog { delegate: TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + RowLayout { width: parent.width Label { diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 94d17d4cf..d4a24e6e3 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -37,6 +37,7 @@ Item { property color colorMine: "yellow" property color colorError: '#ffff8080' + property color colorWarning: 'yellow' property color colorLightningLocal: "blue" property color colorLightningRemote: "yellow" property color colorChannelOpen: "#ff80ff80" diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index a36db7c6a..f089f2e4c 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -50,6 +50,28 @@ ElDialog { columns: 2 + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + visible: invoice.userinfo + borderColor: constants.colorWarning + padding: constants.paddingXLarge + + RowLayout { + Image { + source: '../../icons/warning.png' + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium + } + Label { + width: parent.width + text: invoice.userinfo + wrapMode: Text.Wrap + } + } + } + Label { text: qsTr('Type') color: Material.accentColor @@ -92,12 +114,10 @@ ElDialog { } TextHighlightPane { - visible: invoice.invoiceType == Invoice.OnchainInvoice - Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 + visible: invoice.invoiceType == Invoice.OnchainInvoice leftPadding: constants.paddingMedium Label { @@ -115,19 +135,32 @@ ElDialog { } TextHighlightPane { - visible: invoice.invoiceType == Invoice.LightningInvoice - Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 + visible: invoice.invoiceType == Invoice.LightningInvoice leftPadding: constants.paddingMedium - Label { + RowLayout { width: parent.width - text: 'pubkey' in invoice.lnprops ? invoice.lnprops.pubkey : '' - font.family: FixedFont - wrapMode: Text.Wrap + Label { + id: pubkeyLabel + Layout.fillWidth: true + text: 'pubkey' in invoice.lnprops ? invoice.lnprops.pubkey : '' + font.family: FixedFont + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + enabled: pubkeyLabel.text + onClicked: { + var dialog = app.genericShareDialog.createObject(app, + { title: qsTr('Node public key'), text: invoice.lnprops.pubkey } + ) + dialog.open() + } + } } } @@ -138,19 +171,32 @@ ElDialog { } TextHighlightPane { - visible: invoice.invoiceType == Invoice.LightningInvoice - Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 + visible: invoice.invoiceType == Invoice.LightningInvoice leftPadding: constants.paddingMedium - Label { + RowLayout { width: parent.width - text: 'payment_hash' in invoice.lnprops ? invoice.lnprops.payment_hash : '' - font.family: FixedFont - wrapMode: Text.Wrap + Label { + id: paymenthashLabel + Layout.fillWidth: true + text: 'payment_hash' in invoice.lnprops ? invoice.lnprops.payment_hash : '' + font.family: FixedFont + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + enabled: paymenthashLabel.text + onClicked: { + var dialog = app.genericShareDialog.createObject(app, + { title: qsTr('Payment hash'), text: invoice.lnprops.payment_hash } + ) + dialog.open() + } + } } } @@ -162,13 +208,10 @@ ElDialog { } TextHighlightPane { - visible: invoice.message - Layout.columnSpan: 2 Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - padding: 0 + visible: invoice.message leftPadding: constants.paddingMedium Label { @@ -193,8 +236,7 @@ ElDialog { Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter - padding: 0 - leftPadding: constants.paddingXXLarge + leftPadding: constants.paddingXLarge property bool editmode: false @@ -325,15 +367,6 @@ ElDialog { } - Item { Layout.preferredHeight: constants.paddingLarge; Layout.preferredWidth: 1 } - - InfoTextArea { - Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: parent.width * 3/4 - visible: invoice.userinfo - text: invoice.userinfo - } } } diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index 05da7a5b6..2a120d19a 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -92,8 +92,6 @@ Pane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -148,8 +146,6 @@ Pane { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -182,8 +178,6 @@ Pane { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -216,8 +210,6 @@ Pane { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 3bc7045d3..099fc5b3f 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -197,8 +197,7 @@ ElDialog { delegate: TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + RowLayout { width: parent.width Label { diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index b9643bb44..06dd9b7d9 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -32,9 +32,10 @@ ElDialog { spacing: 0 GridLayout { - Layout.preferredWidth: parent.width + Layout.fillWidth: true Layout.leftMargin: constants.paddingLarge Layout.rightMargin: constants.paddingLarge + columns: 2 Label { @@ -169,8 +170,7 @@ ElDialog { delegate: TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + RowLayout { width: parent.width Label { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 15edf83f1..4bc013f21 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -197,8 +197,6 @@ Pane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -253,8 +251,6 @@ Pane { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width @@ -290,8 +286,7 @@ Pane { delegate: TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + RowLayout { width: parent.width Label { diff --git a/electrum/gui/qml/components/controls/TextHighlightPane.qml b/electrum/gui/qml/components/controls/TextHighlightPane.qml index 7d715af2f..9b5349879 100644 --- a/electrum/gui/qml/components/controls/TextHighlightPane.qml +++ b/electrum/gui/qml/components/controls/TextHighlightPane.qml @@ -4,13 +4,14 @@ import QtQuick.Controls 2.0 import QtQuick.Controls.Material 2.0 Pane { - topPadding: constants.paddingSmall - bottomPadding: constants.paddingSmall - leftPadding: constants.paddingSmall - rightPadding: constants.paddingSmall + padding: constants.paddingSmall + + property color backgroundColor: Qt.lighter(Material.background, 1.15) + property color borderColor: null background: Rectangle { - color: Qt.lighter(Material.background, 1.15) + color: backgroundColor + border.color: borderColor ? borderColor : backgroundColor radius: constants.paddingSmall } } diff --git a/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml index e16237b2d..94e5895b4 100644 --- a/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml +++ b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml @@ -30,17 +30,17 @@ WizardComponent { width: parent.width Label { + Layout.fillWidth: true + visible: cosigner text: qsTr('Here is your master public key. Please share it with your cosigners') - Layout.fillWidth: true wrapMode: Text.Wrap } TextHighlightPane { - visible: cosigner Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + + visible: cosigner RowLayout { width: parent.width diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index 4cfa74f98..b35b8df07 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -54,17 +54,17 @@ WizardComponent { width: parent.width Label { + Layout.fillWidth: true + visible: cosigner text: qsTr('Here is your master public key. Please share it with your cosigners') - Layout.fillWidth: true wrapMode: Text.Wrap } TextHighlightPane { - visible: cosigner Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + + visible: cosigner RowLayout { width: parent.width diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index ca08de3e2..d8e7ed272 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -93,11 +93,10 @@ WizardComponent { } TextHighlightPane { - visible: cosigner Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + + visible: cosigner RowLayout { width: parent.width diff --git a/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml b/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml index 6dae7ea8a..f5e0497e3 100644 --- a/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml +++ b/electrum/gui/qml/components/wizard/WCShowMasterPubkey.qml @@ -22,8 +22,6 @@ WizardComponent { TextHighlightPane { Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall RowLayout { width: parent.width From c9b6917ab7af5792e968c7fc7047438c92f6883b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Feb 2023 01:17:13 +0100 Subject: [PATCH 0122/1143] qml: Addresses fills page --- electrum/gui/qml/components/Addresses.qml | 191 +++++++++++----------- 1 file changed, 93 insertions(+), 98 deletions(-) diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index f0e9e05bd..280239334 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -10,128 +10,123 @@ import "controls" Pane { id: rootItem padding: 0 - width: parent.width ColumnLayout { id: layout - width: parent.width - height: parent.height + anchors.fill: parent - Item { - width: parent.width + ListView { + id: listview + + Layout.fillWidth: true Layout.fillHeight: true - ListView { - id: listview - width: parent.width - height: parent.height - clip: true - model: Daemon.currentWallet.addressModel - currentIndex: -1 - - section.property: 'type' - section.criteria: ViewSection.FullString - section.delegate: sectionDelegate - - delegate: ItemDelegate { - id: delegate - width: ListView.view.width - height: delegateLayout.height - highlighted: ListView.isCurrentItem - - font.pixelSize: constants.fontSizeMedium // set default font size for child controls - - onClicked: { - var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {'address': model.address}) - page.addressDetailsChanged.connect(function() { - // update listmodel when details change - listview.model.update_address(model.address) - }) - } + clip: true + model: Daemon.currentWallet.addressModel + currentIndex: -1 + + section.property: 'type' + section.criteria: ViewSection.FullString + section.delegate: sectionDelegate + + delegate: ItemDelegate { + id: delegate + width: ListView.view.width + height: delegateLayout.height + highlighted: ListView.isCurrentItem - ColumnLayout { - id: delegateLayout - width: parent.width - spacing: 0 + font.pixelSize: constants.fontSizeMedium // set default font size for child controls - GridLayout { - columns: 2 - Layout.topMargin: constants.paddingSmall - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge + onClicked: { + var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {'address': model.address}) + page.addressDetailsChanged.connect(function() { + // update listmodel when details change + listview.model.update_address(model.address) + }) + } + + ColumnLayout { + id: delegateLayout + width: parent.width + spacing: 0 + + GridLayout { + columns: 2 + Layout.topMargin: constants.paddingSmall + Layout.leftMargin: constants.paddingLarge + Layout.rightMargin: constants.paddingLarge + + Label { + id: indexLabel + font.bold: true + text: '#' + ('00'+model.iaddr).slice(-2) + Layout.fillWidth: true + } + Label { + font.family: FixedFont + text: model.address + elide: Text.ElideMiddle + Layout.fillWidth: true + } + + Rectangle { + id: useIndicator + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium + color: model.held + ? constants.colorAddressFrozen + : model.numtx > 0 + ? model.balance.satsInt == 0 + ? constants.colorAddressUsed + : constants.colorAddressUsedWithBalance + : model.type == 'receive' + ? constants.colorAddressExternal + : constants.colorAddressInternal + } + RowLayout { Label { - id: indexLabel - font.bold: true - text: '#' + ('00'+model.iaddr).slice(-2) + id: labelLabel + font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall + text: model.label != '' ? model.label : '' + opacity: model.label != '' ? 1.0 : 0.8 + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.WordWrap Layout.fillWidth: true } Label { font.family: FixedFont - text: model.address - elide: Text.ElideMiddle - Layout.fillWidth: true + text: Config.formatSats(model.balance, false) + visible: model.balance.satsInt != 0 } - - Rectangle { - id: useIndicator - Layout.preferredWidth: constants.iconSizeMedium - Layout.preferredHeight: constants.iconSizeMedium - color: model.held - ? constants.colorAddressFrozen - : model.numtx > 0 - ? model.balance.satsInt == 0 - ? constants.colorAddressUsed - : constants.colorAddressUsedWithBalance - : model.type == 'receive' - ? constants.colorAddressExternal - : constants.colorAddressInternal + Label { + color: Material.accentColor + text: Config.baseUnit + ',' + visible: model.balance.satsInt != 0 } - - RowLayout { - Label { - id: labelLabel - font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall - text: model.label != '' ? model.label : '' - opacity: model.label != '' ? 1.0 : 0.8 - elide: Text.ElideRight - maximumLineCount: 2 - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - Label { - font.family: FixedFont - text: Config.formatSats(model.balance, false) - visible: model.balance.satsInt != 0 - } - Label { - color: Material.accentColor - text: Config.baseUnit + ',' - visible: model.balance.satsInt != 0 - } - Label { - text: model.numtx - visible: model.numtx > 0 - } - Label { - color: Material.accentColor - text: qsTr('tx') - visible: model.numtx > 0 - } + Label { + text: model.numtx + visible: model.numtx > 0 + } + Label { + color: Material.accentColor + text: qsTr('tx') + visible: model.numtx > 0 } } + } - Item { - Layout.preferredWidth: 1 - Layout.preferredHeight: constants.paddingSmall - } + Item { + Layout.preferredWidth: 1 + Layout.preferredHeight: constants.paddingSmall } } - - ScrollIndicator.vertical: ScrollIndicator { } } + ScrollIndicator.vertical: ScrollIndicator { } } + } Component { From 11439fb3fdeb96959dbbe8a5a15d33e1dd0743af Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Feb 2023 01:34:54 +0100 Subject: [PATCH 0123/1143] qml: don't stack exception dialogs when multiple exceptions happen --- electrum/gui/qml/components/main.qml | 14 ++++++++++---- electrum/gui/qml/qeapp.py | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 7957d381b..d39d8b3eb 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -32,6 +32,7 @@ ApplicationWindow property variant activeDialogs: [] property bool _wantClose: false + property var _exceptionDialog header: ToolBar { id: toolbar @@ -364,11 +365,16 @@ ApplicationWindow function onUserNotify(wallet_name, message) { notificationPopup.show(wallet_name, message) } - function onShowException() { - var dialog = crashDialog.createObject(app, { - crashData: AppController.crashData() + function onShowException(crash_data) { + if (app._exceptionDialog) + return + app._exceptionDialog = crashDialog.createObject(app, { + crashData: crash_data }) - dialog.open() + app._exceptionDialog.onClosed.connect(function() { + app._exceptionDialog = null + }) + app._exceptionDialog.open() } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index ae6af94c7..b3fd7636e 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -51,7 +51,7 @@ class QEAppController(BaseCrashReporter, QObject): _dummy = pyqtSignal() userNotify = pyqtSignal(str, str) uriReceived = pyqtSignal(str) - showException = pyqtSignal() + showException = pyqtSignal('QVariantMap') sendingBugreport = pyqtSignal() sendingBugreportSuccess = pyqtSignal(str) sendingBugreportFailure = pyqtSignal(str) @@ -223,7 +223,7 @@ def crashData(self): @pyqtSlot(object,object,object,object) def crash(self, config, e, text, tb): self.exc_args = (e, text, tb) # for BaseCrashReporter - self.showException.emit() + self.showException.emit(self.crashData()) @pyqtSlot() def sendReport(self): From 8c001883485f035c07e82e3e330078aecc855d48 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Feb 2023 10:52:09 +0100 Subject: [PATCH 0124/1143] TxEditor: set locktime --- electrum/gui/qt/confirm_tx_dialog.py | 9 +++++++++ electrum/gui/qt/locktimeedit.py | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 164595f49..98ab9f50f 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -77,6 +77,7 @@ def __init__(self, *, title='', self.is_preview = False self.locktime_e = LockTimeEdit(self) + self.locktime_e.valueEdited.connect(self._trigger_update) self.locktime_label = QLabel(_("LockTime") + ": ") self.io_widget = TxInOutWidget(self.main_window, self.wallet) self.create_fee_controls() @@ -109,6 +110,7 @@ def __init__(self, *, title='', def timer_actions(self): if self.needs_update: self.update_tx() + self.set_locktime() self.update() self.needs_update = False @@ -559,6 +561,13 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): raise self.tx.set_rbf(True) + def set_locktime(self): + if not self.tx: + return + locktime = self.locktime_e.get_locktime() + if locktime is not None: + self.tx.locktime = locktime + def have_enough_funds_assuming_zero_fees(self) -> bool: # called in send_tab.py try: diff --git a/electrum/gui/qt/locktimeedit.py b/electrum/gui/qt/locktimeedit.py index d8dc2b1ad..2ac30b709 100644 --- a/electrum/gui/qt/locktimeedit.py +++ b/electrum/gui/qt/locktimeedit.py @@ -6,7 +6,7 @@ from datetime import datetime from typing import Optional, Any -from PyQt5.QtCore import Qt, QDateTime +from PyQt5.QtCore import Qt, QDateTime, pyqtSignal from PyQt5.QtGui import QPalette, QPainter from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, QDateTimeEdit) @@ -19,6 +19,8 @@ class LockTimeEdit(QWidget): + valueEdited = pyqtSignal() + def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -63,6 +65,11 @@ def on_current_index_changed(i): hbox.addWidget(w) hbox.addStretch(1) + self.locktime_height_e.textEdited.connect(self.valueEdited.emit) + self.locktime_raw_e.textEdited.connect(self.valueEdited.emit) + self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit) + self.combo.currentIndexChanged.connect(self.valueEdited.emit) + def get_locktime(self) -> Optional[int]: return self.editor.get_locktime() From 95433cd153193c437e0ad7fa7a100fbdef4a47b1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Feb 2023 11:07:37 +0100 Subject: [PATCH 0125/1143] TxEditor: update_extra_fees, overloaded by ConfirmTxDialog --- electrum/gui/qt/confirm_tx_dialog.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 98ab9f50f..71b2e8e5a 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -480,13 +480,8 @@ def update(self): fee_rate = fee // tx.estimated_size() #self.feerate_label.setText(self.main_window.format_amount(fee_rate)) - # extra fee - x_fee = run_hook('get_tx_extra_fee', self.wallet, tx) - if x_fee: - x_fee_address, x_fee_amount = x_fee - self.extra_fee_label.setVisible(True) - self.extra_fee_value.setVisible(True) - self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount)) + self.update_extra_fees() + amount = tx.output_value() if self.output_value == '!' else self.output_value tx_size = tx.estimated_size() fee_warning_tuple = self.wallet.get_tx_fee_warning( @@ -500,6 +495,10 @@ def update(self): def _update_amount_label(self): pass + def update_extra_fees(self): + pass + + class ConfirmTxDialog(TxEditor): help_text = ''#_('Set the mining fee of your transaction') @@ -613,3 +612,11 @@ def create_grid(self): grid.addWidget(self.locktime_e, 6, 1, 1, 2) return grid + + def update_extra_fees(self): + x_fee = run_hook('get_tx_extra_fee', self.wallet, self.tx) + if x_fee: + x_fee_address, x_fee_amount = x_fee + self.extra_fee_label.setVisible(True) + self.extra_fee_value.setVisible(True) + self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount)) From a740a8a004db363b36379350ae80720bb5fd5fbc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Feb 2023 11:15:31 +0100 Subject: [PATCH 0126/1143] TxEditor: move set_locktime --- electrum/gui/qt/confirm_tx_dialog.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 71b2e8e5a..1debeb0ef 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -492,6 +492,13 @@ def update(self): else: self.toggle_send_button(True) + def set_locktime(self): + if not self.tx: + return + locktime = self.locktime_e.get_locktime() + if locktime is not None: + self.tx.locktime = locktime + def _update_amount_label(self): pass @@ -560,13 +567,6 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): raise self.tx.set_rbf(True) - def set_locktime(self): - if not self.tx: - return - locktime = self.locktime_e.get_locktime() - if locktime is not None: - self.tx.locktime = locktime - def have_enough_funds_assuming_zero_fees(self) -> bool: # called in send_tab.py try: From 13222c479cd68dbeb85fd34a2414bd20f30124a2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Feb 2023 11:29:10 +0100 Subject: [PATCH 0127/1143] TxEditor: make trigger_update not private, call it on rbf combo changed. --- electrum/gui/qt/confirm_tx_dialog.py | 11 ++++------- electrum/gui/qt/rbf_dialog.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 1debeb0ef..914163904 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -77,7 +77,7 @@ def __init__(self, *, title='', self.is_preview = False self.locktime_e = LockTimeEdit(self) - self.locktime_e.valueEdited.connect(self._trigger_update) + self.locktime_e.valueEdited.connect(self.trigger_update) self.locktime_label = QLabel(_("LockTime") + ": ") self.io_widget = TxInOutWidget(self.main_window, self.wallet) self.create_fee_controls() @@ -212,7 +212,7 @@ def feerounding_onclick(): self.fee_slider.setFixedWidth(200) self.fee_target.setFixedSize(self.feerate_e.sizeHint()) - def _trigger_update(self): + def trigger_update(self): # set tx to None so that the ok button is disabled while we compute the new tx self.tx = None self.update() @@ -229,7 +229,7 @@ def fee_slider_callback(self, dyn, pos, fee_rate): self.fee_e.setModified(False) self.update_fee_target() self.update_feerate_label() - self._trigger_update() + self.trigger_update() def on_fee_or_feerate(self, edit_changed, editing_finished): edit_other = self.feerate_e if edit_changed == self.fee_e else self.fee_e @@ -243,7 +243,7 @@ def on_fee_or_feerate(self, edit_changed, editing_finished): # freeze the correct fee setting (this) edit_other.setModified(False) self.fee_slider.deactivate() - self._trigger_update() + self.trigger_update() def is_send_fee_frozen(self): return self.fee_e.isVisible() and self.fee_e.isModified() \ @@ -477,9 +477,6 @@ def update(self): assert fee is not None self.fee_label.setText(self.main_window.config.format_amount_and_units(fee)) - fee_rate = fee // tx.estimated_size() - #self.feerate_label.setText(self.main_window.format_amount(fee_rate)) - self.update_extra_fees() amount = tx.output_value() if self.output_value == '!' else self.output_value diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index e99a477e4..33af82ab0 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -64,7 +64,7 @@ def create_grid(self): self.method_label = QLabel(_('Method') + ':') self.method_combo = QComboBox() self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')]) - self.method_combo.currentIndexChanged.connect(self.update) + self.method_combo.currentIndexChanged.connect(self.trigger_update) old_size_label = TxSizeLabel() old_size_label.setAlignment(Qt.AlignCenter) old_size_label.setAmount(self.old_tx_size) From 31523449b338b43f162e084f731a9b4b18df6901 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Feb 2023 12:20:50 +0100 Subject: [PATCH 0128/1143] qml: OTP dialog improvements --- electrum/gui/qml/components/OtpDialog.qml | 29 +++++++++++++---------- electrum/gui/qml/qewallet.py | 4 +++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/components/OtpDialog.qml b/electrum/gui/qml/components/OtpDialog.qml index 8b6a6d5f6..41ec51f85 100644 --- a/electrum/gui/qml/components/OtpDialog.qml +++ b/electrum/gui/qml/components/OtpDialog.qml @@ -44,27 +44,30 @@ ElDialog { inputMethodHints: Qt.ImhSensitiveData | Qt.ImhDigitsOnly echoMode: TextInput.Password focus: true + enabled: !_waiting + Keys.onPressed: _otpError = '' onTextChanged: { - if (activeFocus) - _otpError = '' + if (text.length == 6) { + _waiting = true + Daemon.currentWallet.submitOtp(otpEdit.text) + } } } Label { - opacity: _otpError ? 1 : 0 + Layout.topMargin: constants.paddingMedium + Layout.bottomMargin: constants.paddingMedium + Layout.alignment: Qt.AlignHCenter + text: _otpError color: constants.colorError - Layout.alignment: Qt.AlignHCenter - } - Button { - Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter - text: qsTr('Submit') - enabled: !_waiting - onClicked: { - _waiting = true - Daemon.currentWallet.submitOtp(otpEdit.text) + BusyIndicator { + anchors.centerIn: parent + width: constants.iconSizeXLarge + height: constants.iconSizeXLarge + visible: _waiting + running: _waiting } } } diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 7e4961b1f..a86b95ff4 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -538,7 +538,9 @@ def request_otp(self, on_submit): @pyqtSlot(str) def submitOtp(self, otp): - self._otp_on_submit(otp) + def submit_otp_task(): + self._otp_on_submit(otp) + threading.Thread(target=submit_otp_task).start() def broadcast(self, tx): assert tx.is_complete() From d1a40c47cf520f7a8b284150ef58aa9560f985a8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Feb 2023 12:42:09 +0100 Subject: [PATCH 0129/1143] qml: fix max toggle in InvoiceDialog --- electrum/gui/qml/components/InvoiceDialog.qml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index f089f2e4c..bcef5a0c2 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -313,21 +313,22 @@ ElDialog { } Label { - text: Config.baseUnit - color: Material.accentColor Layout.fillWidth: amountMax.visible ? false : true Layout.columnSpan: amountMax.visible ? 1 : 2 + + text: Config.baseUnit + color: Material.accentColor } Switch { id: amountMax + Layout.fillWidth: true + text: qsTr('Max') visible: _canMax - Layout.fillWidth: true - checked: invoice.amount.isMax + checked: false onCheckedChanged: { - if (activeFocus) { + if (activeFocus) invoice.amount.isMax = checked - } } } @@ -419,7 +420,10 @@ ElDialog { if (invoice_key != '') { invoice.initFromKey(invoice_key) } - if (invoice.amount.isEmpty) + if (invoice.amount.isEmpty) { amountContainer.editmode = true + } else if (invoice.amount.isMax) { + amountMax.checked = true + } } } From ad83eaaba19cc6f2a1f9418c5a1d9a4924b9149f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Feb 2023 14:17:58 +0100 Subject: [PATCH 0130/1143] TxEditor: rework update methods, separate _update_message from _update_send_button --- electrum/gui/qt/confirm_tx_dialog.py | 91 ++++++++++++++++------------ electrum/gui/qt/rbf_dialog.py | 22 ++----- 2 files changed, 56 insertions(+), 57 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 914163904..c9ad7b45f 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -67,6 +67,9 @@ def __init__(self, *, title='', self.make_tx = make_tx self.output_value = output_value self.tx = None # type: Optional[PartialTransaction] + self.message = '' # set by side effect + self.error = '' # set by side effect + self.config = window.config self.wallet = window.wallet self.not_enough_funds = False @@ -109,11 +112,14 @@ def __init__(self, *, title='', def timer_actions(self): if self.needs_update: - self.update_tx() - self.set_locktime() self.update() self.needs_update = False + def update(self): + self.update_tx() + self.set_locktime() + self._update_widgets() + def stop_editor_updates(self): self.main_window.gui_object.timer.timeout.disconnect(self.timer_actions) @@ -127,6 +133,7 @@ def set_fee_config(self, dyn, pos, fee_rate): self.config.set_key('fee_per_kb', fee_rate, False) def update_tx(self, *, fallback_to_zero_fee: bool = False): + # expected to set self.tx, self.message and self.error raise NotImplementedError() def update_fee_target(self): @@ -215,7 +222,9 @@ def feerounding_onclick(): def trigger_update(self): # set tx to None so that the ok button is disabled while we compute the new tx self.tx = None - self.update() + self.message = '' + self.error = '' + self._update_widgets() self.needs_update = True def fee_slider_callback(self, dyn, pos, fee_rate): @@ -447,47 +456,40 @@ def on_preview(self): self.is_preview = True self.accept() - def toggle_send_button(self, enable: bool, *, message: str = None): - if message is None: - self.message_label.setStyleSheet(None) - self.message_label.setText(' ') - else: - self.message_label.setStyleSheet(ColorScheme.RED.as_stylesheet()) - self.message_label.setText(message) - - self.preview_button.setEnabled(enable) - self.ok_button.setEnabled(enable) - - def update(self): - tx = self.tx + def _update_widgets(self): self._update_amount_label() if self.not_enough_funds: - text = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen() - self.toggle_send_button(False, message=text) - return - if not tx: - self.toggle_send_button(False) + self.error = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen() + if not self.tx: self.set_feerounding_visibility(False) - return - self.update_fee_fields() - if self.locktime_e.get_locktime() is None: - self.locktime_e.set_locktime(self.tx.locktime) - self.io_widget.update(tx) - fee = tx.get_fee() - assert fee is not None - self.fee_label.setText(self.main_window.config.format_amount_and_units(fee)) + else: + self.check_tx_fee_warning() + self.update_fee_fields() + if self.locktime_e.get_locktime() is None: + self.locktime_e.set_locktime(self.tx.locktime) + self.io_widget.update(self.tx) + self.fee_label.setText(self.main_window.config.format_amount_and_units(self.tx.get_fee())) + self._update_extra_fees() + + self._update_send_button() + self._update_message() - self.update_extra_fees() - amount = tx.output_value() if self.output_value == '!' else self.output_value - tx_size = tx.estimated_size() + def check_tx_fee_warning(self): + # side effects: self.error, self.message + fee = self.tx.get_fee() + assert fee is not None + amount = self.tx.output_value() if self.output_value == '!' else self.output_value + tx_size = self.tx.estimated_size() fee_warning_tuple = self.wallet.get_tx_fee_warning( invoice_amt=amount, tx_size=tx_size, fee=fee) if fee_warning_tuple: allow_send, long_warning, short_warning = fee_warning_tuple - self.toggle_send_button(allow_send, message=long_warning) - else: - self.toggle_send_button(True) + if not allow_send: + self.error = long_warning + else: + # note: this may overrride existing message + self.message = long_warning def set_locktime(self): if not self.tx: @@ -499,9 +501,19 @@ def set_locktime(self): def _update_amount_label(self): pass - def update_extra_fees(self): + def _update_extra_fees(self): pass + def _update_message(self): + style = ColorScheme.RED if self.error else ColorScheme.BLUE + self.message_label.setStyleSheet(style.as_stylesheet()) + self.message_label.setText(self.error or self.message) + + def _update_send_button(self): + enabled = bool(self.tx) and not self.error + self.preview_button.setEnabled(enabled) + self.ok_button.setEnabled(enabled) + class ConfirmTxDialog(TxEditor): help_text = ''#_('Set the mining fee of your transaction') @@ -516,8 +528,7 @@ def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps allow_preview=allow_preview) - BlockingWaitingDialog(window, _("Preparing transaction..."), self.update_tx) - self.update() + BlockingWaitingDialog(window, _("Preparing transaction..."), self.update) def _update_amount_label(self): tx = self.tx @@ -538,6 +549,8 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): self.tx = self.make_tx(fee_estimator) self.not_enough_funds = False self.no_dynfee_estimates = False + error = '' + message = '' except NotEnoughFunds: self.not_enough_funds = True self.tx = None @@ -610,7 +623,7 @@ def create_grid(self): return grid - def update_extra_fees(self): + def _update_extra_fees(self): x_fee = run_hook('get_tx_extra_fee', self.wallet, self.tx) if x_fee: x_fee_address, x_fee_amount = x_fee diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index 33af82ab0..e85055a32 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -51,8 +51,7 @@ def __init__( new_fee_rate = self.old_fee_rate + max(1, self.old_fee_rate // 20) self.feerate_e.setAmount(new_fee_rate) - self._update_tx(new_fee_rate) - self._update_message() + self.update() self.fee_slider.activate() # are we paying max? invoices = self.wallet.get_relevant_invoices_for_tx(txid) @@ -111,22 +110,18 @@ def sign_done(success): def update_tx(self): fee_rate = self.feerate_e.get_amount() - self._update_tx(fee_rate) - self._update_message() - - def _update_tx(self, fee_rate): if fee_rate is None: self.tx = None - self.message = '' + self.error = _('No fee rate') elif fee_rate <= self.old_fee_rate: self.tx = None - self.message = _("The new fee rate needs to be higher than the old fee rate.") + self.error = _("The new fee rate needs to be higher than the old fee rate.") else: try: self.tx = self.make_tx(fee_rate) except CannotBumpFee as e: self.tx = None - self.message = str(e) + self.error = str(e) if not self.tx: return delta = self.tx.get_fee() - self.old_tx.get_fee() @@ -135,15 +130,6 @@ def _update_tx(self, fee_rate): else: self.message = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta)) - def _update_message(self): - enabled = bool(self.tx) - self.ok_button.setEnabled(enabled) - if enabled: - style = ColorScheme.BLUE.as_stylesheet() - else: - style = ColorScheme.RED.as_stylesheet() - self.message_label.setStyleSheet(style) - self.message_label.setText(self.message) class BumpFeeDialog(_BaseRBFDialog): From a4928ea6eda84a59eca243db17503ad87171fab4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Feb 2023 14:58:57 +0100 Subject: [PATCH 0131/1143] TxEditor: various tweaks --- electrum/gui/qt/confirm_tx_dialog.py | 10 ++++++++-- electrum/gui/qt/fee_slider.py | 1 - electrum/gui/qt/rbf_dialog.py | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index c9ad7b45f..80e6cfe7d 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -181,6 +181,7 @@ def create_fee_controls(self): self.fee_target = QLabel('') self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback) self.fee_combo = FeeComboBox(self.fee_slider) + self.fee_combo.setFocusPolicy(Qt.NoFocus) def feerounding_onclick(): text = (self.feerounding_text + '\n\n' + @@ -251,8 +252,10 @@ def on_fee_or_feerate(self, edit_changed, editing_finished): # edit_changed was edited just now, so make sure we will # freeze the correct fee setting (this) edit_other.setModified(False) - self.fee_slider.deactivate() - self.trigger_update() + self.fee_slider.deactivate() + # do not call trigger_update on editing_finished, + # because that event is emitted when we press OK + self.trigger_update() def is_send_fee_frozen(self): return self.fee_e.isVisible() and self.fee_e.isModified() \ @@ -357,6 +360,8 @@ def update_fee_fields(self): self.set_feerounding_text(int(feerounding)) self.feerounding_icon.setToolTip(self.feerounding_text) self.set_feerounding_visibility(abs(feerounding) >= 1) + # feerate_label needs to be updated from feerate_e + self.update_feerate_label() def create_buttons_bar(self): self.preview_button = QPushButton(_('Preview')) @@ -382,6 +387,7 @@ def create_top_bar(self, text): self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) self.pref_button.setPopupMode(QToolButton.InstantPopup) + self.pref_button.setFocusPolicy(Qt.NoFocus) hbox = QHBoxLayout() hbox.addWidget(QLabel(text)) hbox.addStretch() diff --git a/electrum/gui/qt/fee_slider.py b/electrum/gui/qt/fee_slider.py index b73c4b49b..05f681426 100644 --- a/electrum/gui/qt/fee_slider.py +++ b/electrum/gui/qt/fee_slider.py @@ -40,7 +40,6 @@ def __init__(self, window, config, callback): self.update() self.valueChanged.connect(self.moved) self._active = True - self.setFocusPolicy(Qt.NoFocus) def get_fee_rate(self, pos): if self.dyn: diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index e85055a32..34f5f3ec6 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -52,7 +52,7 @@ def __init__( new_fee_rate = self.old_fee_rate + max(1, self.old_fee_rate // 20) self.feerate_e.setAmount(new_fee_rate) self.update() - self.fee_slider.activate() + self.fee_slider.deactivate() # are we paying max? invoices = self.wallet.get_relevant_invoices_for_tx(txid) if len(invoices) == 1 and len(invoices[0].outputs) == 1: @@ -64,6 +64,7 @@ def create_grid(self): self.method_combo = QComboBox() self.method_combo.addItems([_('Preserve payment'), _('Decrease payment')]) self.method_combo.currentIndexChanged.connect(self.trigger_update) + self.method_combo.setFocusPolicy(Qt.NoFocus) old_size_label = TxSizeLabel() old_size_label.setAlignment(Qt.AlignCenter) old_size_label.setAmount(self.old_tx_size) From d1eb909bee758c44d0782f1dda1147663d801c16 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 1 Feb 2023 11:27:41 +0100 Subject: [PATCH 0132/1143] UTXO tab: simplify freeze menus --- electrum/gui/qt/utxo_list.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 3c1d067e7..535c7640f 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -212,34 +212,30 @@ def create_menu(self, position): return self.add_copy_menu(menu, idx) # "Freeze coin" + menu_freeze = menu.addMenu(_("Freeze")) if not self.wallet.is_frozen_coin(utxo): - menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True)) + menu_freeze.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], 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], False)) - menu.addSeparator() + menu_freeze.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False)) # "Freeze address" if not self.wallet.is_frozen_address(addr): - menu.addAction(_("Freeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) + menu_freeze.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() + menu_freeze.addAction(_("Unfreeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], False)) elif len(coins) > 1: # multiple items selected menu.addSeparator() addrs = [utxo.address for utxo in coins] is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins] is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins] + menu_freeze = menu.addMenu(_("Freeze")) if not all(is_coin_frozen): - menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True)) + menu_freeze.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)) + menu_freeze.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)) + menu_freeze.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_freeze.addAction(_("Unfreeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, False)) menu.exec_(self.viewport().mapToGlobal(position)) From 965ccedc88f60dd968b56e61ed2b4c85ee3524a0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 10 Feb 2023 13:02:58 +0100 Subject: [PATCH 0133/1143] tx dialog clean-up: remove 'finalized' field and related code --- electrum/gui/qt/transaction_dialog.py | 44 ++++++++------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 30e8ac6b9..bbf9db722 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -136,11 +136,6 @@ def update(self, tx): self.tx = tx inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs()) - #if not self.finalized: - # selected_coins = self.main_window.get_manually_selected_coins() - # if selected_coins is not None: - # inputs_header_text += f" - " + _("Coin selection active ({} UTXOs selected)").format(len(selected_coins)) - self.inputs_header.setText(inputs_header_text) ext = QTextCharFormat() # "external" @@ -362,9 +357,10 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, pr -class BaseTxDialog(QDialog, MessageBoxMixin): - def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None): +class TxDialog(QDialog, MessageBoxMixin): + + def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, external_keypairs=None): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' @@ -372,7 +368,6 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz QDialog.__init__(self, parent=None) self.tx = None # type: Optional[Transaction] self.external_keypairs = external_keypairs - self.finalized = finalized self.main_window = parent self.config = parent.config self.wallet = parent.wallet @@ -380,7 +375,6 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.saved = False self.desc = desc self.setMinimumWidth(640) - self.set_title() self.psbt_only_widgets = [] # type: List[QWidget] @@ -444,21 +438,16 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz # Transaction sharing buttons self.sharing_buttons = [self.export_actions_button, self.save_button] run_hook('transaction_dialog', self) - #if not self.finalized: - # self.create_fee_controls() - # vbox.addWidget(self.feecontrol_fields) self.hbox = hbox = QHBoxLayout() hbox.addLayout(Buttons(*self.sharing_buttons)) hbox.addStretch(1) hbox.addLayout(Buttons(*self.buttons)) vbox.addLayout(hbox) - self.set_buttons_visibility() - dialogs.append(self) - def set_buttons_visibility(self): - for b in [self.export_actions_button, self.save_button, self.sign_button, self.broadcast_button, self.partial_tx_actions_button]: - b.setVisible(self.finalized) + self.set_tx(tx) + self.update() + self.set_title() def set_tx(self, tx: 'Transaction'): # Take a copy; it might get updated in the main window by @@ -667,8 +656,7 @@ def update(self): txid = self.tx.txid() fx = self.main_window.fx tx_item_fiat = None - if (self.finalized # ensures we don't use historical rates for tx being constructed *now* - and txid is not None and fx.is_enabled() and amount is not None): + if (txid is not None and fx.is_enabled() and amount is not None): tx_item_fiat = self.wallet.get_tx_item_fiat( tx_hash=txid, amount_sat=abs(amount), fx=fx, tx_fee=fee) lnworker_history = self.wallet.lnworker.get_onchain_history() if self.wallet.lnworker else {} @@ -683,7 +671,7 @@ def update(self): can_sign = not self.tx.is_complete() and \ (self.wallet.can_sign(self.tx) or bool(self.external_keypairs)) self.sign_button.setEnabled(can_sign) - if self.finalized and tx_details.txid: + if tx_details.txid: self.tx_hash_e.setText(tx_details.txid) else: # note: when not finalized, RBF and locktime changes do not trigger @@ -785,7 +773,7 @@ def update(self): self.ln_amount_label.setText(ln_amount_str) else: self.ln_amount_label.hide() - show_psbt_only_widgets = self.finalized and isinstance(self.tx, PartialTransaction) + show_psbt_only_widgets = isinstance(self.tx, PartialTransaction) for widget in self.psbt_only_widgets: if isinstance(widget, QMenu): widget.menuAction().setVisible(show_psbt_only_widgets) @@ -863,11 +851,11 @@ def add_tx_stats(self, vbox): vbox.addWidget(self.block_hash_label) # set visibility after parenting can be determined by Qt - self.rbf_label.setVisible(self.finalized) - self.locktime_final_label.setVisible(self.finalized) + self.rbf_label.setVisible(True) + self.locktime_final_label.setVisible(True) def set_title(self): - self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction")) + self.setWindowTitle(_("Transaction") + ' ' + self.tx.txid()) def can_finalize(self) -> bool: return False @@ -911,11 +899,3 @@ def __init__( self.text_char_format.setBackground(QBrush(self.color)) self.text_char_format.setToolTip(tooltip) - -class TxDialog(BaseTxDialog): - def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved): - BaseTxDialog.__init__(self, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved, finalized=True) - self.set_tx(tx) - self.update() - - From 3cb9ded1ca3558da7fe303ac4c5b31cf8ff0ac9b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Feb 2023 12:57:26 +0100 Subject: [PATCH 0134/1143] qml: fix flickable margins TxDetails and WalletDetails --- electrum/gui/qml/components/TxDetails.qml | 439 +++++------ electrum/gui/qml/components/WalletDetails.qml | 687 +++++++++--------- 2 files changed, 565 insertions(+), 561 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 4bc013f21..bd2537ed9 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -36,282 +36,283 @@ Pane { Flickable { Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: constants.paddingLarge - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge - contentHeight: contentLayout.height + contentHeight: flickableRoot.height clip: true interactive: height < contentHeight - GridLayout { - id: contentLayout + Pane { + id: flickableRoot width: parent.width - columns: 2 + padding: constants.paddingLarge - Heading { - Layout.columnSpan: 2 - text: qsTr('Transaction Details') - } + GridLayout { + width: parent.width + columns: 2 - RowLayout { - Layout.fillWidth: true - Layout.columnSpan: 2 - visible: txdetails.isUnrelated - Image { - source: '../../icons/warning.png' - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall + Heading { + Layout.columnSpan: 2 + text: qsTr('Transaction Details') } + + RowLayout { + Layout.fillWidth: true + Layout.columnSpan: 2 + visible: txdetails.isUnrelated + Image { + source: '../../icons/warning.png' + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + } + Label { + text: qsTr('Transaction is unrelated to this wallet') + color: Material.accentColor + } + } + Label { - text: qsTr('Transaction is unrelated to this wallet') + visible: !txdetails.isUnrelated && txdetails.lnAmount.satsInt == 0 + text: txdetails.amount.satsInt > 0 + ? qsTr('Amount received') + : qsTr('Amount sent') color: Material.accentColor } - } - - Label { - visible: !txdetails.isUnrelated && txdetails.lnAmount.satsInt == 0 - text: txdetails.amount.satsInt > 0 - ? qsTr('Amount received') - : qsTr('Amount sent') - color: Material.accentColor - } - Label { - Layout.fillWidth: true - visible: !txdetails.isUnrelated && txdetails.lnAmount.satsInt != 0 - text: txdetails.lnAmount.satsInt > 0 - ? qsTr('Amount received in channels') - : qsTr('Amount withdrawn from channels') - color: Material.accentColor - wrapMode: Text.Wrap - } + Label { + Layout.fillWidth: true + visible: !txdetails.isUnrelated && txdetails.lnAmount.satsInt != 0 + text: txdetails.lnAmount.satsInt > 0 + ? qsTr('Amount received in channels') + : qsTr('Amount withdrawn from channels') + color: Material.accentColor + wrapMode: Text.Wrap + } - FormattedAmount { - visible: !txdetails.isUnrelated - Layout.fillWidth: true - amount: txdetails.lnAmount.isEmpty ? txdetails.amount : txdetails.lnAmount - } + FormattedAmount { + visible: !txdetails.isUnrelated + Layout.fillWidth: true + amount: txdetails.lnAmount.isEmpty ? txdetails.amount : txdetails.lnAmount + } - Label { - visible: !txdetails.fee.isEmpty - text: qsTr('Transaction fee') - color: Material.accentColor - } + Label { + visible: !txdetails.fee.isEmpty + text: qsTr('Transaction fee') + color: Material.accentColor + } - RowLayout { - Layout.fillWidth: true - visible: !txdetails.fee.isEmpty - FormattedAmount { + RowLayout { Layout.fillWidth: true - amount: txdetails.fee + visible: !txdetails.fee.isEmpty + FormattedAmount { + Layout.fillWidth: true + amount: txdetails.fee + } } - } - Item { - visible: feebumpButton.visible - Layout.preferredWidth: 1 ; Layout.preferredHeight: 1 - } - FlatButton { - id: feebumpButton - visible: txdetails.canBump || txdetails.canCpfp - textUnderIcon: false - icon.source: '../../icons/warning.png' - icon.color: 'transparent' - text: qsTr('Bump fee') - onClicked: { - if (txdetails.canBump) { - var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) - } else { - var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + Item { + visible: feebumpButton.visible + Layout.preferredWidth: 1 ; Layout.preferredHeight: 1 + } + FlatButton { + id: feebumpButton + visible: txdetails.canBump || txdetails.canCpfp + textUnderIcon: false + icon.source: '../../icons/warning.png' + icon.color: 'transparent' + text: qsTr('Bump fee') + onClicked: { + if (txdetails.canBump) { + var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) + } else { + var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + } + dialog.open() } - dialog.open() } - } - Label { - Layout.fillWidth: true - text: qsTr('Status') - color: Material.accentColor - } + Label { + Layout.fillWidth: true + text: qsTr('Status') + color: Material.accentColor + } - Label { - Layout.fillWidth: true - text: txdetails.status - } + Label { + Layout.fillWidth: true + text: txdetails.status + } - Label { - text: qsTr('Mempool depth') - color: Material.accentColor - visible: txdetails.mempoolDepth - } + Label { + text: qsTr('Mempool depth') + color: Material.accentColor + visible: txdetails.mempoolDepth + } - Label { - text: txdetails.mempoolDepth - visible: txdetails.mempoolDepth - } + Label { + text: txdetails.mempoolDepth + visible: txdetails.mempoolDepth + } - Label { - visible: txdetails.isMined - text: qsTr('Date') - color: Material.accentColor - } + Label { + visible: txdetails.isMined + text: qsTr('Date') + color: Material.accentColor + } - Label { - visible: txdetails.isMined - text: txdetails.date - } + Label { + visible: txdetails.isMined + text: txdetails.date + } - Label { - visible: txdetails.isMined - text: qsTr('Height') - color: Material.accentColor - } + Label { + visible: txdetails.isMined + text: qsTr('Height') + color: Material.accentColor + } - Label { - visible: txdetails.isMined - text: txdetails.height - } + Label { + visible: txdetails.isMined + text: txdetails.height + } - Label { - visible: txdetails.isMined - text: qsTr('TX index') - color: Material.accentColor - } + Label { + visible: txdetails.isMined + text: qsTr('TX index') + color: Material.accentColor + } - Label { - visible: txdetails.isMined - text: txdetails.txpos - } + Label { + visible: txdetails.isMined + text: txdetails.txpos + } - Label { - text: qsTr('Label') - Layout.columnSpan: 2 - color: Material.accentColor - } + Label { + text: qsTr('Label') + Layout.columnSpan: 2 + color: Material.accentColor + } - TextHighlightPane { - id: labelContent + TextHighlightPane { + id: labelContent - property bool editmode: false + property bool editmode: false - Layout.columnSpan: 2 - Layout.fillWidth: true + Layout.columnSpan: 2 + Layout.fillWidth: true - RowLayout { - width: parent.width - Label { - visible: !labelContent.editmode - text: txdetails.label - wrapMode: Text.Wrap - Layout.fillWidth: true - font.pixelSize: constants.fontSizeLarge - } - ToolButton { - visible: !labelContent.editmode - icon.source: '../../icons/pen.png' - icon.color: 'transparent' - onClicked: { - labelEdit.text = txdetails.label - labelContent.editmode = true - labelEdit.focus = true + RowLayout { + width: parent.width + Label { + visible: !labelContent.editmode + text: txdetails.label + wrapMode: Text.Wrap + Layout.fillWidth: true + font.pixelSize: constants.fontSizeLarge } - } - TextField { - id: labelEdit - visible: labelContent.editmode - text: txdetails.label - font.pixelSize: constants.fontSizeLarge - Layout.fillWidth: true - } - ToolButton { - visible: labelContent.editmode - icon.source: '../../icons/confirmed.png' - icon.color: 'transparent' - onClicked: { - labelContent.editmode = false - txdetails.set_label(labelEdit.text) + ToolButton { + visible: !labelContent.editmode + icon.source: '../../icons/pen.png' + icon.color: 'transparent' + onClicked: { + labelEdit.text = txdetails.label + labelContent.editmode = true + labelEdit.focus = true + } } - } - ToolButton { - visible: labelContent.editmode - icon.source: '../../icons/closebutton.png' - icon.color: 'transparent' - onClicked: labelContent.editmode = false - } - } - } - - Label { - text: qsTr('Transaction ID') - Layout.columnSpan: 2 - color: Material.accentColor - } - - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - - RowLayout { - width: parent.width - Label { - text: txdetails.txid - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - Layout.fillWidth: true - wrapMode: Text.Wrap - } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - enabled: txdetails.txid - onClicked: { - var dialog = app.genericShareDialog.createObject(root, - { title: qsTr('Transaction ID'), text: txdetails.txid } - ) - dialog.open() + TextField { + id: labelEdit + visible: labelContent.editmode + text: txdetails.label + font.pixelSize: constants.fontSizeLarge + Layout.fillWidth: true + } + ToolButton { + visible: labelContent.editmode + icon.source: '../../icons/confirmed.png' + icon.color: 'transparent' + onClicked: { + labelContent.editmode = false + txdetails.set_label(labelEdit.text) + } + } + ToolButton { + visible: labelContent.editmode + icon.source: '../../icons/closebutton.png' + icon.color: 'transparent' + onClicked: labelContent.editmode = false } } } - } - Label { - text: qsTr('Outputs') - Layout.columnSpan: 2 - color: Material.accentColor - } + Label { + text: qsTr('Transaction ID') + Layout.columnSpan: 2 + color: Material.accentColor + } - Repeater { - model: txdetails.outputs - delegate: TextHighlightPane { + TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true RowLayout { width: parent.width Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap + text: txdetails.txid font.pixelSize: constants.fontSizeLarge font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground + Layout.fillWidth: true + wrapMode: Text.Wrap } - Label { - text: Config.formatSats(modelData.value) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + enabled: txdetails.txid + onClicked: { + var dialog = app.genericShareDialog.createObject(root, + { title: qsTr('Transaction ID'), text: txdetails.txid } + ) + dialog.open() + } } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor + } + } + + Label { + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: txdetails.outputs + delegate: TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + RowLayout { + width: parent.width + Label { + text: modelData.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: modelData.is_mine ? constants.colorMine : Material.foreground + } + Label { + text: Config.formatSats(modelData.value) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } } } } } } - } ButtonContainer { diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 074ed9af3..d9e83fbb1 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -45,417 +45,420 @@ Pane { ColumnLayout { id: rootLayout - width: parent.width - height: parent.height + anchors.fill: parent spacing: 0 Flickable { Layout.fillWidth: true Layout.fillHeight: true - Layout.margins: constants.paddingLarge - contentHeight: flickableLayout.height + contentHeight: flickableRoot.height clip:true interactive: height < contentHeight - ColumnLayout { - id: flickableLayout + Pane { + id: flickableRoot width: parent.width - spacing: constants.paddingLarge + padding: constants.paddingLarge - Heading { - text: qsTr('Wallet details') - } - - GridLayout { - columns: 3 - Layout.alignment: Qt.AlignHCenter + ColumnLayout { + width: parent.width + spacing: constants.paddingLarge - Tag { - Layout.alignment: Qt.AlignHCenter - text: Daemon.currentWallet.walletType - font.pixelSize: constants.fontSizeSmall - font.bold: true - iconSource: '../../../icons/wallet.png' - } - Tag { - Layout.alignment: Qt.AlignHCenter - text: Daemon.currentWallet.txinType - font.pixelSize: constants.fontSizeSmall - font.bold: true - } - Tag { - Layout.alignment: Qt.AlignHCenter - text: qsTr('HD') - visible: Daemon.currentWallet.isDeterministic - font.pixelSize: constants.fontSizeSmall - font.bold: true - } - Tag { - Layout.alignment: Qt.AlignHCenter - text: qsTr('Watch only') - visible: Daemon.currentWallet.isWatchOnly - font.pixelSize: constants.fontSizeSmall - font.bold: true - iconSource: '../../../icons/eye1.png' + Heading { + text: qsTr('Wallet details') } - Tag { - Layout.alignment: Qt.AlignHCenter - text: qsTr('Encrypted') - visible: Daemon.currentWallet.isEncrypted - font.pixelSize: constants.fontSizeSmall - font.bold: true - iconSource: '../../../icons/key.png' - } - Tag { - Layout.alignment: Qt.AlignHCenter - text: qsTr('HW') - visible: Daemon.currentWallet.isHardware - font.pixelSize: constants.fontSizeSmall - font.bold: true - iconSource: '../../../icons/seed.png' - } - Tag { - Layout.alignment: Qt.AlignHCenter - text: qsTr('Lightning') - visible: Daemon.currentWallet.isLightning - font.pixelSize: constants.fontSizeSmall - font.bold: true - iconSource: '../../../icons/lightning.png' - } - Tag { + + GridLayout { + columns: 3 Layout.alignment: Qt.AlignHCenter - text: qsTr('Seed') - visible: Daemon.currentWallet.hasSeed - font.pixelSize: constants.fontSizeSmall - font.bold: true - iconSource: '../../../icons/seed.png' + + Tag { + Layout.alignment: Qt.AlignHCenter + text: Daemon.currentWallet.walletType + font.pixelSize: constants.fontSizeSmall + font.bold: true + iconSource: '../../../icons/wallet.png' + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: Daemon.currentWallet.txinType + font.pixelSize: constants.fontSizeSmall + font.bold: true + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: qsTr('HD') + visible: Daemon.currentWallet.isDeterministic + font.pixelSize: constants.fontSizeSmall + font.bold: true + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: qsTr('Watch only') + visible: Daemon.currentWallet.isWatchOnly + font.pixelSize: constants.fontSizeSmall + font.bold: true + iconSource: '../../../icons/eye1.png' + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: qsTr('Encrypted') + visible: Daemon.currentWallet.isEncrypted + font.pixelSize: constants.fontSizeSmall + font.bold: true + iconSource: '../../../icons/key.png' + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: qsTr('HW') + visible: Daemon.currentWallet.isHardware + font.pixelSize: constants.fontSizeSmall + font.bold: true + iconSource: '../../../icons/seed.png' + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: qsTr('Lightning') + visible: Daemon.currentWallet.isLightning + font.pixelSize: constants.fontSizeSmall + font.bold: true + iconSource: '../../../icons/lightning.png' + } + Tag { + Layout.alignment: Qt.AlignHCenter + text: qsTr('Seed') + visible: Daemon.currentWallet.hasSeed + font.pixelSize: constants.fontSizeSmall + font.bold: true + iconSource: '../../../icons/seed.png' + } } - } - Piechart { - id: piechart - visible: Daemon.currentWallet.totalBalance.satsInt > 0 - Layout.preferredWidth: parent.width - implicitHeight: 220 // TODO: sane value dependent on screen - innerOffset: 6 - function updateSlices() { - var totalB = Daemon.currentWallet.totalBalance.satsInt - var onchainB = Daemon.currentWallet.confirmedBalance.satsInt - var frozenB = Daemon.currentWallet.frozenBalance.satsInt - var lnB = Daemon.currentWallet.lightningBalance.satsInt - piechart.slices = [ - { v: lnB/totalB, color: constants.colorPiechartLightning, text: 'Lightning' }, - { v: (onchainB-frozenB)/totalB, color: constants.colorPiechartOnchain, text: 'On-chain' }, - { v: frozenB/totalB, color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, - ] + Piechart { + id: piechart + visible: Daemon.currentWallet.totalBalance.satsInt > 0 + Layout.preferredWidth: parent.width + implicitHeight: 220 // TODO: sane value dependent on screen + innerOffset: 6 + function updateSlices() { + var totalB = Daemon.currentWallet.totalBalance.satsInt + var onchainB = Daemon.currentWallet.confirmedBalance.satsInt + var frozenB = Daemon.currentWallet.frozenBalance.satsInt + var lnB = Daemon.currentWallet.lightningBalance.satsInt + piechart.slices = [ + { v: lnB/totalB, color: constants.colorPiechartLightning, text: 'Lightning' }, + { v: (onchainB-frozenB)/totalB, color: constants.colorPiechartOnchain, text: 'On-chain' }, + { v: frozenB/totalB, color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, + ] + } } - } - GridLayout { - Layout.alignment: Qt.AlignHCenter - visible: Daemon.currentWallet - columns: 3 + GridLayout { + Layout.alignment: Qt.AlignHCenter + visible: Daemon.currentWallet + columns: 3 - Item { - visible: !Daemon.currentWallet.totalBalance.isEmpty - Layout.preferredWidth: 1; Layout.preferredHeight: 1 - } - Label { - visible: !Daemon.currentWallet.totalBalance.isEmpty - text: qsTr('Total') - } - FormattedAmount { - visible: !Daemon.currentWallet.totalBalance.isEmpty - amount: Daemon.currentWallet.totalBalance - } + Item { + visible: !Daemon.currentWallet.totalBalance.isEmpty + Layout.preferredWidth: 1; Layout.preferredHeight: 1 + } + Label { + visible: !Daemon.currentWallet.totalBalance.isEmpty + text: qsTr('Total') + } + FormattedAmount { + visible: !Daemon.currentWallet.totalBalance.isEmpty + amount: Daemon.currentWallet.totalBalance + } - Rectangle { - visible: !Daemon.currentWallet.lightningBalance.isEmpty - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - color: constants.colorPiechartLightning - } - Label { - visible: !Daemon.currentWallet.lightningBalance.isEmpty - text: qsTr('Lightning') + Rectangle { + visible: !Daemon.currentWallet.lightningBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartLightning + } + Label { + visible: !Daemon.currentWallet.lightningBalance.isEmpty + text: qsTr('Lightning') - } - FormattedAmount { - amount: Daemon.currentWallet.lightningBalance - visible: !Daemon.currentWallet.lightningBalance.isEmpty - } + } + FormattedAmount { + amount: Daemon.currentWallet.lightningBalance + visible: !Daemon.currentWallet.lightningBalance.isEmpty + } - Rectangle { - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - color: constants.colorPiechartOnchain - } - Label { - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty - text: qsTr('On-chain') + Rectangle { + visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartOnchain + } + Label { + visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + text: qsTr('On-chain') - } - FormattedAmount { - amount: Daemon.currentWallet.confirmedBalance - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty - } + } + FormattedAmount { + amount: Daemon.currentWallet.confirmedBalance + visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + } - Rectangle { - visible: !Daemon.currentWallet.frozenBalance.isEmpty - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - color: constants.colorPiechartFrozen - } - Label { - visible: !Daemon.currentWallet.frozenBalance.isEmpty - text: qsTr('Frozen') - } - FormattedAmount { - amount: Daemon.currentWallet.frozenBalance - visible: !Daemon.currentWallet.frozenBalance.isEmpty + Rectangle { + visible: !Daemon.currentWallet.frozenBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartFrozen + } + Label { + visible: !Daemon.currentWallet.frozenBalance.isEmpty + text: qsTr('Frozen') + } + FormattedAmount { + amount: Daemon.currentWallet.frozenBalance + visible: !Daemon.currentWallet.frozenBalance.isEmpty + } } - } - GridLayout { - Layout.preferredWidth: parent.width - visible: Daemon.currentWallet - columns: 2 + GridLayout { + Layout.preferredWidth: parent.width + visible: Daemon.currentWallet + columns: 2 - Label { - Layout.columnSpan: 2 - visible: Daemon.currentWallet.hasSeed - text: qsTr('Seed') - color: Material.accentColor - } + Label { + Layout.columnSpan: 2 + visible: Daemon.currentWallet.hasSeed + text: qsTr('Seed') + color: Material.accentColor + } - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - visible: Daemon.currentWallet.hasSeed - RowLayout { - width: parent.width - Label { - id: seedText - visible: false - Layout.fillWidth: true - text: Daemon.currentWallet.seed - wrapMode: Text.Wrap - font.family: FixedFont - font.pixelSize: constants.fontSizeMedium - } - Label { - id: showSeedText - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - text: qsTr('Tap to show seed') - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - } - MouseArea { - anchors.fill: parent - onClicked: { - seedText.visible = true - showSeedText.visible = false + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: Daemon.currentWallet.hasSeed + RowLayout { + width: parent.width + Label { + id: seedText + visible: false + Layout.fillWidth: true + text: Daemon.currentWallet.seed + wrapMode: Text.Wrap + font.family: FixedFont + font.pixelSize: constants.fontSizeMedium + } + Label { + id: showSeedText + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: qsTr('Tap to show seed') + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + } + MouseArea { + anchors.fill: parent + onClicked: { + seedText.visible = true + showSeedText.visible = false + } } } } - } - Label { - Layout.columnSpan: 2 - visible: Daemon.currentWallet.isLightning - text: qsTr('Lightning Node ID') - color: Material.accentColor - } + Label { + Layout.columnSpan: 2 + visible: Daemon.currentWallet.isLightning + text: qsTr('Lightning Node ID') + color: Material.accentColor + } - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - visible: Daemon.currentWallet.isLightning - - RowLayout { - width: parent.width - Label { - Layout.fillWidth: true - text: Daemon.currentWallet.lightningNodePubkey - wrapMode: Text.Wrap - font.family: FixedFont - font.pixelSize: constants.fontSizeMedium - } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - onClicked: { - var dialog = app.genericShareDialog.createObject(rootItem, { - title: qsTr('Lightning Node ID'), - text: Daemon.currentWallet.lightningNodePubkey - }) - dialog.open() + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: Daemon.currentWallet.isLightning + + RowLayout { + width: parent.width + Label { + Layout.fillWidth: true + text: Daemon.currentWallet.lightningNodePubkey + wrapMode: Text.Wrap + font.family: FixedFont + font.pixelSize: constants.fontSizeMedium + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = app.genericShareDialog.createObject(rootItem, { + title: qsTr('Lightning Node ID'), + text: Daemon.currentWallet.lightningNodePubkey + }) + dialog.open() + } } } } - } - Label { - visible: _is2fa - text: qsTr('2FA') - color: Material.accentColor - } + Label { + visible: _is2fa + text: qsTr('2FA') + color: Material.accentColor + } - Label { - Layout.fillWidth: true - visible: _is2fa - text: Daemon.currentWallet.canSignWithoutServer - ? qsTr('disabled (can sign without server)') - : qsTr('enabled') - } + Label { + Layout.fillWidth: true + visible: _is2fa + text: Daemon.currentWallet.canSignWithoutServer + ? qsTr('disabled (can sign without server)') + : qsTr('enabled') + } - Label { - visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer - text: qsTr('Remaining TX') - color: Material.accentColor - } + Label { + visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer + text: qsTr('Remaining TX') + color: Material.accentColor + } - Label { - Layout.fillWidth: true - visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer - text: 'tx_remaining' in Daemon.currentWallet.billingInfo - ? Daemon.currentWallet.billingInfo['tx_remaining'] - : qsTr('unknown') - } + Label { + Layout.fillWidth: true + visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer + text: 'tx_remaining' in Daemon.currentWallet.billingInfo + ? Daemon.currentWallet.billingInfo['tx_remaining'] + : qsTr('unknown') + } - Label { - Layout.columnSpan: 2 - visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer - text: qsTr('Billing') - color: Material.accentColor - } + Label { + Layout.columnSpan: 2 + visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer + text: qsTr('Billing') + color: Material.accentColor + } - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer - ColumnLayout { - spacing: 0 + ColumnLayout { + spacing: 0 - ButtonGroup { - id: billinggroup - onCheckedButtonChanged: { - Config.trustedcoinPrepay = checkedButton.value + ButtonGroup { + id: billinggroup + onCheckedButtonChanged: { + Config.trustedcoinPrepay = checkedButton.value + } } - } - Repeater { - model: AppController.plugin('trustedcoin').billingModel - delegate: RowLayout { - RadioButton { - ButtonGroup.group: billinggroup - property string value: modelData.value - text: modelData.text - checked: modelData.value == Config.trustedcoinPrepay - } - Label { - text: Config.formatSats(modelData.sats_per_tx) - font.family: FixedFont - } - Label { - text: Config.baseUnit + '/tx' - color: Material.accentColor + Repeater { + model: AppController.plugin('trustedcoin').billingModel + delegate: RowLayout { + RadioButton { + ButtonGroup.group: billinggroup + property string value: modelData.value + text: modelData.text + checked: modelData.value == Config.trustedcoinPrepay + } + Label { + text: Config.formatSats(modelData.sats_per_tx) + font.family: FixedFont + } + Label { + text: Config.baseUnit + '/tx' + color: Material.accentColor + } } } } } - } - - Repeater { - id: keystores - model: Daemon.currentWallet.keystores - delegate: ColumnLayout { - Layout.columnSpan: 2 - RowLayout { - Label { - text: qsTr('Keystore') - color: Material.accentColor - } - Label { - text: '#' + index - visible: keystores.count > 1 - } - Image { - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - source: modelData.watch_only ? '../../icons/eye1.png' : '../../icons/key.png' - } - } - TextHighlightPane { - Layout.fillWidth: true - leftPadding: constants.paddingLarge - - GridLayout { - width: parent.width - columns: 2 + Repeater { + id: keystores + model: Daemon.currentWallet.keystores + delegate: ColumnLayout { + Layout.columnSpan: 2 + RowLayout { Label { - text: qsTr('Derivation prefix') - visible: modelData.derivation_prefix + text: qsTr('Keystore') color: Material.accentColor } Label { - Layout.fillWidth: true - text: modelData.derivation_prefix - visible: modelData.derivation_prefix - font.family: FixedFont + text: '#' + index + visible: keystores.count > 1 } - - Label { - text: qsTr('BIP32 fingerprint') - visible: modelData.fingerprint - color: Material.accentColor - } - Label { - Layout.fillWidth: true - text: modelData.fingerprint - visible: modelData.fingerprint - font.family: FixedFont + Image { + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + source: modelData.watch_only ? '../../icons/eye1.png' : '../../icons/key.png' } + } + TextHighlightPane { + Layout.fillWidth: true + leftPadding: constants.paddingLarge - Label { - Layout.columnSpan: 2 - visible: modelData.master_pubkey - text: qsTr('Master Public Key') - color: Material.accentColor - } - RowLayout { - Layout.fillWidth: true - Layout.columnSpan: 2 - Layout.leftMargin: constants.paddingLarge + GridLayout { + width: parent.width + columns: 2 + + Label { + text: qsTr('Derivation prefix') + visible: modelData.derivation_prefix + color: Material.accentColor + } Label { - text: modelData.master_pubkey - wrapMode: Text.Wrap Layout.fillWidth: true + text: modelData.derivation_prefix + visible: modelData.derivation_prefix font.family: FixedFont - font.pixelSize: constants.fontSizeMedium } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - onClicked: { - var dialog = app.genericShareDialog.createObject(rootItem, { - title: qsTr('Master Public Key'), - text: modelData.master_pubkey - }) - dialog.open() + + Label { + text: qsTr('BIP32 fingerprint') + visible: modelData.fingerprint + color: Material.accentColor + } + Label { + Layout.fillWidth: true + text: modelData.fingerprint + visible: modelData.fingerprint + font.family: FixedFont + } + + Label { + Layout.columnSpan: 2 + visible: modelData.master_pubkey + text: qsTr('Master Public Key') + color: Material.accentColor + } + RowLayout { + Layout.fillWidth: true + Layout.columnSpan: 2 + Layout.leftMargin: constants.paddingLarge + Label { + text: modelData.master_pubkey + wrapMode: Text.Wrap + Layout.fillWidth: true + font.family: FixedFont + font.pixelSize: constants.fontSizeMedium + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = app.genericShareDialog.createObject(rootItem, { + title: qsTr('Master Public Key'), + text: modelData.master_pubkey + }) + dialog.open() + } } } } } } } - } + } } } } From 02fd25141ea8fff75075fff7be244c8fb3ee8b98 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 10:05:39 +0100 Subject: [PATCH 0135/1143] qml: ConfirmTxDialog layout fixes --- .../gui/qml/components/ConfirmTxDialog.qml | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index c4fce4b44..2c539ba8b 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -57,6 +57,8 @@ ElDialog { Label { id: amountLabel + Layout.fillWidth: true + Layout.minimumWidth: implicitWidth text: qsTr('Amount to send') color: Material.accentColor } @@ -135,31 +137,39 @@ ElDialog { text: finalizer.target } - Slider { - id: feeslider - leftPadding: constants.paddingMedium - snapMode: Slider.SnapOnRelease - stepSize: 1 - from: 0 - to: finalizer.sliderSteps - onValueChanged: { - if (activeFocus) - finalizer.sliderPos = value - } - Component.onCompleted: { - value = finalizer.sliderPos - } - Connections { - target: finalizer - function onSliderPosChanged() { - feeslider.value = finalizer.sliderPos + RowLayout { + Layout.columnSpan: 2 + Layout.fillWidth: true + + Slider { + id: feeslider + Layout.fillWidth: true + leftPadding: constants.paddingMedium + + snapMode: Slider.SnapOnRelease + stepSize: 1 + from: 0 + to: finalizer.sliderSteps + + onValueChanged: { + if (activeFocus) + finalizer.sliderPos = value + } + Component.onCompleted: { + value = finalizer.sliderPos + } + Connections { + target: finalizer + function onSliderPosChanged() { + feeslider.value = finalizer.sliderPos + } } } - } - FeeMethodComboBox { - id: target - feeslider: finalizer + FeeMethodComboBox { + id: target + feeslider: finalizer + } } InfoTextArea { From 1bfc4f1529b0b0b5649c362748ee96bcc45590e0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 11:36:36 +0100 Subject: [PATCH 0136/1143] qml: android notification params --- electrum/gui/qml/qeapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index b3fd7636e..f3ab6dedc 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -116,7 +116,7 @@ def on_notification_timer(self): except queue.Empty: pass - def notifyAndroid(self, message): + def notifyAndroid(self, wallet_name, message): try: # TODO: lazy load not in UI thread please global notification From 8ccc4801f7b3aa8b5d0ba83f2b8783500f471faa Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 13:28:27 +0100 Subject: [PATCH 0137/1143] qml: ConfirmTxDialog styling slider, avoid running off small screens --- electrum/gui/qml/components/ConfirmTxDialog.qml | 5 +++-- electrum/gui/qml/components/WalletMainView.qml | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 2c539ba8b..73097766f 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -174,8 +174,9 @@ ElDialog { InfoTextArea { Layout.columnSpan: 2 - Layout.preferredWidth: parent.width * 3/4 - Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge visible: finalizer.warning != '' text: finalizer.warning iconStyle: InfoTextArea.IconStyle.Warn diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 09fe37deb..569e17e43 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -327,6 +327,10 @@ Item { _confirmPaymentDialog.destroy() } } + // TODO: lingering confirmPaymentDialogs can raise exceptions in + // the child finalizer when currentWallet disappears, but we need + // it long enough for the finalizer to finish.. + // onClosed: destroy() } } From 2d5ba84e35d047d58170569d5aa116fa9ed4a8e1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 14:11:31 +0100 Subject: [PATCH 0138/1143] qml: styling InfoTextArea --- electrum/gui/qml/components/Constants.qml | 6 +- .../qml/components/controls/InfoTextArea.qml | 66 ++++++++----------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index d4a24e6e3..1852d4a46 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -35,9 +35,11 @@ Item { property color colorCredit: "#ff80ff80" property color colorDebit: "#ffff8080" - property color colorMine: "yellow" - property color colorError: '#ffff8080' + property color colorInfo: Material.accentColor property color colorWarning: 'yellow' + property color colorError: '#ffff8080' + + property color colorMine: "yellow" property color colorLightningLocal: "blue" property color colorLightningRemote: "yellow" property color colorChannelOpen: "#ff80ff80" diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index 0bca39421..eb64c5aa9 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts 1.0 import QtQuick.Controls 2.1 import QtQuick.Controls.Material 2.0 -Item { +TextHighlightPane { enum IconStyle { None, Info, @@ -15,50 +15,36 @@ Item { property int iconStyle: InfoTextArea.IconStyle.Info property alias textFormat: infotext.textFormat - implicitHeight: layout.height - - ColumnLayout { - id: layout - - spacing: 0 + // borderColor: constants.colorWarning + borderColor: iconStyle == InfoTextArea.IconStyle.Info + ? constants.colorInfo + : iconStyle == InfoTextArea.IconStyle.Warn + ? constants.colorWarning + : iconStyle == InfoTextArea.IconStyle.Error + ? constants.colorError + : constants.colorInfo + padding: constants.paddingXLarge + + RowLayout { width: parent.width - - Rectangle { - height: 2 - Layout.fillWidth: true - color: Qt.rgba(1,1,1,0.25) + Image { + source: iconStyle == InfoTextArea.IconStyle.Info + ? "../../../icons/info.png" + : iconStyle == InfoTextArea.IconStyle.Warn + ? "../../../icons/warning.png" + : iconStyle == InfoTextArea.IconStyle.Error + ? "../../../icons/expired.png" + : "" + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium } + Label { - TextArea { id: infotext Layout.fillWidth: true - Layout.minimumHeight: constants.iconSizeLarge + 2*constants.paddingLarge - readOnly: true - rightPadding: constants.paddingLarge - leftPadding: 2*constants.iconSizeLarge - wrapMode: TextInput.Wrap - textFormat: TextEdit.RichText - background: Rectangle { - color: Qt.rgba(1,1,1,0.05) // whiten 5% - } - - Image { - source: iconStyle == InfoTextArea.IconStyle.Info ? "../../../icons/info.png" : iconStyle == InfoTextArea.IconStyle.Warn ? "../../../icons/warning.png" : iconStyle == InfoTextArea.IconStyle.Error ? "../../../icons/expired.png" : "" - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: constants.paddingLarge - anchors.topMargin: constants.paddingLarge - height: constants.iconSizeLarge - width: constants.iconSizeLarge - fillMode: Image.PreserveAspectCrop - } - - } - - Rectangle { - height: 2 - Layout.fillWidth: true - color: Qt.rgba(0,0,0,0.25) + width: parent.width + text: invoice.userinfo + wrapMode: Text.Wrap } } } From fc212b1dcc880dd45b18fb37eed848532b03bae1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 14:14:23 +0100 Subject: [PATCH 0139/1143] qml: improve OpenWalletDialog, PasswordDialog and PasswordField --- .../gui/qml/components/OpenWalletDialog.qml | 96 +++++++++---------- .../gui/qml/components/PasswordDialog.qml | 31 +++--- electrum/gui/qml/components/WalletDetails.qml | 12 +-- .../qml/components/controls/PasswordField.qml | 7 ++ 4 files changed, 79 insertions(+), 67 deletions(-) diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index 45d159a87..3c9424646 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -10,16 +10,14 @@ import "controls" ElDialog { id: openwalletdialog - width: parent.width * 4/5 - - anchors.centerIn: parent - - title: qsTr("Open Wallet") - iconSource: '../../../icons/wallet.png' - property string name property string path + property bool _unlockClicked: false + + title: qsTr('Open Wallet') + iconSource: Qt.resolvedUrl('../../icons/wallet.png') + modal: true parent: Overlay.overlay Overlay.modal: Rectangle { @@ -28,45 +26,43 @@ ElDialog { focus: true - property bool _unlockClicked: false + width: parent.width * 4/5 + anchors.centerIn: parent + + padding: 0 ColumnLayout { - id: rootLayout + spacing: 0 width: parent.width - spacing: constants.paddingLarge - - Label { - Layout.alignment: Qt.AlignHCenter - text: name - } - Item { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: passwordLayout.width - Layout.preferredHeight: notice.height + ColumnLayout { + id: rootLayout + Layout.fillWidth: true + Layout.leftMargin: constants.paddingXXLarge + Layout.rightMargin: constants.paddingXXLarge + spacing: constants.paddingLarge InfoTextArea { id: notice - text: qsTr("Wallet requires password to unlock") - visible: wallet_db.needsPassword + text: qsTr("Wallet %1 requires password to unlock").arg(name) + // visible: false //wallet_db.needsPassword iconStyle: InfoTextArea.IconStyle.Warn - width: parent.width + Layout.fillWidth: true } - } - RowLayout { - id: passwordLayout - Layout.alignment: Qt.AlignHCenter Label { text: qsTr('Password') visible: wallet_db.needsPassword Layout.fillWidth: true + color: Material.accentColor } PasswordField { id: password - visible: wallet_db.needsPassword Layout.fillWidth: true + Layout.leftMargin: constants.paddingXLarge + visible: wallet_db.needsPassword + onTextChanged: { unlockButton.enabled = true _unlockClicked = false @@ -75,18 +71,33 @@ ElDialog { unlock() } } - } - Label { - Layout.alignment: Qt.AlignHCenter - text: !wallet_db.validPassword && _unlockClicked ? qsTr("Invalid Password") : '' - color: constants.colorError - font.pixelSize: constants.fontSizeLarge + Label { + Layout.alignment: Qt.AlignHCenter + text: !wallet_db.validPassword && _unlockClicked ? qsTr("Invalid Password") : '' + color: constants.colorError + font.pixelSize: constants.fontSizeLarge + } + + Label { + Layout.alignment: Qt.AlignHCenter + visible: wallet_db.requiresSplit + text: qsTr('Wallet requires splitting') + font.pixelSize: constants.fontSizeLarge + } + + FlatButton { + Layout.alignment: Qt.AlignHCenter + visible: wallet_db.requiresSplit + text: qsTr('Split wallet') + onClicked: wallet_db.doSplit() + } } FlatButton { id: unlockButton - Layout.alignment: Qt.AlignHCenter + // Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true visible: wallet_db.needsPassword icon.source: '../../icons/unlock.png' text: qsTr("Unlock") @@ -95,20 +106,6 @@ ElDialog { } } - Label { - Layout.alignment: Qt.AlignHCenter - visible: wallet_db.requiresSplit - text: qsTr('Wallet requires splitting') - font.pixelSize: constants.fontSizeLarge - } - - FlatButton { - Layout.alignment: Qt.AlignHCenter - visible: wallet_db.requiresSplit - text: qsTr('Split wallet') - onClicked: wallet_db.doSplit() - } - } function unlock() { @@ -135,6 +132,9 @@ ElDialog { onInvalidPassword: { password.tf.forceActiveFocus() } + onNeedsPasswordChanged: { + notice.visible = needsPassword + } } Component.onCompleted: { diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index e1a5455aa..276f1a4a3 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -11,7 +11,7 @@ ElDialog { id: passworddialog title: qsTr("Enter Password") - iconSource: '../../../icons/lock.png' + iconSource: Qt.resolvedUrl('../../icons/lock.png') property bool confirmPassword: false property string password @@ -21,6 +21,7 @@ ElDialog { modal: true anchors.centerIn: parent + width: parent.width * 4/5 padding: 0 Overlay.modal: Rectangle { @@ -28,37 +29,43 @@ ElDialog { } ColumnLayout { + id: rootLayout width: parent.width spacing: 0 - InfoTextArea { - visible: infotext - text: infotext - Layout.margins: constants.paddingMedium - Layout.fillWidth: true - } - - GridLayout { + ColumnLayout { id: password_layout - columns: 2 - Layout.fillWidth: true - Layout.margins: constants.paddingXXLarge + Layout.leftMargin: constants.paddingXXLarge + Layout.rightMargin: constants.paddingXXLarge + + InfoTextArea { + visible: infotext + text: infotext + Layout.bottomMargin: constants.paddingMedium + Layout.fillWidth: true + } Label { + Layout.fillWidth: true text: qsTr('Password') + color: Material.accentColor } PasswordField { id: pw_1 + Layout.leftMargin: constants.paddingXLarge } Label { + Layout.fillWidth: true text: qsTr('Password (again)') visible: confirmPassword + color: Material.accentColor } PasswordField { id: pw_2 + Layout.leftMargin: constants.paddingXLarge visible: confirmPassword } } diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index d9e83fbb1..7ab98b254 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -543,13 +543,11 @@ Pane { Connections { target: Daemon.currentWallet function onRequestNewPassword() { // new wallet password - var dialog = app.passwordDialog.createObject(app, - { - 'confirmPassword': true, - 'title': qsTr('Enter new password'), - 'infotext': qsTr('If you forget your password, you\'ll need to\ - restore from seed. Please make sure you have your seed stored safely') - } ) + var dialog = app.passwordDialog.createObject(app, { + 'confirmPassword': true, + 'title': qsTr('Enter new password'), + 'infotext': qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') + }) dialog.accepted.connect(function() { Daemon.currentWallet.set_password(dialog.password) }) diff --git a/electrum/gui/qml/components/controls/PasswordField.qml b/electrum/gui/qml/components/controls/PasswordField.qml index 9318b73c9..019a928ba 100644 --- a/electrum/gui/qml/components/controls/PasswordField.qml +++ b/electrum/gui/qml/components/controls/PasswordField.qml @@ -13,6 +13,7 @@ RowLayout { echoMode: TextInput.Password inputMethodHints: Qt.ImhSensitiveData Layout.fillWidth: true + Layout.minimumWidth: fontMetrics.advanceWidth('X') * 16 onAccepted: root.accepted() } ToolButton { @@ -21,4 +22,10 @@ RowLayout { password_tf.echoMode = password_tf.echoMode == TextInput.Password ? TextInput.Normal : TextInput.Password } } + + FontMetrics { + id: fontMetrics + font: password_tf.font + } + } From e9ad9986d75e3bdb10a16f1317932c2ee01a2358 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 14:27:39 +0100 Subject: [PATCH 0140/1143] qml: qedaemon doesn't need wallet path and name properties, pass them via the signal --- electrum/gui/qml/components/main.qml | 5 +++-- electrum/gui/qml/qedaemon.py | 11 +++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index d39d8b3eb..380086160 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -345,9 +345,9 @@ ApplicationWindow Connections { target: Daemon - function onWalletRequiresPassword() { + function onWalletRequiresPassword(name, path) { console.log('wallet requires password') - var dialog = openWalletDialog.createObject(app, { path: Daemon.path }) + var dialog = openWalletDialog.createObject(app, { path: path, name: name }) dialog.open() } function onWalletOpenError(error) { @@ -445,6 +445,7 @@ ApplicationWindow property bool _lockDialogShown: false onActiveChanged: { + console.log('app active = ' + active) if (!active) { // deactivated _lastActive = Date.now() diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 8ea470476..04448c2d2 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -114,6 +114,7 @@ class QEDaemon(AuthMixin, QObject): _new_wallet_wizard = None _server_connect_wizard = None _path = None + _name = None _use_single_password = False _password = None @@ -123,7 +124,7 @@ class QEDaemon(AuthMixin, QObject): serverConnectWizardChanged = pyqtSignal() walletLoaded = pyqtSignal() - walletRequiresPassword = pyqtSignal() + walletRequiresPassword = pyqtSignal([str,str], arguments=['name','path']) walletOpenError = pyqtSignal([str], arguments=["error"]) walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message']) @@ -137,7 +138,7 @@ def __init__(self, daemon, parent=None): @pyqtSlot() def passwordValidityCheck(self): if not self._walletdb._validPassword: - self.walletRequiresPassword.emit() + self.walletRequiresPassword.emit(self._name, self._path) @pyqtSlot() @pyqtSlot(str) @@ -153,6 +154,8 @@ def load_wallet(self, path=None, password=None): return self._path = standardize_path(self._path) + self._name = os.path.basename(self._path) + self._logger.debug('load wallet ' + str(self._path)) if not password: @@ -230,10 +233,6 @@ def delete_wallet(self, wallet): self.availableWallets.remove_wallet(path) - @pyqtProperty('QString') - def path(self): - return self._path - @pyqtProperty(QEWallet, notify=walletLoaded) def currentWallet(self): return self._current_wallet From f617887509c92833ffaa6db25d8ed0497acb9ddb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 10 Feb 2023 16:30:08 +0100 Subject: [PATCH 0141/1143] RBF dialog: do not decrease payment for swap funding transactions. --- electrum/gui/qt/rbf_dialog.py | 3 +++ electrum/submarine_swaps.py | 9 +++++++-- electrum/wallet.py | 13 ++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index 34f5f3ec6..d3ec31a89 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -58,6 +58,9 @@ def __init__( if len(invoices) == 1 and len(invoices[0].outputs) == 1: if invoices[0].outputs[0].value == '!': self.set_decrease_payment() + # do not decrease payment if it is a swap + if self.wallet.get_swap_by_funding_tx(self.old_tx): + self.method_combo.setEnabled(False) def create_grid(self): self.method_label = QLabel(_('Method') + ':') diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 7779c8988..fe6c20c8f 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -594,8 +594,13 @@ def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Op f"recv_amount={recv_amount} -> send_amount={send_amount} -> inverted_recv_amount={inverted_recv_amount}") return send_amount - def get_swap_by_tx(self, tx: Transaction) -> Optional[SwapData]: - # determine if tx is spending from a swap + def get_swap_by_funding_tx(self, tx: Transaction) -> Optional[SwapData]: + if len(tx.outputs()) != 1: + return False + prevout = TxOutpoint(txid=bytes.fromhex(tx.txid()), out_idx=0) + return self._swaps_by_funding_outpoint.get(prevout) + + def get_swap_by_claim_tx(self, tx: Transaction) -> Optional[SwapData]: txin = tx.inputs()[0] return self.get_swap_by_claim_txin(txin) diff --git a/electrum/wallet.py b/electrum/wallet.py index a89be2734..913611663 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -710,8 +710,11 @@ def is_lightning_funding_tx(self, txid: Optional[str]) -> bool: return any([chan.funding_outpoint.txid == txid for chan in self.lnworker.channels.values()]) - def is_swap_tx(self, tx: Transaction) -> bool: - return bool(self.lnworker.swap_manager.get_swap_by_tx(tx)) if self.lnworker else False + def get_swap_by_claim_tx(self, tx: Transaction) -> bool: + return self.lnworker.swap_manager.get_swap_by_claim_tx(tx) if self.lnworker else None + + def get_swap_by_funding_tx(self, tx: Transaction) -> bool: + return bool(self.lnworker.swap_manager.get_swap_by_funding_tx(tx)) if self.lnworker else None def get_wallet_delta(self, tx: Transaction) -> TxWalletDelta: """Return the effect a transaction has on the wallet. @@ -757,7 +760,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: tx_wallet_delta = self.get_wallet_delta(tx) is_relevant = tx_wallet_delta.is_relevant is_any_input_ismine = tx_wallet_delta.is_any_input_ismine - is_swap = self.is_swap_tx(tx) + is_swap = bool(self.get_swap_by_claim_tx(tx)) fee = tx_wallet_delta.fee exp_n = None can_broadcast = False @@ -2180,7 +2183,7 @@ def can_sign(self, tx: Transaction) -> bool: for k in self.get_keystores(): if k.can_sign_txin(txin): return True - if self.is_swap_tx(tx): + if self.get_swap_by_claim_tx(tx): return True return False @@ -2235,7 +2238,7 @@ def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransac if not isinstance(tx, PartialTransaction): return # note: swap signing does not require the password - swap = self.lnworker.swap_manager.get_swap_by_tx(tx) if self.lnworker else None + swap = self.get_swap_by_claim_tx(tx) if swap: self.lnworker.swap_manager.sign_tx(tx, swap) return From d7f48c8805e1800c76a3c2a5730478b7412ae481 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 10 Feb 2023 17:13:02 +0100 Subject: [PATCH 0142/1143] Qt history tab: create submenu for edit actions --- electrum/gui/qt/history_list.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 38e3dbd3e..9d5e22539 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -728,12 +728,13 @@ def create_menu(self, position: QPoint): menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) cc = self.add_copy_menu(menu, idx) cc.addAction(_("Transaction ID"), lambda: self.place_text_on_clipboard(tx_hash, title="TXID")) + menu_edit = menu.addMenu(_("Edit")) for c in self.editable_columns: if self.isColumnHidden(c): continue label = self.hm.headerData(c, Qt.Horizontal, Qt.DisplayRole) # TODO use siblingAtColumn when min Qt version is >=5.11 persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) - menu.addAction(_("Edit {}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) + menu_edit.addAction(_("{}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) menu.addAction(_("View Transaction"), lambda: self.show_transaction(tx_item, tx)) channel_id = tx_item.get('channel_id') if channel_id and self.wallet.lnworker and (chan := self.wallet.lnworker.get_channel_by_id(bytes.fromhex(channel_id))): From 8a8703d5eaa73e4e810ac9c7377b4ea6bd583283 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Feb 2023 17:24:14 +0100 Subject: [PATCH 0143/1143] qml: styling --- electrum/gui/qml/components/History.qml | 5 ++--- electrum/gui/qml/components/InvoiceDialog.qml | 22 +++++-------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index f8c6d0f8f..e47345666 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -59,7 +59,7 @@ Pane { text: listview.sectionLabels[section] Layout.alignment: Qt.AlignHCenter Layout.topMargin: constants.paddingLarge - font.pixelSize: constants.fontSizeLarge + font.pixelSize: constants.fontSizeMedium color: Material.accentColor } } @@ -76,8 +76,7 @@ Pane { DelegateModelGroup { name: 'older'; includeByDefault: false } ] - delegate: HistoryItemDelegate { - } + delegate: HistoryItemDelegate { } } ScrollIndicator.vertical: ScrollIndicator { } diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index bcef5a0c2..941349d01 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -50,26 +50,14 @@ ElDialog { columns: 2 - TextHighlightPane { + InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true - + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge visible: invoice.userinfo - borderColor: constants.colorWarning - padding: constants.paddingXLarge - - RowLayout { - Image { - source: '../../icons/warning.png' - Layout.preferredWidth: constants.iconSizeMedium - Layout.preferredHeight: constants.iconSizeMedium - } - Label { - width: parent.width - text: invoice.userinfo - wrapMode: Text.Wrap - } - } + text: invoice.userinfo + iconStyle: InfoTextArea.IconStyle.Warn } Label { From 56cdc4a92bbb3f7c321589321a22f2e3ee718b81 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 11 Feb 2023 09:48:39 +0100 Subject: [PATCH 0144/1143] qml: fix display of negative millisat amounts in FormattedAmount, qefx.py --- electrum/gui/qml/components/controls/FormattedAmount.qml | 2 +- electrum/gui/qml/qefx.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/FormattedAmount.qml b/electrum/gui/qml/components/controls/FormattedAmount.qml index 52327d7ff..04e342d89 100644 --- a/electrum/gui/qml/components/controls/FormattedAmount.qml +++ b/electrum/gui/qml/components/controls/FormattedAmount.qml @@ -24,7 +24,7 @@ GridLayout { } Label { visible: valid - text: amount.msatsInt > 0 ? Config.formatMilliSats(amount) : Config.formatSats(amount) + text: amount.msatsInt != 0 ? Config.formatMilliSats(amount) : Config.formatSats(amount) font.family: FixedFont } Label { diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index 3c31e5a9d..9cd3741ff 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -101,7 +101,7 @@ def enabled(self, enable): def fiatValue(self, satoshis, plain=True): rate = self.fx.exchange_rate() if isinstance(satoshis, QEAmount): - satoshis = satoshis.msatsInt / 1000 if satoshis.msatsInt > 0 else satoshis.satsInt + satoshis = satoshis.msatsInt / 1000 if satoshis.msatsInt != 0 else satoshis.satsInt else: try: sd = Decimal(satoshis) From b5f0be2d8d6398c7fe6dab0d2882da19aaa242d8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 11 Feb 2023 09:51:24 +0100 Subject: [PATCH 0145/1143] qml: use FormattedAmount for fee in LightningPaymentDetails --- .../gui/qml/components/LightningPaymentDetails.qml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index 2a120d19a..c1cacfa81 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -68,15 +68,9 @@ Pane { color: Material.accentColor } - RowLayout { + FormattedAmount { visible: lnpaymentdetails.amount.msatsInt < 0 - Label { - text: Config.formatMilliSats(lnpaymentdetails.fee) - } - Label { - text: Config.baseUnit - color: Material.accentColor - } + amount: lnpaymentdetails.fee } Label { From 095b6dab0f623006e9f46e19ae01c9fdca1e7c9d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 11 Feb 2023 09:56:09 +0100 Subject: [PATCH 0146/1143] qml: fix display of negative millisat amounts in qefx.py for historic amounts too --- electrum/gui/qml/qefx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index 9cd3741ff..ba1e0d1c7 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -118,7 +118,7 @@ def fiatValue(self, satoshis, plain=True): @pyqtSlot(QEAmount, str, bool, result=str) def fiatValueHistoric(self, satoshis, timestamp, plain=True): if isinstance(satoshis, QEAmount): - satoshis = satoshis.msatsInt / 1000 if satoshis.msatsInt > 0 else satoshis.satsInt + satoshis = satoshis.msatsInt / 1000 if satoshis.msatsInt != 0 else satoshis.satsInt else: try: sd = Decimal(satoshis) From 215629235d6d5ce079a3e87632da98b0b837139a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Feb 2023 10:21:01 +0100 Subject: [PATCH 0147/1143] submarine_swaps: fix bugs and create method for max_amount_forward_swap --- electrum/gui/qt/swap_dialog.py | 9 +++++---- electrum/submarine_swaps.py | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 6cfd1060f..62d111882 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -138,10 +138,11 @@ def _spend_max_forward_swap(self): self._update_tx('!') if self.tx: amount = self.tx.output_value_for_address(ln_dummy_address()) - max_swap_amt = self.swap_manager.get_max_amount() - max_recv_amt_ln = int(self.swap_manager.num_sats_can_receive()) - max_recv_amt_oc = self.swap_manager.get_send_amount(max_recv_amt_ln, is_reverse=False) or float('inf') - max_amt = int(min(max_swap_amt, max_recv_amt_oc)) + max_amt = self.swap_manager.max_amount_forward_swap() + if max_amt is None: + self.send_amount_e.setAmount(None) + self.max_button.setChecked(False) + return if amount > max_amt: amount = max_amt self._update_tx(amount) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index fe6c20c8f..2124ca73d 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -639,3 +639,12 @@ def sign_tx(self, tx: PartialTransaction, swap: SwapData) -> None: sig = bytes.fromhex(tx.sign_txin(0, swap.privkey)) witness = [sig, preimage, witness_script] txin.witness = bytes.fromhex(construct_witness(witness)) + + def max_amount_forward_swap(self) -> Optional[int]: + """ returns None if we cannot swap """ + max_swap_amt_ln = self.get_max_amount() + max_recv_amt_ln = int(self.num_sats_can_receive()) + max_amt_ln = int(min(max_swap_amt_ln, max_recv_amt_ln)) + max_amt_oc = self.get_send_amount(max_amt_ln, is_reverse=False) or 0 + min_amt_oc = self.get_send_amount(self.min_amount, is_reverse=False) or 0 + return max_amt_oc if max_amt_oc >= min_amt_oc else None From 5e88b0da88686cfea6c30c475f6b1f32a087fdb6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Feb 2023 10:27:12 +0100 Subject: [PATCH 0148/1143] swaps: cache pairs to file --- electrum/submarine_swaps.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 2124ca73d..1ffeca188 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -148,8 +148,8 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): self.normal_fee = 0 self.lockup_fee = 0 self.percentage = 0 - self.min_amount = 0 - self._max_amount = 0 + self._min_amount = None + self._max_amount = None self.wallet = wallet self.lnworker = lnworker @@ -170,6 +170,8 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): self.api_url = API_URL_TESTNET else: self.api_url = API_URL_REGTEST + # init default min & max + self.init_min_max_values() def start_network(self, *, network: 'Network', lnwatcher: 'LNWalletWatcher'): assert network @@ -491,19 +493,40 @@ async def get_pairs(self) -> None: raise SwapServerError() from e # we assume server response is well-formed; otherwise let an exception propagate to the crash reporter pairs = json.loads(response) + # cache data to disk + with open(self.pairs_filename(), 'w', encoding='utf-8') as f: + f.write(json.dumps(pairs)) fees = pairs['pairs']['BTC/BTC']['fees'] self.percentage = fees['percentage'] self.normal_fee = fees['minerFees']['baseAsset']['normal'] self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] limits = pairs['pairs']['BTC/BTC']['limits'] - self.min_amount = limits['minimal'] + self._min_amount = limits['minimal'] self._max_amount = limits['maximal'] + def pairs_filename(self): + return os.path.join(self.wallet.config.path, 'swap_pairs') + + def init_min_max_values(self): + # use default values if we never requested pairs + try: + with open(self.pairs_filename(), 'r', encoding='utf-8') as f: + pairs = json.loads(f.read()) + limits = pairs['pairs']['BTC/BTC']['limits'] + self._min_amount = limits['minimal'] + self._max_amount = limits['maximal'] + except: + self._min_amount = 10000 + self._max_amount = 10000000 + def get_max_amount(self): return self._max_amount + def get_min_amount(self): + return self._min_amount + def check_invoice_amount(self, x): - return x >= self.min_amount and x <= self._max_amount + return x >= self.get_min_amount() and x <= self.get_max_amount() def _get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]: """For a given swap direction and amount we send, returns how much we will receive. @@ -646,5 +669,5 @@ def max_amount_forward_swap(self) -> Optional[int]: max_recv_amt_ln = int(self.num_sats_can_receive()) max_amt_ln = int(min(max_swap_amt_ln, max_recv_amt_ln)) max_amt_oc = self.get_send_amount(max_amt_ln, is_reverse=False) or 0 - min_amt_oc = self.get_send_amount(self.min_amount, is_reverse=False) or 0 + min_amt_oc = self.get_send_amount(self.get_min_amount(), is_reverse=False) or 0 return max_amt_oc if max_amt_oc >= min_amt_oc else None From df842af0b6b48074a510155fbfd28df295c200d4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 6 Jan 2023 17:24:30 +0100 Subject: [PATCH 0149/1143] UTXO tab: add menus to fully spend utxo: - send to address - in new channel - in submarine swap This is easier than coin control, because it does not involve switching tabs. Coin control is activated during the operation, so that users learn how it works. --- electrum/gui/qt/swap_dialog.py | 8 +++-- electrum/gui/qt/utxo_list.py | 65 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 62d111882..259ab95ec 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -95,8 +95,12 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No self.update() def init_recv_amount(self, recv_amount_sat): - recv_amount_sat = max(recv_amount_sat, self.swap_manager.min_amount) - self.recv_amount_e.setAmount(recv_amount_sat) + if recv_amount_sat == '!': + self.max_button.setChecked(True) + self.spend_max() + else: + recv_amount_sat = max(recv_amount_sat, self.swap_manager.min_amount) + self.recv_amount_e.setAmount(recv_amount_sat) def fee_slider_callback(self, dyn, pos, fee_rate): if dyn: diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 535c7640f..5e9d11bed 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -32,9 +32,12 @@ from PyQt5.QtWidgets import QAbstractItemView, QMenu, QLabel, QHBoxLayout from electrum.i18n import _ -from electrum.transaction import PartialTxInput +from electrum.bitcoin import is_address +from electrum.transaction import PartialTxInput, PartialTxOutput +from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton +from .new_channel_dialog import NewChannelDialog class UTXOList(MyTreeView): @@ -183,6 +186,57 @@ def _maybe_reset_coincontrol(self, current_wallet_utxos: Sequence[PartialTxInput if not all([prevout_str in utxo_set for prevout_str in self._spend_set]): self._spend_set.clear() + def can_swap_coins(self, coins): + # fixme: min and max_amounts are known only after first request + if self.wallet.lnworker is None: + return False + value = sum(x.value_sats() for x in coins) + min_amount = self.wallet.lnworker.swap_manager.get_min_amount() + max_amount = self.wallet.lnworker.swap_manager.max_amount_forward_swap() + if value < min_amount: + return False + if value > max_amount: + return False + return True + + def swap_coins(self, coins): + #self.clear_coincontrol() + self.add_to_coincontrol(coins) + self.parent.run_swap_dialog(is_reverse=False, recv_amount_sat='!') + self.clear_coincontrol() + + def can_open_channel(self, coins): + if self.wallet.lnworker is None: + return False + value = sum(x.value_sats() for x in coins) + return value >= MIN_FUNDING_SAT and value <= LN_MAX_FUNDING_SAT + + def open_channel_with_coins(self, coins): + # todo : use a single dialog in new flow + #self.clear_coincontrol() + self.add_to_coincontrol(coins) + d = NewChannelDialog(self.parent) + d.max_button.setChecked(True) + d.max_button.setEnabled(False) + d.min_button.setEnabled(False) + d.clear_button.setEnabled(False) + d.amount_e.setFrozen(True) + d.spend_max() + d.run() + self.clear_coincontrol() + + def clipboard_contains_address(self): + text = self.parent.app.clipboard().text() + return is_address(text) + + def pay_to_clipboard_address(self, coins): + addr = self.parent.app.clipboard().text() + outputs = [PartialTxOutput.from_address_and_value(addr, '!')] + #self.clear_coincontrol() + self.add_to_coincontrol(coins) + self.parent.send_tab.pay_onchain_dialog(coins, outputs) + self.clear_coincontrol() + def create_menu(self, position): selected = self.get_selected_outpoints() if selected is None: @@ -190,8 +244,15 @@ def create_menu(self, position): menu = QMenu() menu.setSeparatorsCollapsible(True) # consecutive separators are merged together coins = [self._utxo_dict[name] for name in selected] - # coin control if coins: + menu_spend = menu.addMenu(_("Fully spend") + '…') + m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) + m.setEnabled(self.clipboard_contains_address()) + m = menu_spend.addAction(_("in new channel"), lambda: self.open_channel_with_coins(coins)) + m.setEnabled(self.can_open_channel(coins)) + m = menu_spend.addAction(_("in submarine swap"), lambda: self.swap_coins(coins)) + m.setEnabled(self.can_swap_coins(coins)) + # coin control if self.are_in_coincontrol(coins): menu.addAction(_("Remove from coin control"), lambda: self.remove_from_coincontrol(coins)) else: From d766f2fd9ef060f3247d939b9962e55ce94b678f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Feb 2023 11:13:03 +0100 Subject: [PATCH 0150/1143] Qt: make copy menus more consistent --- electrum/gui/qt/history_list.py | 4 +-- electrum/gui/qt/invoice_list.py | 7 ++--- electrum/gui/qt/request_list.py | 8 +++--- electrum/gui/qt/util.py | 4 ++- electrum/gui/qt/utxo_list.py | 45 +++++++++++++++++---------------- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 9d5e22539..66476723f 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -726,8 +726,8 @@ def create_menu(self, position: QPoint): menu = QMenu() if tx_details.can_remove: menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) - cc = self.add_copy_menu(menu, idx) - cc.addAction(_("Transaction ID"), lambda: self.place_text_on_clipboard(tx_hash, title="TXID")) + copy_menu = self.add_copy_menu(menu, idx) + copy_menu.addAction(_("Transaction ID"), lambda: self.place_text_on_clipboard(tx_hash, title="TXID")) menu_edit = menu.addMenu(_("Edit")) for c in self.editable_columns: if self.isColumnHidden(c): continue diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 9a928d4f0..6a5375ae9 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -157,12 +157,13 @@ def create_menu(self, position): key = item_col0.data(ROLE_REQUEST_ID) invoice = self.wallet.get_invoice(key) menu = QMenu(self) - self.add_copy_menu(menu, idx) + copy_menu = self.add_copy_menu(menu, idx) + address = invoice.get_address() + if address: + copy_menu.addAction(_("Address"), lambda: self.parent.do_copy(invoice.get_address(), title='Bitcoin Address')) if invoice.is_lightning(): menu.addAction(_("Details"), lambda: self.parent.show_lightning_invoice(invoice)) else: - if len(invoice.outputs) == 1: - menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(invoice.get_address(), title='Bitcoin Address')) menu.addAction(_("Details"), lambda: self.parent.show_onchain_invoice(invoice)) status = wallet.get_invoice_status(invoice) if status == PR_UNPAID: diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 1e3fcfad4..8d03ce713 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -194,13 +194,13 @@ def create_menu(self, position): self.update() return menu = QMenu(self) + copy_menu = self.add_copy_menu(menu, idx) if req.get_address(): - menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address')) + copy_menu.addAction(_("Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address')) if URI := self.wallet.get_request_URI(req): - menu.addAction(_("Copy URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) + copy_menu.addAction(_("Bitcoin URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) if req.is_lightning(): - menu.addAction(_("Copy Lightning Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request')) - self.add_copy_menu(menu, idx) + copy_menu.addAction(_("Lightning Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request')) #if 'view_url' in req: # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) menu.addAction(_("Delete"), lambda: self.delete_requests([key])) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index fce6f4474..677e4f2b7 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -774,8 +774,10 @@ def toggle_toolbar(self, config=None): self.show_toolbar(not self.toolbar_shown, config) def add_copy_menu(self, menu: QMenu, idx) -> QMenu: - cc = menu.addMenu(_("Copy Column")) + cc = menu.addMenu(_("Copy")) for column in self.Columns: + if self.isColumnHidden(column): + continue column_title = self.original_model().horizontalHeaderItem(column).text() if not column_title: continue diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 5e9d11bed..d5adc5327 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -244,41 +244,42 @@ def create_menu(self, position): menu = QMenu() menu.setSeparatorsCollapsible(True) # consecutive separators are merged together coins = [self._utxo_dict[name] for name in selected] - if coins: - menu_spend = menu.addMenu(_("Fully spend") + '…') - m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) - m.setEnabled(self.clipboard_contains_address()) - m = menu_spend.addAction(_("in new channel"), lambda: self.open_channel_with_coins(coins)) - m.setEnabled(self.can_open_channel(coins)) - m = menu_spend.addAction(_("in submarine swap"), lambda: self.swap_coins(coins)) - m.setEnabled(self.can_swap_coins(coins)) - # coin control - if self.are_in_coincontrol(coins): - menu.addAction(_("Remove from coin control"), lambda: self.remove_from_coincontrol(coins)) - else: - menu.addAction(_("Add to coin control"), lambda: self.add_to_coincontrol(coins)) - + if not coins: + return if len(coins) == 1: + idx = self.indexAt(position) + if not idx.isValid(): + return + self.add_copy_menu(menu, idx) utxo = coins[0] - addr = utxo.address txid = utxo.prevout.txid.hex() # "Details" tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label)) - # "Copy ..." - idx = self.indexAt(position) - if not idx.isValid(): - return - self.add_copy_menu(menu, idx) - # "Freeze coin" + # fully spend + menu_spend = menu.addMenu(_("Fully spend") + '…') + m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) + m.setEnabled(self.clipboard_contains_address()) + m = menu_spend.addAction(_("in new channel"), lambda: self.open_channel_with_coins(coins)) + m.setEnabled(self.can_open_channel(coins)) + m = menu_spend.addAction(_("in submarine swap"), lambda: self.swap_coins(coins)) + m.setEnabled(self.can_swap_coins(coins)) + # coin control + if self.are_in_coincontrol(coins): + menu.addAction(_("Remove from coin control"), lambda: self.remove_from_coincontrol(coins)) + else: + menu.addAction(_("Add to coin control"), lambda: self.add_to_coincontrol(coins)) + # Freeze menu + if len(coins) == 1: + utxo = coins[0] + addr = utxo.address menu_freeze = menu.addMenu(_("Freeze")) if not self.wallet.is_frozen_coin(utxo): menu_freeze.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True)) else: menu_freeze.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False)) - # "Freeze address" if not self.wallet.is_frozen_address(addr): menu_freeze.addAction(_("Freeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) else: From 4a9121304464462221155e46987654d9bd479af9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Feb 2023 13:28:19 +0100 Subject: [PATCH 0151/1143] minor fix --- electrum/gui/qt/utxo_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index d5adc5327..ba6415e17 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -195,7 +195,7 @@ def can_swap_coins(self, coins): max_amount = self.wallet.lnworker.swap_manager.max_amount_forward_swap() if value < min_amount: return False - if value > max_amount: + if max_amount is None or value > max_amount: return False return True From faea1e6e1a0798176850a159062ab90979c0339f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Feb 2023 01:23:47 +0000 Subject: [PATCH 0152/1143] lnchannel: add more debug logging for ctx/htlc sigs related: https://github.com/spesmilo/electrum/issues/8191 --- electrum/lnchannel.py | 25 ++++++++++++++++++++----- electrum/lnpeer.py | 1 + 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 2328e191a..6bf73ff52 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -988,10 +988,11 @@ def sign_next_commitment(self) -> Tuple[bytes, Sequence[bytes]]: """ # TODO: when more channel types are supported, this method should depend on channel type next_remote_ctn = self.get_next_ctn(REMOTE) - self.logger.info(f"sign_next_commitment {next_remote_ctn}") + self.logger.info(f"sign_next_commitment. ctn={next_remote_ctn}") pending_remote_commitment = self.get_next_commitment(REMOTE) sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE]) + self.logger.debug(f"sign_next_commitment. {pending_remote_commitment.serialize()=}. {sig_64.hex()=}") their_remote_htlc_privkey_number = derive_privkey( int.from_bytes(self.config[LOCAL].htlc_basepoint.privkey, 'big'), @@ -1039,8 +1040,12 @@ def receive_new_commitment(self, sig: bytes, htlc_sigs: Sequence[bytes]) -> None pre_hash = sha256d(bfh(preimage_hex)) if not ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, sig, pre_hash): raise LNProtocolWarning( - f'failed verifying signature of our updated commitment transaction: ' - f'{bh2u(sig)} preimage is {preimage_hex}, rawtx: {pending_local_commitment.serialize()}') + f'failed verifying signature for our updated commitment transaction. ' + f'sig={sig.hex()}. ' + f'pre_hash={pre_hash.hex()}. ' + f'pubkey={self.config[REMOTE].multisig_key.pubkey}. ' + f'ctx={pending_local_commitment.serialize()} ' + ) htlc_sigs_string = b''.join(htlc_sigs) @@ -1077,10 +1082,20 @@ def _verify_htlc_sig(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_directi commit=ctx, ctx_output_idx=ctx_output_idx, htlc=htlc) - pre_hash = sha256d(bfh(htlc_tx.serialize_preimage(0))) + preimage_hex = htlc_tx.serialize_preimage(0) + pre_hash = sha256d(bfh(preimage_hex)) remote_htlc_pubkey = derive_pubkey(self.config[REMOTE].htlc_basepoint.pubkey, pcp) if not ecc.verify_signature(remote_htlc_pubkey, htlc_sig, pre_hash): - raise LNProtocolWarning(f'failed verifying HTLC signatures: {htlc} {htlc_direction}, rawtx: {htlc_tx.serialize()}') + raise LNProtocolWarning( + f'failed verifying HTLC signatures: {htlc=}, {htlc_direction=}. ' + f'htlc_tx={htlc_tx.serialize()}. ' + f'htlc_sig={htlc_sig.hex()}. ' + f'remote_htlc_pubkey={remote_htlc_pubkey.hex()}. ' + f'pre_hash={pre_hash.hex()}. ' + f'ctx={ctx.serialize()}. ' + f'ctx_output_idx={ctx_output_idx}. ' + f'ctn={ctn}. ' + ) def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes: data = self.config[LOCAL].current_htlc_signatures diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 24e4c3de4..b69f1783c 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -767,6 +767,7 @@ async def channel_establishment_flow( # <- accept_channel payload = await self.wait_for_message('accept_channel', temp_channel_id) + self.logger.debug(f"received accept_channel for temp_channel_id={temp_channel_id.hex()}. {payload=}") remote_per_commitment_point = payload['first_per_commitment_point'] funding_txn_minimum_depth = payload['minimum_depth'] if funding_txn_minimum_depth <= 0: From 1da65451c024add7fe756d9babe68590846c02fd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Feb 2023 11:11:26 +0100 Subject: [PATCH 0153/1143] Qt: schedule tooltip in do_copy --- electrum/gui/qt/main_window.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index dd5fee6b1..18ee80232 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1082,13 +1082,11 @@ def create_receive_tab(self): from .receive_tab import ReceiveTab return ReceiveTab(self) - def do_copy(self, content: str, *, title: str = None) -> None: - self.app.clipboard().setText(content) - if title is None: - tooltip_text = _("Text copied to clipboard") - else: - tooltip_text = _("{} copied to clipboard").format(title) - QToolTip.showText(QCursor.pos(), tooltip_text, self) + def do_copy(self, text: str, *, title: str = None) -> None: + self.app.clipboard().setText(text) + message = _("Text copied to Clipboard") if title is None else _("{} copied to Clipboard").format(title) + # tooltip cannot be displayed immediately when called from a menu; wait 200ms + self.gui_object.timer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, self)) def toggle_qr_window(self): from . import qrwindow From 292ce359452b583e57a051499ef26408b8010119 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Feb 2023 12:52:42 +0100 Subject: [PATCH 0154/1143] receive tab: copy to clipboard when tab is changed --- electrum/gui/qt/receive_tab.py | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 37d28d54f..dfe3da7fb 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -161,9 +161,8 @@ def on_receive_swap(): self.receive_tabs.addTab(self.receive_URI_widget, read_QIcon("link.png"), _('URI')) self.receive_tabs.addTab(self.receive_address_widget, read_QIcon("bitcoin.png"), _('Address')) self.receive_tabs.addTab(self.receive_lightning_widget, read_QIcon("lightning.png"), _('Lightning')) - self.receive_tabs.currentChanged.connect(self.update_receive_qr_window) self.receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) - self.receive_tabs.currentChanged.connect(lambda i: self.config.set_key('receive_tabs_index', i)) + self.receive_tabs.currentChanged.connect(self.on_tab_changed) receive_tabs_sp = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) receive_tabs_sp.setRetainSizeWhenHidden(True) self.receive_tabs.setSizePolicy(receive_tabs_sp) @@ -195,6 +194,12 @@ def on_receive_swap(): vbox.setStretchFactor(self.request_list, 60) self.request_list.update() # after parented and put into a layout, can update without flickering + def on_tab_changed(self, i): + self.config.set_key('receive_tabs_index', i) + title, data = self.get_tab_data(i) + self.window.do_copy(data, title=title) + self.update_receive_qr_window() + def toggle_receive_qr(self, e): if e.button() != Qt.LeftButton: return @@ -236,7 +241,6 @@ def update_current_request(self): self.receive_tabs.setTabIcon(2, read_QIcon(icon_name)) # encode lightning invoices as uppercase so QR encoding can use # alphanumeric mode; resulting in smaller QR codes - lnaddr_qr = lnaddr.upper() self.receive_address_e.setText(addr) self.receive_address_qr.setData(addr) self.receive_address_help_text.setText(address_help) @@ -245,7 +249,7 @@ def update_current_request(self): self.receive_URI_help.setText(URI_help) self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? self.receive_lightning_help_text.setText(ln_help) - self.receive_lightning_qr.setData(lnaddr_qr) + self.receive_lightning_qr.setData(lnaddr.upper()) self.update_textedit_warning(text_e=self.receive_address_e, warning_text=address_help) self.update_textedit_warning(text_e=self.receive_URI_e, warning_text=URI_help) self.update_textedit_warning(text_e=self.receive_lightning_e, warning_text=ln_help) @@ -257,15 +261,20 @@ def update_current_request(self): self.receive_tabs.setVisible(True) self.update_receive_qr_window() + def get_tab_data(self, i): + if i == 0: + return _('Bitcoin URI'), self.receive_URI_e.text() + elif i == 1: + return _('Address'), self.receive_address_e.text() + else: + return _('Lightning Request'), self.receive_lightning_e.text() + def update_receive_qr_window(self): if self.window.qr_window and self.window.qr_window.isVisible(): i = self.receive_tabs.currentIndex() - if i == 0: - data = self.receive_URI_qr.data - elif i == 1: - data = self.receive_address_qr.data - else: - data = self.receive_lightning_qr.data + title, data = self.get_tab_data(i) + if i == 2: + data = data.upper() self.window.qr_window.qrw.setData(data) def create_invoice(self): @@ -301,11 +310,8 @@ def create_invoice(self): # clear request fields self.receive_amount_e.setText('') self.receive_message_e.setText('') - # copy to clipboard - r = self.wallet.get_request(key) - content = r.lightning_invoice if r.is_lightning() else r.get_address() - title = _('Invoice') if r.is_lightning() else _('Address') - self.window.do_copy(content, title=title) + # copy current tab to clipboard + self.on_tab_changed(self.receive_tabs.currentIndex()) def get_bitcoin_address_for_request(self, amount) -> Optional[str]: addr = self.wallet.get_unused_address() From 995754e5238d1ebd26a428f3abce4891899bf150 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Feb 2023 11:36:13 +0100 Subject: [PATCH 0155/1143] qml: add expiry timers to update status string in InvoiceDialog and ReceiveDialog --- electrum/gui/qml/components/ReceiveDialog.qml | 11 +++++- electrum/gui/qml/qeinvoice.py | 28 +++++++++++++- electrum/gui/qml/qerequestdetails.py | 37 ++++++------------- electrum/gui/qml/util.py | 20 ++++++++++ 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index e96291f07..31cc4c083 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -207,10 +207,17 @@ ElDialog { GridLayout { columns: 2 - visible: request.message || !request.amount.isEmpty + // visible: request.message || !request.amount.isEmpty Layout.maximumWidth: buttons.width Layout.alignment: Qt.AlignHCenter + Label { + text: qsTr('Status') + color: Material.accentColor + } + Label { + text: request.status_str + } Label { visible: request.message text: qsTr('Message') @@ -234,7 +241,7 @@ ElDialog { } Rectangle { - visible: request.message || !request.amount.isEmpty + // visible: request.message || !request.amount.isEmpty height: 1 Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: buttons.width diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index aa640fb1c..9e7969b9e 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -2,14 +2,14 @@ import asyncio from urllib.parse import urlparse -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS, QTimer from electrum import bitcoin from electrum import lnutil from electrum.i18n import _ from electrum.invoices import Invoice from electrum.invoices import (PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, - PR_FAILED, PR_ROUTING, PR_UNCONFIRMED) + PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, LN_EXPIRY_NEVER) from electrum.lnaddr import LnInvoiceException from electrum.logging import get_logger from electrum.transaction import PartialTxOutput @@ -20,6 +20,7 @@ from .qetypes import QEAmount from .qewallet import QEWallet +from .util import status_update_timer_interval class QEInvoice(QObject): class Type: @@ -140,6 +141,10 @@ def __init__(self, parent=None): self._amount = QEAmount() self._userinfo = '' + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self.updateStatusString) + self.clear() @pyqtProperty(int, notify=invoiceChanged) @@ -190,6 +195,10 @@ def amount(self, new_amount): self.determine_can_pay() self.invoiceChanged.emit() + @pyqtProperty('quint64', notify=invoiceChanged) + def time(self): + return self._effectiveInvoice.time if self._effectiveInvoice else 0 + @pyqtProperty('quint64', notify=invoiceChanged) def expiration(self): return self._effectiveInvoice.exp if self._effectiveInvoice else 0 @@ -268,6 +277,21 @@ def set_effective_invoice(self, invoice: Invoice): self.invoiceChanged.emit() self.statusChanged.emit() + self.set_status_timer() + + def set_status_timer(self): + if self.status != PR_EXPIRED: + if self.expiration > 0 and self.expiration != LN_EXPIRY_NEVER: + interval = status_update_timer_interval(self.time + self.expiration) + if interval > 0: + self._timer.setInterval(interval) # msec + self._timer.start() + + @pyqtSlot() + def updateStatusString(self): + self.statusChanged.emit() + self.set_status_timer() + def determine_can_pay(self): self.canPay = False self.userinfo = '' diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 59569c163..95eeac6e4 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -1,5 +1,3 @@ -from time import time - from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS from electrum.logging import get_logger @@ -8,7 +6,7 @@ from .qewallet import QEWallet from .qetypes import QEAmount -from .util import QtEventListener, event_listener +from .util import QtEventListener, event_listener, status_update_timer_interval class QERequestDetails(QObject, QtEventListener): @@ -38,6 +36,10 @@ def __init__(self, parent=None): self._timer = None self._amount = None + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self.updateStatusString) + self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) @@ -134,31 +136,16 @@ def initRequest(self): self._amount = QEAmount(from_invoice=self._req) self.detailsChanged.emit() - self.initStatusStringTimer() + self.statusChanged.emit() + self.set_status_timer() - def initStatusStringTimer(self): + def set_status_timer(self): if self.status == PR_UNPAID: if self.expiration > 0 and self.expiration != LN_EXPIRY_NEVER: - self._timer = QTimer(self) - self._timer.setSingleShot(True) - self._timer.timeout.connect(self.updateStatusString) - - # very roughly according to util.time_difference - exp_in = int(self.expiration - time()) - exp_in_min = int(exp_in/60) - - interval = 0 - if exp_in < 0: - interval = 0 - if exp_in_min < 2: - interval = 1000 - elif exp_in_min < 90: - interval = 1000 * 60 - elif exp_in_min < 1440: - interval = 1000 * 60 * 60 - + self._logger.debug(f'set_status_timer, expiration={self.expiration}') + interval = status_update_timer_interval(self.expiration) if interval > 0: - self._logger.debug(f'setting status update timer to {interval}, req expires in {exp_in} seconds') + self._logger.debug(f'setting status update timer to {interval}') self._timer.setInterval(interval) # msec self._timer.start() @@ -166,5 +153,5 @@ def initStatusStringTimer(self): @pyqtSlot() def updateStatusString(self): self.statusChanged.emit() - self.initStatusStringTimer() + self.set_status_timer() diff --git a/electrum/gui/qml/util.py b/electrum/gui/qml/util.py index 36060e0f5..0ac83721e 100644 --- a/electrum/gui/qml/util.py +++ b/electrum/gui/qml/util.py @@ -1,4 +1,5 @@ from functools import wraps +from time import time from PyQt5.QtCore import pyqtSignal @@ -27,3 +28,22 @@ def qt_event_listener(func): def decorator(self, *args): self.qt_callback_signal.emit( (func,) + args) return decorator + +# return delay in msec when expiry time string should be updated +# returns 0 when expired or expires > 1 day away (no updates needed) +def status_update_timer_interval(exp): + # very roughly according to util.time_difference + exp_in = int(exp - time()) + exp_in_min = int(exp_in/60) + + interval = 0 + if exp_in < 0: + interval = 0 + elif exp_in_min < 2: + interval = 1000 + elif exp_in_min < 90: + interval = 1000 * 60 + elif exp_in_min < 1440: + interval = 1000 * 60 * 60 + + return interval From 2b0e624876f12815af086e936ca054264fd7bb8c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Feb 2023 12:32:33 +0100 Subject: [PATCH 0156/1143] qml: styling CloseChannelDialog, InvoiceDialog --- .../gui/qml/components/CloseChannelDialog.qml | 19 ++++++++++--------- electrum/gui/qml/components/InvoiceDialog.qml | 1 - electrum/gui/qml/qechanneldetails.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index 24631ef3c..176277486 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -62,6 +62,15 @@ ElDialog { text: channeldetails.name } + Label { + text: qsTr('Short channel ID') + color: Material.accentColor + } + + Label { + text: channeldetails.short_cid + } + Label { text: qsTr('Remote node ID') Layout.columnSpan: 2 @@ -82,20 +91,12 @@ ElDialog { } } - Label { - text: qsTr('Short channel ID') - color: Material.accentColor - } - - Label { - text: channeldetails.short_cid - } - Item { Layout.preferredHeight: constants.paddingMedium; Layout.preferredWidth: 1; Layout.columnSpan: 2 } InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge text: channeldetails.message_force_close } diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 941349d01..a414b813f 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -53,7 +53,6 @@ ElDialog { InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.topMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingLarge visible: invoice.userinfo text: invoice.userinfo diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 61f39479a..8647066c7 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -154,7 +154,7 @@ def canDelete(self): @pyqtProperty(str, notify=channelChanged) def message_force_close(self, notify=channelChanged): - return _(messages.MSG_REQUEST_FORCE_CLOSE) + return _(messages.MSG_REQUEST_FORCE_CLOSE).strip() @pyqtProperty(bool, notify=channelChanged) def isBackup(self): From df2bd61de6607fa8bbc73cbf53ee1d99fac61bfd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Feb 2023 05:51:53 +0000 Subject: [PATCH 0157/1143] bip32: change hardened char "'"->"h" when encoding derivation paths We accept either when decoding - this only changes what we use when encoding. Single quotes are annoying to use in a shell, as they often need to be escaped. --- electrum/bip32.py | 4 +++- electrum/tests/test_bitcoin.py | 10 +++++----- electrum/tests/test_wallet_vertical.py | 14 +++++++------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index ad851f82a..78938d47e 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -17,6 +17,8 @@ BIP32_PRIME = 0x80000000 UINT32_MAX = (1 << 32) - 1 +BIP32_HARDENED_CHAR = "h" # default "hardened" char we put in str paths + def protect_against_invalid_ecpoint(func): def func_wrapper(*args): @@ -345,7 +347,7 @@ def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str: raise ValueError(f"bip32 path child index out of range: {child_index}") prime = "" if child_index & BIP32_PRIME: - prime = "'" + prime = BIP32_HARDENED_CHAR child_index = child_index ^ BIP32_PRIME s += str(child_index) + prime + '/' # cut trailing "/" diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index e043ec64b..ecfa4bf0b 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -799,15 +799,15 @@ def test_convert_bip32_path_to_list_of_uint32(self): self.assertEqual([2147483692, 2147488889, 221], convert_bip32_path_to_list_of_uint32("m/44'/5241h/221")) def test_convert_bip32_intpath_to_strpath(self): - self.assertEqual("m/0/1'/1'", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001])) + self.assertEqual("m/0/1h/1h", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001])) self.assertEqual("m", convert_bip32_intpath_to_strpath([])) - self.assertEqual("m/44'/5241'/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221])) + self.assertEqual("m/44h/5241h/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221])) def test_normalize_bip32_derivation(self): - self.assertEqual("m/0/1'/1'", normalize_bip32_derivation("m/0/1h/1'")) + self.assertEqual("m/0/1h/1h", normalize_bip32_derivation("m/0/1h/1'")) self.assertEqual("m", normalize_bip32_derivation("m////")) - self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/")) - self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h")) + self.assertEqual("m/0/2/1h", normalize_bip32_derivation("m/0/2/-1/")) + self.assertEqual("m/0/1h/1h/5h", normalize_bip32_derivation("m/0//-1/1'///5h")) def test_is_xkey_consistent_with_key_origin_info(self): ### actual data (high depth path) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index a7f3beace..31646defb 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -2482,7 +2482,7 @@ def test_export_psbt_with_xpubs__multisig(self, mock_save_db): self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( - {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999'/0/0"), + {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), '03cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca': ('015148ee', "m/0/0")}, tx.inputs()[0].to_json()['bip32_paths']) self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000", @@ -2496,7 +2496,7 @@ def test_export_psbt_with_xpubs__multisig(self, mock_save_db): # Note that xpub0 itself has to be changed as its serialisation includes depth/fp/child_num. self.assertEqual( {'tpubD6NzVbkrYhZ4WW1saJM1hDjGz1rm5swdKwbhcsx9hW5VVXDdbnt6GbXEQVXQq97dYsvGVeMEw5Ge2Zx4QGBy6W5KXahih4aTRs5hLqgy9c9': ('015148ee', 'm'), - 'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999'")}, + 'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999h")}, tx.to_json()['xpubs']) self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24004f01043587cf0000000000000000001044dcc4a72f0084f25ca3b7927abd5596715a515e2a59004ce10a51a17cf4b403a5b8b89c28c5a51832be51bb184749ac2ea6c561259bfc5bf58b852ad60f6fe404015148ee4f01043587cf019559fbd18000270f1b7a7db8a20f23be687941c8bcc8b330fd8823f19eea6ad5cb4af09b00cf6fd802db662ac8cf00e16cebe67e4d9f88b266eddbe0dfbb24b884bf3002b68ade721b089559fbd10f2700800001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000", tx.serialize_as_bytes().hex()) @@ -2509,15 +2509,15 @@ def test_export_psbt_with_xpubs__multisig(self, mock_save_db): self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( - {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999'/0/0"), - '03cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca': ('30cf1be5', "m/48'/1'/0'/2'/0/0")}, + {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), + '03cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca': ('30cf1be5', "m/48h/1h/0h/2h/0/0")}, tx.inputs()[0].to_json()['bip32_paths']) self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000", tx.serialize_as_bytes().hex()) tx.prepare_for_export_for_hardware_device(wallet) self.assertEqual( - {'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', "m/48'/1'/0'/2'"), - 'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999'")}, + {'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', "m/48h/1h/0h/2h"), + 'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999h")}, tx.to_json()['xpubs']) self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24004f01043587cf04b5faa014800000021044dcc4a72f0084f25ca3b7927abd5596715a515e2a59004ce10a51a17cf4b403a5b8b89c28c5a51832be51bb184749ac2ea6c561259bfc5bf58b852ad60f6fe41430cf1be5300000800100008000000080020000804f01043587cf019559fbd18000270f1b7a7db8a20f23be687941c8bcc8b330fd8823f19eea6ad5cb4af09b00cf6fd802db662ac8cf00e16cebe67e4d9f88b266eddbe0dfbb24b884bf3002b68ade721b089559fbd10f2700800001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000", tx.serialize_as_bytes().hex()) @@ -2547,7 +2547,7 @@ def test_export_psbt_with_xpubs__singlesig(self, mock_save_db): self.assertEqual("5c0d5eea8c2c12a383406bb37e6158167e44bfe6cd1ad590b7d97002cdfc9fff", tx.txid()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( - {'029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb8': ('30cf1be5', "m/84'/1'/0'/0/0")}, + {'029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb8': ('30cf1be5', "m/84h/1h/0h/0/0")}, tx.inputs()[0].to_json()['bip32_paths']) self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000", tx.serialize_as_bytes().hex()) From 2d6e34c8c289f6c8c9ab2597c20b9f5976fefd32 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Feb 2023 08:54:39 +0000 Subject: [PATCH 0158/1143] ecc: make ECPubkey.__lt__ relation strongly-connected/total Previously we had: ``` >>> import electrum >>> from electrum.ecc import POINT_AT_INFINITY >>> G = electrum.ecc.GENERATOR >>> G <= (-1 * G) False >>> (-1 * G) <= G False ``` --- electrum/ecc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/ecc.py b/electrum/ecc.py index d00a11f16..c8503593b 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -303,7 +303,9 @@ def __hash__(self): def __lt__(self, other): if not isinstance(other, ECPubkey): raise TypeError('comparison not defined for ECPubkey and {}'.format(type(other))) - return (self.x() or 0) < (other.x() or 0) + p1 = ((self.x() or 0), (self.y() or 0)) + p2 = ((other.x() or 0), (other.y() or 0)) + return p1 < p2 def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: sha256d(msg_magic(x))) -> bool: assert_bytes(message) From 2378f92a6adea7f2de6e1fa579df70ade71fc14a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 14 Feb 2023 12:17:07 +0000 Subject: [PATCH 0159/1143] tests: add more "sweep" tests for different script types --- electrum/tests/test_wallet_vertical.py | 138 ++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 31646defb..58f61606a 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1813,8 +1813,7 @@ def test_cpfp_p2wpkh(self, mock_save_db): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - def test_sweep_p2pk(self): - + def test_sweep_uncompressed_p2pk(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1841,6 +1840,141 @@ async def get_transaction(self, txid): self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid()) self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) + def test_sweep_compressed_p2pk(self): + class NetworkMock: + relay_fee = 1000 + async def listunspent_for_scripthash(self, scripthash): + if scripthash == 'cc911adb9fb939d0003a138ebdaa5195bf1d6f9172e438309ab4c00a5ebc255b': + return [{'tx_hash': '84a4a1943f7a620e0d8413f4c10877000768797a93bb106b3e7cd6fccc59b35e', 'tx_pos': 1, 'height': 2420005, 'value': 111111}] + else: + return [] + async def get_transaction(self, txid): + if txid == "84a4a1943f7a620e0d8413f4c10877000768797a93bb106b3e7cd6fccc59b35e": + return "02000000000102b7bfcd442c91134743c6e4100bb9f79456a6015de3c3920166bb0c3b7a8f7c070100000000fdffffff5ab39480d4b35ffa843691d944a8479dfe825d38b03fcb1804197482bfad80fb0100000000fdffffff02d4ec000000000000160014769114e56e0913de3719a3b00a446b78e61751f007b201000000000023210332e147520e4743299d95196afaf9db7c86fe02507d9ca89acd7a4e96a63653d5ac0247304402200387fe79ffe10cec73d9b131058d7128665f729d14597828b483842889c4f5ea02201197b2f1295e4011e2d174d53c240fd13c6351451ab961ccb3678fc21fa5323b0121023c221dfbf7c3f61b9e5f66343c1a302d6beca2a8883504b0f484faec9919636b024730440220687d387af37df458efc104ee0065262cb5ea195e526ed7a480fd16e6cf708c3a022019bd3fd9c3ca3f1a1fbeabe20547876eb4572a7339de37b706fbd55031e60428012102c9c459e58b01a864d7bb80f6d577326465a04219c48541b5f3ea556a06ca61a425ed2400" + else: + raise Exception("unexpected txid") + + privkeys = ['cUygTZe4jZLVwE4G44NznCPTeGvgsgassqucUHkAJxGC71Rst2kH',] + network = NetworkMock() + dest_addr = 'tb1q5uy5xjcn55gwdkmghht8yp3vwz3088f6e3e0em' + sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420006, tx_version=2) + loop = util.get_asyncio_loop() + tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + + tx_copy = tx_from_any(tx.serialize()) + self.assertEqual('02000000015eb359ccfcd67c3e6b10bb937a796807007708c1f413840d0e627a3f94a1a48401000000484730440220043fc85a43e918ac41e494e309fdf204ca245d260cb5ea09108b196ca65d8a09022056f852f0f521e79ab2124d7e9f779c7290329ce5628ef8e92601980b065d3eb501fdffffff017f9e010000000000160014a709434b13a510e6db68bdd672062c70a2f39d3a26ed2400', + str(tx_copy)) + self.assertEqual('968a501350b954ecb51948202b8d0613aa84123ca9b745c14e208cb14feeff59', tx_copy.txid()) + self.assertEqual('968a501350b954ecb51948202b8d0613aa84123ca9b745c14e208cb14feeff59', tx_copy.wtxid()) + + def test_sweep_uncompressed_p2pkh(self): + class NetworkMock: + relay_fee = 1000 + async def listunspent_for_scripthash(self, scripthash): + if scripthash == '71e8c6a9fd8ab498290d5ccbfe1cfe2c5dc2a389b4c036dd84e305a59c4a4d53': + return [{'tx_hash': '15a78cc7664c42f1040474763bf794d555f6092bfba97d6c276f296c2d141506', 'tx_pos': 0, 'height': -1, 'value': 222222}] + else: + return [] + async def get_transaction(self, txid): + if txid == "15a78cc7664c42f1040474763bf794d555f6092bfba97d6c276f296c2d141506": + return "02000000000101c6a49fbd701f1526c8e43025a6dda8dd235b3593cfd38af040cba3e37b474fdb0e00000000fdffffff020e640300000000001976a914f1b02b7028fb81aefbb25809a2baf8d94d0c2ba288acb9e3080000000000160014c2eee75efe6621be177f7edd8198f671d1640c2602473044022072b8a6154590704063c377af451b4d69f76cc9064085d4a0c80f08625c57628802207844164839d93ce54ce7db092bbd809d5270142b5dedc823e95400e8bdae88c6012102b6ad13f48fd679a209b7d822376550e5e694a3a2862546ceb72c4012977eac4829ed2400" + else: + raise Exception("unexpected txid") + + privkeys = ['p2pkh:91gxDahzHiJ63HXmLP7pvZrkF8i5gKBXk4VqWfhbhJjtf6Ni5NU',] + network = NetworkMock() + dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' + sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420010, tx_version=2) + loop = util.get_asyncio_loop() + tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + + tx_copy = tx_from_any(tx.serialize()) + self.assertEqual('02000000010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca715000000008a47304402206d2dae571ca2f51e0d4a8ce6a6335fa25ac09f4bbed26439124d93f035bdbb130220249dc2039f1da338a40679f0e79c25a2dc2983688e6c04753348f2aa8435e375014104b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b2987b4c862d5b687bb5328adccc69e67a17b109b6328228695a1c384573acd6199fdffffff0186500300000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071f2aed2400', + str(tx_copy)) + self.assertEqual('d62048493bf8459be5e1e3cab6caabc8f15661d02c364d8dc008297e573772bf', tx_copy.txid()) + self.assertEqual('d62048493bf8459be5e1e3cab6caabc8f15661d02c364d8dc008297e573772bf', tx_copy.wtxid()) + + def test_sweep_compressed_p2pkh(self): + class NetworkMock: + relay_fee = 1000 + async def listunspent_for_scripthash(self, scripthash): + if scripthash == '941b2ca8bd850e391abc5e024c83b773842c40268a8fa8a5ef7aeca19fb395c5': + return [{'tx_hash': '8a764102b4a5c5d1b5235e6ce7e67ed3c146130f8a52e7692a151e2e5a831767', 'tx_pos': 0, 'height': -1, 'value': 123456}] + else: + return [] + async def get_transaction(self, txid): + if txid == "8a764102b4a5c5d1b5235e6ce7e67ed3c146130f8a52e7692a151e2e5a831767": + return "020000000001010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca7150100000000fdffffff0240e20100000000001976a914f1d49f51f9b58c4805431c303d12d3dcf51ae54188ace9000700000000001600145bdb04f2d096ee48b8b350c85481392ab47c01e70247304402200a72a4599cb27f16011cd67e2951733d6775cbd008506eacb2c20d69db3f531702204c944ec09224a347481c9eea78cac79b77b194b19dfef01b1e3b428010a82570012102fc38612ca7cc42d05a7089f1a6ec3900535604bd779f83c7817aae7bfd907dbd2aed2400" + else: + raise Exception("unexpected txid") + + privkeys = ['p2pkh:cN3LiXmurmGRF5xngYd8XS2ZsP2KeXFUh4SH7wpC8uJJzw52JPq1',] + network = NetworkMock() + dest_addr = 'tb1q782f750ekkxysp2rrscr6yknmn634e2pv8lktu' + sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=1000, locktime=2420010, tx_version=2) + loop = util.get_asyncio_loop() + tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + + tx_copy = tx_from_any(tx.serialize()) + self.assertEqual('02000000016717835a2e1e152a69e7528a0f1346c1d37ee6e76c5e23b5d1c5a5b40241768a000000006a473044022038ad38003943bfd3ed39ba4340d545753fcad632a8fe882d01e4f0140ddb3cfb022019498260e29f5fbbcde9176bfb3553b7acec5fe284a9a3a33547a2d082b60355012103b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b29fdffffff0158de010000000000160014f1d49f51f9b58c4805431c303d12d3dcf51ae5412aed2400', + str(tx_copy)) + self.assertEqual('432c108626581fc6a7d3efc9dac5f3dec8286cec47dfaab86b4267d10381586c', tx_copy.txid()) + self.assertEqual('432c108626581fc6a7d3efc9dac5f3dec8286cec47dfaab86b4267d10381586c', tx_copy.wtxid()) + + def test_sweep_p2wpkh_p2sh(self): + class NetworkMock: + relay_fee = 1000 + async def listunspent_for_scripthash(self, scripthash): + if scripthash == '9ee9bddbe9dc47f7f6c5a652a09012f49dfc54d5b997f58d7ccc49040871e61b': + return [{'tx_hash': '9a7bf98ed72b1002559d3d61805838a00e94afec78b8597a68606e2a0725171d', 'tx_pos': 0, 'height': -1, 'value': 150000}] + else: + return [] + async def get_transaction(self, txid): + if txid == "9a7bf98ed72b1002559d3d61805838a00e94afec78b8597a68606e2a0725171d": + return "020000000001038fc862be3bc8022866cc83b4f2feeaa914b015a3c6644251960baaccc4a5740b0000000000fdffffff7bfd61e391034e28848fae269183f1c5929e26befd5b2d798cf12c91d4d00dbf0100000000fdffffff014764d324e70e7e3e4fa27077bda2d880b3d1545588b75f79deb2855d9f31cb0000000000fdffffff01f04902000000000017a9147d0530db22c8124ff1558269f543dfeedd37131b87024730440220568ae75314f6414ccf2b0bbed522e1b4b1086ed6eb185ba4bc044ba2723c1f3402206c82253797d0f180db38986b46d8ad952829cf25bc31e3ca6ee54665f5a44b3c0121038a466bdcb979b96d70fde84b9ded4aba0c3cd9c0d2d59121fc3555428fd1a4890247304402203ba1b482b0b6ce5c3d29ef21ee8afad641af8381d3b131103c384757922f0c04022072320e260b60fc862669b2ea3dfb663f7f3a0b6babe8d265ac9ebf268e7225c2012103ff0877f34157a3444afbfdd7432032a93187bc1932e1c155d56dd66ef527906c02473044022058b1c1a2a8c1a256d4870b550ba93777a2cce36b89abe3515f024fd4eec48ce4022023e0002193a26064275433e8ade98642d74d58ee4f8e9717a8acca737856a6c401210364e8f5d9c30986931bca1197138d7250a17a0711a223f113b3ccc11ef09efccb2aed2400" + else: + raise Exception("unexpected txid") + + privkeys = ['p2wpkh-p2sh:cQMRGsiEsFX5YoxVZaMEzBruAkCWnoFf1SG7SRm2tLHDEN165TrA',] + network = NetworkMock() + dest_addr = 'tb1qu7n2tzm90a3f29kvxlhzsc7t40ddk075ut5w44' + sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2) + loop = util.get_asyncio_loop() + tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + + tx_copy = tx_from_any(tx.serialize()) + self.assertEqual('020000000001011d1725072a6e60687a59b878ecaf940ea0385880613d9d5502102bd78ef97b9a0000000017160014e7a6a58b657f629516cc37ee2863cbabdadb3fd4fdffffff01fc47020000000000160014e7a6a58b657f629516cc37ee2863cbabdadb3fd402473044022048ea4c558fd374f5d5066440a7f4933393cb377802cb949e3039fedf0378a29402204b4a58c591117cc1e37f07b03cc03cc6198dbf547e2bff813e2e2102bd2057e00121029f46ba81b3c6ad84e52841364dc54ca1097d0c30a68fb529766504c4b1c599352aed2400', + str(tx_copy)) + self.assertEqual('0680124954ccc158cbf24d289c93579f68fd75916509214066f69e09adda1861', tx_copy.txid()) + self.assertEqual('da8567d9b28e9e0ed8b3dcef6e619eba330cec6cb0c55d57f658f5ca06e02eb0', tx_copy.wtxid()) + + def test_sweep_p2wpkh(self): + class NetworkMock: + relay_fee = 1000 + async def listunspent_for_scripthash(self, scripthash): + if scripthash == '7630f6b2121336279b55e5b71d4a59be5ffa782e86bae249ba0b5ad6a791933f': + return [{'tx_hash': '01d76acdb8992f4262fb847f5efbd95ea178049be59c70a2851bdcf9b4ae28e3', 'tx_pos': 0, 'height': 2420006, 'value': 98300}] + else: + return [] + async def get_transaction(self, txid): + if txid == "01d76acdb8992f4262fb847f5efbd95ea178049be59c70a2851bdcf9b4ae28e3": + return "02000000000101208840a3310ae4b88181374b5812f56f5dd56f12574f3bcd8041b48bfadc92cf0000000000fdffffff02fc7f010000000000160014d339efed7cd5d28d31995caf10b8973a9a13c656a08601000000000043410403886197eb13c59721b94a29f9a68a841caedb7782b35121cd81d50d0cc70db3f8955c7a07b08dd6470141b66eedd324406e29d6b6799033314512334461e3f9ac0247304402203328153753e934d7a13215bf58f093f84281d57f8c7d42f3b7704cd714c7b32c02205a502f3f3e4302561ccc93df413be3c78a439ff35b60cea03d19f8804a9a1239012103f41052be701441d1bc8f7cc6a6053d7e7f5e63be212fe5e3687344ddd52e3af525ed2400" + else: + raise Exception("unexpected txid") + + privkeys = ['p2wpkh:cV2BvgtpLNX328m4QrhqycBGA6EkZUFfHM9kKjVXjfyD53uNfC4q',] + network = NetworkMock() + dest_addr = 'tb1qhuy2e45lrdcp9s4ezeptx5kwxcnahzgpar9scc' + sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2) + loop = util.get_asyncio_loop() + tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + + tx_copy = tx_from_any(tx.serialize()) + self.assertEqual('02000000000101e328aeb4f9dc1b85a2709ce59b0478a15ed9fb5e7f84fb62422f99b8cd6ad7010000000000fdffffff01087e010000000000160014bf08acd69f1b7012c2b91642b352ce3627db89010247304402204993099c4663d92ef4c9a28b3f45a40a6585754fe22ecfdc0a76c43fda7c9d04022006a75e0fd3ad1862d8e81015a71d2a1489ec7a9264e6e63b8fe6bb90c27e799b0121038ca94e7c715152fd89803c2a40a934c7c4035fb87b3cba981cd1e407369cfe312aed2400', + str(tx_copy)) + self.assertEqual('e02641928e5394332eec0a36c196f1e30e2b8645ebbeef89d6cc27bf237ae548', tx_copy.txid()) + self.assertEqual('b062d2e19880c66b36e80b823c2d00a2769658d1e574ff854dab15efd8fd7da8', tx_copy.wtxid()) + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db): wallet1 = WalletIntegrityHelper.create_standard_wallet( From 421bd93047476d41194d3fa6a401d916be6f5858 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Feb 2023 15:37:17 +0100 Subject: [PATCH 0160/1143] qml: fix a few leftovers --- electrum/gui/qml/components/controls/InfoTextArea.qml | 4 +--- electrum/gui/qml/components/controls/TextHighlightPane.qml | 2 +- electrum/gui/qml/components/main.qml | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index eb64c5aa9..9acbc90c2 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -15,7 +15,6 @@ TextHighlightPane { property int iconStyle: InfoTextArea.IconStyle.Info property alias textFormat: infotext.textFormat - // borderColor: constants.colorWarning borderColor: iconStyle == InfoTextArea.IconStyle.Info ? constants.colorInfo : iconStyle == InfoTextArea.IconStyle.Warn @@ -38,12 +37,11 @@ TextHighlightPane { Layout.preferredWidth: constants.iconSizeMedium Layout.preferredHeight: constants.iconSizeMedium } - Label { + Label { id: infotext Layout.fillWidth: true width: parent.width - text: invoice.userinfo wrapMode: Text.Wrap } } diff --git a/electrum/gui/qml/components/controls/TextHighlightPane.qml b/electrum/gui/qml/components/controls/TextHighlightPane.qml index 9b5349879..a2d92f685 100644 --- a/electrum/gui/qml/components/controls/TextHighlightPane.qml +++ b/electrum/gui/qml/components/controls/TextHighlightPane.qml @@ -7,7 +7,7 @@ Pane { padding: constants.paddingSmall property color backgroundColor: Qt.lighter(Material.background, 1.15) - property color borderColor: null + property color borderColor: 'transparent' background: Rectangle { color: backgroundColor diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 380086160..dd90b0918 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -66,6 +66,7 @@ ApplicationWindow } Image { + visible: Daemon.currentWallet source: '../../icons/wallet.png' Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall From 3aa10b483f0488646571eea90f56cd11884a8304 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Feb 2023 15:51:11 +0100 Subject: [PATCH 0161/1143] qml: add status update timer for invoice listmodel --- electrum/gui/qml/qeinvoicelistmodel.py | 46 ++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 524bea661..16b918f77 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -1,13 +1,13 @@ from abc import abstractmethod -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex from electrum.logging import get_logger from electrum.util import Satoshis, format_time -from electrum.invoices import Invoice +from electrum.invoices import Invoice, PR_EXPIRED, LN_EXPIRY_NEVER -from .util import QtEventListener, qt_event_listener +from .util import QtEventListener, qt_event_listener, status_update_timer_interval from .qetypes import QEAmount class QEAbstractInvoiceListModel(QAbstractListModel): @@ -24,6 +24,11 @@ class QEAbstractInvoiceListModel(QAbstractListModel): def __init__(self, wallet, parent=None): super().__init__(parent) self.wallet = wallet + + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self.updateStatusStrings) + self.init_model() def rowCount(self, index): @@ -60,6 +65,8 @@ def init_model(self): self.invoices = invoices self.endInsertRows() + self.set_status_timer() + def add_invoice(self, invoice: Invoice): item = self.invoice_to_model(invoice) self._logger.debug(str(item)) @@ -68,6 +75,8 @@ def add_invoice(self, invoice: Invoice): self.invoices.insert(0, item) self.endInsertRows() + self.set_status_timer() + @pyqtSlot(str) def addInvoice(self, key): self.add_invoice(self.get_invoice_for_key(key)) @@ -81,6 +90,7 @@ def delete_invoice(self, key: str): self.endRemoveRows() break i = i + 1 + self.set_status_timer() def get_model_invoice(self, key: str): for invoice in self.invoices: @@ -104,7 +114,7 @@ def updateInvoice(self, key, status): def invoice_to_model(self, invoice: Invoice): item = self.get_invoice_as_dict(invoice) - #item['key'] = invoice.get_id() + item['key'] = invoice.get_id() item['is_lightning'] = invoice.is_lightning() if invoice.is_lightning() and 'address' not in item: item['address'] = '' @@ -115,6 +125,32 @@ def invoice_to_model(self, invoice: Invoice): return item + def set_status_timer(self): + nearest_interval = LN_EXPIRY_NEVER + for invoice in self.invoices: + if invoice['status'] != PR_EXPIRED: + if invoice['expiration'] > 0 and invoice['expiration'] != LN_EXPIRY_NEVER: + interval = status_update_timer_interval(invoice['timestamp'] + invoice['expiration']) + if interval > 0: + nearest_interval = nearest_interval if nearest_interval < interval else interval + + if nearest_interval != LN_EXPIRY_NEVER: + self._timer.setInterval(nearest_interval) # msec + self._timer.start() + + @pyqtSlot() + def updateStatusStrings(self): + i = 0 + for item in self.invoices: + invoice = self.get_invoice_for_key(item['key']) + item['status'] = self.wallet.get_invoice_status(invoice) + item['status_str'] = invoice.get_status_str(item['status']) + index = self.index(i,0) + self.dataChanged.emit(index, index, [self._ROLE_RMAP['status'], self._ROLE_RMAP['status_str']]) + i = i + 1 + + self.set_status_timer() + @abstractmethod def get_invoice_for_key(self, key: str): raise Exception('provide impl') @@ -148,7 +184,6 @@ def on_event_invoice_status(self, wallet, key, status): def invoice_to_model(self, invoice: Invoice): item = super().invoice_to_model(invoice) item['type'] = 'invoice' - item['key'] = invoice.get_id() return item @@ -181,7 +216,6 @@ def on_event_request_status(self, wallet, key, status): def invoice_to_model(self, invoice: Invoice): item = super().invoice_to_model(invoice) item['type'] = 'request' - item['key'] = invoice.get_id() return item From 6a6391c6a3df1c833db93667ca29ad4038838a7c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 14 Feb 2023 15:05:34 +0000 Subject: [PATCH 0162/1143] bitcoin.py: (trivial) rm redundant "net=" defaults --- electrum/bitcoin.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index abbf0fea2..ecea83900 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -398,7 +398,6 @@ def hash160_to_p2sh(h160: bytes, *, net=None) -> str: return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH) def public_key_to_p2pkh(public_key: bytes, *, net=None) -> str: - if net is None: net = constants.net return hash160_to_p2pkh(hash_160(public_key), net=net) def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str: @@ -408,11 +407,9 @@ def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str: return addr def public_key_to_p2wpkh(public_key: bytes, *, net=None) -> str: - if net is None: net = constants.net return hash_to_segwit_addr(hash_160(public_key), witver=0, net=net) def script_to_p2wsh(script: str, *, net=None) -> str: - if net is None: net = constants.net return hash_to_segwit_addr(sha256(bfh(script)), witver=0, net=net) def p2wpkh_nested_script(pubkey: str) -> str: @@ -424,7 +421,6 @@ def p2wsh_nested_script(witness_script: str) -> str: return construct_script([0, wsh]) def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: - if net is None: net = constants.net if txin_type == 'p2pkh': return public_key_to_p2pkh(bfh(pubkey), net=net) elif txin_type == 'p2wpkh': @@ -438,7 +434,6 @@ def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: # TODO this method is confusingly named def redeem_script_to_address(txin_type: str, scriptcode: str, *, net=None) -> str: - if net is None: net = constants.net if txin_type == 'p2sh': # given scriptcode is a redeem_script return hash160_to_p2sh(hash_160(bfh(scriptcode)), net=net) @@ -733,7 +728,6 @@ def is_b58_address(addr: str, *, net=None) -> bool: return True def is_address(addr: str, *, net=None) -> bool: - if net is None: net = constants.net return is_segwit_address(addr, net=net) \ or is_b58_address(addr, net=net) From d3d66e7248cf94591fb775643490ccf097198d88 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Feb 2023 16:25:14 +0100 Subject: [PATCH 0163/1143] qml: RbF: do not decrease payment when payment is a swap ref f617887509c92833ffaa6db25d8ed0497acb9ddb --- electrum/gui/qml/components/RbfBumpFeeDialog.qml | 2 ++ electrum/gui/qml/qetxfinalizer.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 099fc5b3f..7f7164cbd 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -56,6 +56,8 @@ ElDialog { Layout.preferredWidth: 1 Layout.fillWidth: true ElComboBox { + enabled: rbffeebumper.canChangeBumpMethod + textRole: 'text' valueRole: 'value' diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 55dc7bc8a..edd733d05 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -449,6 +449,12 @@ def __init__(self, parent=None): self._orig_tx = None self._rbf = True self._bump_method = 'preserve_payment' + self._can_change_bump_method = True + + canChangeBumpMethodChanged = pyqtSignal() + @pyqtProperty(bool, notify=canChangeBumpMethodChanged) + def canChangeBumpMethod(self): + return self._can_change_bump_method oldfeeChanged = pyqtSignal() @pyqtProperty(QEAmount, notify=oldfeeChanged) @@ -479,6 +485,7 @@ def bumpMethod(self): @bumpMethod.setter def bumpMethod(self, bumpmethod): + assert self._can_change_bump_method if self._bump_method != bumpmethod: self._bump_method = bumpmethod self.bumpMethodChanged.emit() @@ -490,6 +497,9 @@ def get_tx(self): self._orig_tx = self._wallet.wallet.get_input_tx(self._txid) assert self._orig_tx + if self._wallet.wallet.get_swap_by_funding_tx(self._orig_tx): + self._can_change_bump_method = False + if not isinstance(self._orig_tx, PartialTransaction): self._orig_tx = PartialTransaction.from_tx(self._orig_tx) From ebb3f90e29196436c17358fdca0e2f740fd0721c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Feb 2023 16:34:01 +0100 Subject: [PATCH 0164/1143] qml: refresh transaction list when wallet_updated+is_up_to_date and history is dirty --- electrum/gui/qml/qewallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index a86b95ff4..31d615ec4 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -200,6 +200,8 @@ def on_event_wallet_updated(self, wallet): self._logger.debug('wallet_updated') self.balanceChanged.emit() self.synchronizing = not wallet.is_up_to_date() + if not self.synchronizing: + self.historyModel.init_model() # refresh if dirty @event_listener def on_event_channel(self, wallet, channel): From 46ed94eb3aa3224682b6d8bb4aa7e6d980544928 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 15 Feb 2023 15:21:25 +0100 Subject: [PATCH 0165/1143] qml: don't present bolt11 invoice when invoice amount > num_sats_can_receive --- electrum/gui/qml/qerequestdetails.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 95eeac6e4..2d2e05fe5 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -116,7 +116,11 @@ def expiration(self): @pyqtProperty(str, notify=detailsChanged) def bolt11(self): - return self._req.lightning_invoice if self._req else '' + can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() + if self._req and can_receive > 0 and self._req.amount_msat/1000 <= can_receive: + return self._req.lightning_invoice + else: + return '' @pyqtProperty(str, notify=detailsChanged) def bip21(self): From fa45e1b7ba1ea0167021f7fe1926d066ea1c5122 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Feb 2023 13:22:24 +0100 Subject: [PATCH 0166/1143] qml: fix name clash QML vs registered QObjects NewWalletWizard and ServerConnectWizard --- electrum/gui/qml/components/controls/InfoTextArea.qml | 2 ++ electrum/gui/qml/qeapp.py | 4 ++-- electrum/gui/qml/qetxfinalizer.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index 9acbc90c2..a660a722d 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -26,6 +26,8 @@ TextHighlightPane { RowLayout { width: parent.width + spacing: constants.paddingLarge + Image { source: iconStyle == InfoTextArea.IconStyle.Info ? "../../../icons/info.png" diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index f3ab6dedc..43ddd78db 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -298,8 +298,8 @@ def __init__(self, args, config, daemon, plugins): qmlRegisterType(QETxCanceller, 'org.electrum', 1, 0, 'TxCanceller') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') - qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'NewWalletWizard', 'NewWalletWizard can only be used as property') - qmlRegisterUncreatableType(QEServerConnectWizard, 'org.electrum', 1, 0, 'ServerConnectWizard', 'ServerConnectWizard can only be used as property') + qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard', 'QNewWalletWizard can only be used as property') + qmlRegisterUncreatableType(QEServerConnectWizard, 'org.electrum', 1, 0, 'QServerConnectWizard', 'QServerConnectWizard can only be used as property') qmlRegisterUncreatableType(QEFilterProxyModel, 'org.electrum', 1, 0, 'FilterProxyModel', 'FilterProxyModel can only be used as property') qmlRegisterUncreatableType(QSortFilterProxyModel, 'org.electrum', 1, 0, 'QSortFilterProxyModel', 'QSortFilterProxyModel can only be used as property') diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index edd733d05..a7ab66fdf 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -499,6 +499,7 @@ def get_tx(self): if self._wallet.wallet.get_swap_by_funding_tx(self._orig_tx): self._can_change_bump_method = False + self.canChangeBumpMethodChanged.emit() if not isinstance(self._orig_tx, PartialTransaction): self._orig_tx = PartialTransaction.from_tx(self._orig_tx) From 15d73daf8d980cc2125c67127a789e279d3e882c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Feb 2023 13:24:23 +0100 Subject: [PATCH 0167/1143] qml: fix lifecycle issues with swap helper. previously tied to Channels view, now dynamically created and parented to app --- electrum/gui/qml/components/Channels.qml | 27 ++++----------- electrum/gui/qml/components/SwapDialog.qml | 33 +++++++++++-------- .../gui/qml/components/SwapProgressDialog.qml | 1 + electrum/gui/qml/components/main.qml | 26 +++++++++++++++ electrum/gui/qml/qeswaphelper.py | 1 + 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 85b43ee7a..dfc2001ed 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -110,8 +110,13 @@ Pane { Layout.preferredWidth: 1 text: qsTr('Swap'); visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 - icon.source: '../../icons/status_waiting.png' + icon.source: Qt.resolvedUrl('../../icons/update.png') onClicked: { + var swaphelper = app.swaphelper.createObject(app) + swaphelper.swapStarted.connect(function() { + var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + dialog.open() + }) var dialog = swapDialog.createObject(root, { swaphelper: swaphelper }) dialog.open() } @@ -141,26 +146,6 @@ Pane { } - SwapHelper { - id: swaphelper - wallet: Daemon.currentWallet - onConfirm: { - var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) - dialog.yesClicked.connect(function() { - dialog.close() - swaphelper.executeSwap(true) - }) - dialog.open() - } - onAuthRequired: { - app.handleAuthRequired(swaphelper, method) - } - onSwapStarted: { - var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) - dialog.open() - } - } - Component { id: swapDialog SwapDialog { diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index abebaa049..6239e6a06 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -50,7 +50,9 @@ ElDialog { Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall - source: swaphelper.isReverse ? '../../icons/lightning.png' : '../../icons/bitcoin.png' + source: swaphelper.isReverse + ? '../../icons/lightning.png' + : '../../icons/bitcoin.png' visible: swaphelper.valid } } @@ -83,7 +85,9 @@ ElDialog { Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall - source: swaphelper.isReverse ? '../../icons/bitcoin.png' : '../../icons/lightning.png' + source: swaphelper.isReverse + ? '../../icons/bitcoin.png' + : '../../icons/lightning.png' visible: swaphelper.valid } } @@ -123,7 +127,9 @@ ElDialog { color: Material.accentColor } Label { - text: '(' + swaphelper.serverfeeperc + ')' + text: swaphelper.serverfeeperc + ? '(' + swaphelper.serverfeeperc + ')' + : '' } } @@ -164,15 +170,6 @@ ElDialog { if (activeFocus) swaphelper.sliderPos = value } - Component.onCompleted: { - value = swaphelper.sliderPos - } - Connections { - target: swaphelper - function onSliderPosChanged() { - swapslider.value = swaphelper.sliderPos - } - } } InfoTextArea { @@ -190,7 +187,7 @@ ElDialog { Layout.columnSpan: 2 Layout.fillWidth: true text: qsTr('Swap') - icon.source: '../../icons/status_waiting.png' + icon.source: Qt.resolvedUrl('../../icons/update.png') enabled: swaphelper.valid onClicked: swaphelper.executeSwap() } @@ -198,8 +195,16 @@ ElDialog { Connections { target: swaphelper - function onSwapStarted() { + function onSliderPosChanged() { + swapslider.value = swaphelper.sliderPos + } + function onSwapSuccess() { root.close() } } + + Component.onCompleted: { + swapslider.value = swaphelper.sliderPos + } + } diff --git a/electrum/gui/qml/components/SwapProgressDialog.qml b/electrum/gui/qml/components/SwapProgressDialog.qml index 5cc9feea3..ec3434808 100644 --- a/electrum/gui/qml/components/SwapProgressDialog.qml +++ b/electrum/gui/qml/components/SwapProgressDialog.qml @@ -15,6 +15,7 @@ ElDialog { width: parent.width height: parent.height + iconSource: Qt.resolvedUrl('../../icons/update.png') title: swaphelper.isReverse ? qsTr('Reverse swap...') : qsTr('Swap...') diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index dd90b0918..47b9464e8 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -7,6 +7,8 @@ import QtQuick.Controls.Material.impl 2.12 import QtQml 2.6 import QtMultimedia 5.6 +import org.electrum 1.0 + import "controls" ApplicationWindow @@ -274,6 +276,30 @@ ApplicationWindow } } + property alias swaphelper: _swaphelper + Component { + id: _swaphelper + SwapHelper { + id: __swaphelper + wallet: Daemon.currentWallet + onConfirm: { + var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) + dialog.yesClicked.connect(function() { + dialog.close() + __swaphelper.executeSwap(true) + }) + dialog.open() + } + onAuthRequired: { + app.handleAuthRequired(__swaphelper, method) + } + onError: { + var dialog = app.messageDialog.createObject(app, { text: message }) + dialog.open() + } + } + } + Component.onCompleted: { coverTimer.start() diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 2924809d6..66e8e3b6e 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -19,6 +19,7 @@ class QESwapHelper(AuthMixin, QObject): _logger = get_logger(__name__) confirm = pyqtSignal([str], arguments=['message']) + error = pyqtSignal([str], arguments=['message']) swapStarted = pyqtSignal() swapSuccess = pyqtSignal() swapFailed = pyqtSignal([str], arguments=['message']) From 845f4aee4d1a290eab81cdb586a90fe0759819f6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Feb 2023 09:44:09 +0000 Subject: [PATCH 0168/1143] transaction.py: move Tx.serialize_input -> TxInput.serialize_to_network --- electrum/transaction.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 425ce9a80..95b6ae7c4 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -283,7 +283,18 @@ def to_json(self): d['witness'] = self.witness.hex() return d - def witness_elements(self)-> Sequence[bytes]: + def serialize_to_network(self, *, script_sig: bytes = None) -> bytes: + if script_sig is None: + script_sig = self.script_sig + # Prev hash and index + s = self.prevout.serialize_to_network() + # Script length, script, sequence + s += bytes.fromhex(var_int(len(script_sig))) + s += script_sig + s += bytes.fromhex(int_to_hex(self.nsequence, 4)) + return s + + def witness_elements(self) -> Sequence[bytes]: if not self.witness: return [] vds = BCDataStream() @@ -854,16 +865,6 @@ def get_preimage_script(cls, txin: 'PartialTxInput') -> str: else: raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}') - @classmethod - def serialize_input(self, txin: TxInput, script: str) -> str: - # Prev hash and index - s = txin.prevout.serialize_to_network().hex() - # Script length, script, sequence - s += var_int(len(script)//2) - s += script - s += int_to_hex(txin.nsequence, 4) - return s - def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: inputs = self.inputs() outputs = self.outputs() @@ -902,11 +903,12 @@ def serialize_to_network(self, *, estimate_size=False, include_sigs=True, force_ inputs = self.inputs() outputs = self.outputs() - def create_script_sig(txin: TxInput) -> str: + def create_script_sig(txin: TxInput) -> bytes: if include_sigs: - return self.input_script(txin, estimate_size=estimate_size) - return '' - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, create_script_sig(txin)) + script_sig = self.input_script(txin, estimate_size=estimate_size) + return bytes.fromhex(script_sig) + return b"" + txins = var_int(len(inputs)) + ''.join(txin.serialize_to_network(script_sig=create_script_sig(txin)).hex() for txin in inputs) txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs) @@ -973,10 +975,10 @@ def estimated_size(self): return self.virtual_size_from_weight(weight) @classmethod - def estimated_input_weight(cls, txin, is_segwit_tx): + def estimated_input_weight(cls, txin: TxInput, is_segwit_tx: bool): '''Return an estimate of serialized input weight in weight units.''' - script = cls.input_script(txin, estimate_size=True) - input_size = len(cls.serialize_input(txin, script)) // 2 + script_sig = cls.input_script(txin, estimate_size=True) + input_size = len(txin.serialize_to_network(script_sig=bytes.fromhex(script_sig))) if txin.is_segwit(guess_for_address=True): witness_size = len(cls.serialize_witness(txin, estimate_size=True)) // 2 @@ -1964,7 +1966,7 @@ def serialize_preimage(self, txin_index: int, *, nSequence = int_to_hex(txin.nsequence, 4) preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType else: - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, preimage_script if txin_index==k else '') + txins = var_int(len(inputs)) + ''.join(txin.serialize_to_network(script_sig=bfh(preimage_script) if txin_index==k else b"").hex() for k, txin in enumerate(inputs)) txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs) preimage = nVersion + txins + txouts + nLocktime + nHashType From 1c53035b9304fd727d0519f413bb9ee3d67f1997 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Feb 2023 10:11:33 +0000 Subject: [PATCH 0169/1143] ecc.py: add/fix some type hints --- electrum/ecc.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/electrum/ecc.py b/electrum/ecc.py index c8503593b..8af09d491 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -209,7 +209,7 @@ def from_x_and_y(cls, x: int, y: int) -> 'ECPubkey': + int.to_bytes(y, length=32, byteorder='big', signed=False)) return ECPubkey(_bytes) - def get_public_key_bytes(self, compressed=True): + def get_public_key_bytes(self, compressed=True) -> bytes: if self.is_at_infinity(): raise Exception('point is at infinity') x = int.to_bytes(self.x(), length=32, byteorder='big', signed=False) y = int.to_bytes(self.y(), length=32, byteorder='big', signed=False) @@ -220,16 +220,19 @@ def get_public_key_bytes(self, compressed=True): header = b'\x04' return header + x + y - def get_public_key_hex(self, compressed=True): + def get_public_key_hex(self, compressed=True) -> str: return bh2u(self.get_public_key_bytes(compressed)) - def point(self) -> Tuple[int, int]: - return self.x(), self.y() + def point(self) -> Tuple[Optional[int], Optional[int]]: + x = self.x() + y = self.y() + assert (x is None) == (y is None), f"either both x and y, or neither should be None. {(x, y)=}" + return x, y - def x(self) -> int: + def x(self) -> Optional[int]: return self._x - def y(self) -> int: + def y(self) -> Optional[int]: return self._y def _to_libsecp256k1_pubkey_ptr(self): @@ -356,14 +359,14 @@ def encrypt_message(self, message: bytes, magic: bytes = b'BIE1') -> bytes: return base64.b64encode(encrypted + mac) @classmethod - def order(cls): + def order(cls) -> int: return CURVE_ORDER - def is_at_infinity(self): + def is_at_infinity(self) -> bool: return self == POINT_AT_INFINITY @classmethod - def is_pubkey_bytes(cls, b: bytes): + def is_pubkey_bytes(cls, b: bytes) -> bool: try: ECPubkey(b) return True @@ -430,12 +433,12 @@ def __init__(self, privkey_bytes: bytes): super().__init__(pubkey.get_public_key_bytes(compressed=False)) @classmethod - def from_secret_scalar(cls, secret_scalar: int): + def from_secret_scalar(cls, secret_scalar: int) -> 'ECPrivkey': secret_bytes = int.to_bytes(secret_scalar, length=32, byteorder='big', signed=False) return ECPrivkey(secret_bytes) @classmethod - def from_arbitrary_size_secret(cls, privkey_bytes: bytes): + def from_arbitrary_size_secret(cls, privkey_bytes: bytes) -> 'ECPrivkey': """This method is only for legacy reasons. Do not introduce new code that uses it. Unlike the default constructor, this method does not require len(privkey_bytes) == 32, and the secret does not need to be within the curve order either. @@ -454,7 +457,7 @@ def __repr__(self): return f"" @classmethod - def generate_random_key(cls): + def generate_random_key(cls) -> 'ECPrivkey': randint = randrange(CURVE_ORDER) ephemeral_exponent = int.to_bytes(randint, length=32, byteorder='big', signed=False) return ECPrivkey(ephemeral_exponent) From 1ce37c8bb1a845e4343b2335c47752e2f6d3544c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Feb 2023 11:07:19 +0000 Subject: [PATCH 0170/1143] transaction: rm hardcoded sighash magic numbers --- electrum/lnchannel.py | 6 +++--- electrum/lnpeer.py | 10 +++++----- electrum/plugins/bitbox02/bitbox02.py | 5 +++-- electrum/plugins/digitalbitbox/digitalbitbox.py | 4 ++-- electrum/plugins/keepkey/keepkey.py | 5 +++-- electrum/plugins/safe_t/safe_t.py | 5 +++-- electrum/plugins/trezor/trezor.py | 5 +++-- electrum/tests/test_lnutil.py | 5 +++-- electrum/transaction.py | 15 +++++++++++---- 9 files changed, 36 insertions(+), 24 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 6bf73ff52..d0670a676 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -39,7 +39,7 @@ from .invoices import PR_PAID from .bitcoin import redeem_script_to_address from .crypto import sha256, sha256d -from .transaction import Transaction, PartialTransaction, TxInput +from .transaction import Transaction, PartialTransaction, TxInput, Sighash from .logging import Logger from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure from . import lnutil @@ -1101,7 +1101,7 @@ def get_remote_htlc_sig_for_htlc(self, *, htlc_relative_idx: int) -> bytes: data = self.config[LOCAL].current_htlc_signatures htlc_sigs = list(chunks(data, 64)) htlc_sig = htlc_sigs[htlc_relative_idx] - remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + b'\x01' + remote_htlc_sig = ecc.der_sig_from_sig_string(htlc_sig) + Sighash.to_sigbytes(Sighash.ALL) return remote_htlc_sig def revoke_current_commitment(self): @@ -1554,7 +1554,7 @@ def force_close_tx(self) -> PartialTransaction: assert self.signature_fits(tx) tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)}) remote_sig = self.config[LOCAL].current_commitment_signature - remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01" + remote_sig = ecc.der_sig_from_sig_string(remote_sig) + Sighash.to_sigbytes(Sighash.ALL) tx.add_signature_to_txin(txin_idx=0, signing_pubkey=self.config[REMOTE].multisig_key.pubkey.hex(), sig=remote_sig.hex()) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index b69f1783c..301269ba2 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -25,7 +25,7 @@ UnrelatedTransactionException) from . import transaction from .bitcoin import make_op_return -from .transaction import PartialTxOutput, match_script_against_template +from .transaction import PartialTxOutput, match_script_against_template, Sighash from .logging import Logger from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment, process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailure, @@ -2039,8 +2039,8 @@ async def _shutdown(self, chan: Channel, payload, *, is_local: bool): assert our_scriptpubkey # estimate fee of closing tx dummy_sig, dummy_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0) - our_sig = None - closing_tx = None + our_sig = None # type: Optional[bytes] + closing_tx = None # type: Optional[PartialTransaction] is_initiator = chan.constraints.is_initiator our_fee, our_fee_range = self.get_shutdown_fee_range(chan, dummy_tx, is_local) @@ -2185,11 +2185,11 @@ def choose_new_fee(our_fee, our_fee_range, their_fee, their_fee_range, their_pre closing_tx.add_signature_to_txin( txin_idx=0, signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(), - sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) + sig=bh2u(der_sig_from_sig_string(our_sig) + Sighash.to_sigbytes(Sighash.ALL))) closing_tx.add_signature_to_txin( txin_idx=0, signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(), - sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) + sig=bh2u(der_sig_from_sig_string(their_sig) + Sighash.to_sigbytes(Sighash.ALL))) # save local transaction and set state try: self.lnworker.wallet.adb.add_transaction(closing_tx) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 50f2e9115..6f8462295 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -8,7 +8,7 @@ from electrum import bip32, constants from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore -from electrum.transaction import PartialTransaction +from electrum.transaction import PartialTransaction, Sighash from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wallet from electrum.util import bh2u, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard @@ -523,7 +523,8 @@ def sign_transaction( # Fill signatures if len(sigs) != len(tx.inputs()): raise Exception("Incorrect number of inputs signed.") # Should never occur - signatures = [bh2u(ecc.der_sig_from_sig_string(x[1])) + "01" for x in sigs] + sighash = Sighash.to_sigbytes(Sighash.ALL).hex() + signatures = [bh2u(ecc.der_sig_from_sig_string(x[1])) + sighash for x in sigs] tx.update_signatures(signatures) def sign_message(self, keypath: str, message: bytes, script_type: str) -> bytes: diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 629d35b8a..2d64b2f66 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -23,7 +23,7 @@ from electrum.ecc import msg_magic from electrum.wallet import Standard_Wallet from electrum import constants -from electrum.transaction import Transaction, PartialTransaction, PartialTxInput +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, Sighash from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from electrum.util import to_string, UserCancelled, UserFacingException, bfh @@ -645,7 +645,7 @@ def input_script(self, txin: PartialTxInput, *, estimate_size=False): sig_r = int(signed['sig'][:64], 16) sig_s = int(signed['sig'][64:], 16) sig = ecc.der_sig_from_r_and_s(sig_r, sig_s) - sig = to_hexstr(sig) + '01' + sig = to_hexstr(sig) + Sighash.to_sigbytes(Sighash.ALL).hex() tx.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_bytes.hex(), sig=sig) except UserCancelled: raise diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 33ede8f6d..a2807fbcf 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -7,7 +7,7 @@ from electrum.bip32 import BIP32Node from electrum import constants from electrum.i18n import _ -from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, Sighash from electrum.keystore import Hardware_KeyStore from electrum.plugin import Device, runs_in_hwd_thread from electrum.base_wizard import ScriptTypeNotSupported @@ -330,7 +330,8 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): outputs = self.tx_outputs(tx, keystore=keystore) signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] - signatures = [(bh2u(x) + '01') for x in signatures] + sighash = Sighash.to_sigbytes(Sighash.ALL).hex() + signatures = [(bh2u(x) + sighash) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 62af36bc0..5b1bae5c1 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -8,7 +8,7 @@ from electrum import constants from electrum.i18n import _ from electrum.plugin import Device, runs_in_hwd_thread -from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, Sighash from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported @@ -300,7 +300,8 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): outputs = self.tx_outputs(tx, keystore=keystore) signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] - signatures = [(bh2u(x) + '01') for x in signatures] + sighash = Sighash.to_sigbytes(Sighash.ALL).hex() + signatures = [(bh2u(x) + sighash) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 03aa61f3d..5fdb3f50a 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -7,7 +7,7 @@ from electrum import constants from electrum.i18n import _ from electrum.plugin import Device, runs_in_hwd_thread -from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, Sighash from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from electrum.logging import get_logger @@ -370,7 +370,8 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): amount_unit=self.get_trezor_amount_unit(), serialize=False, prev_txes=prev_tx) - signatures = [(bh2u(x) + '01') for x in signatures] + sighash = Sighash.to_sigbytes(Sighash.ALL).hex() + signatures = [(bh2u(x) + sighash) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 1fb395ab4..299c8e557 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -11,7 +11,7 @@ ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features, IncompatibleLightningFeatures, ChannelType) from electrum.util import bh2u, bfh, MyEncoder -from electrum.transaction import Transaction, PartialTransaction +from electrum.transaction import Transaction, PartialTransaction, Sighash from electrum.lnworker import LNWallet from . import ElectrumTestCase @@ -725,7 +725,8 @@ def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remo assert len(pubkey) == 33 assert len(privkey) == 33 tx.sign({bh2u(pubkey): (privkey[:-1], True)}) - tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + "01") + sighash = Sighash.to_sigbytes(Sighash.ALL).hex() + tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + sighash) def test_get_compressed_pubkey_from_bech32(self): self.assertEqual(b'\x03\x84\xef\x87\xd9d\xa2\xaaa7=\xff\xb8\xfe=t8[}>;\n\x13\xa8e\x8eo:\xf5Mi\xb5H', diff --git a/electrum/transaction.py b/electrum/transaction.py index 95b6ae7c4..e718841ed 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -90,19 +90,25 @@ class MissingTxInputAmount(Exception): class Sighash(IntEnum): + # note: this is not an IntFlag, as ALL|NONE != SINGLE + ALL = 1 NONE = 2 SINGLE = 3 ANYONECANPAY = 0x80 @classmethod - def is_valid(cls, sighash) -> bool: + def is_valid(cls, sighash: int) -> bool: for flag in Sighash: for base_flag in [Sighash.ALL, Sighash.NONE, Sighash.SINGLE]: if (flag & ~0x1f | base_flag) == sighash: return True return False + @classmethod + def to_sigbytes(cls, sighash: int) -> bytes: + return sighash.to_bytes(length=1, byteorder="big") + class TxOutput: scriptpubkey: bytes @@ -1940,7 +1946,7 @@ def serialize_preimage(self, txin_index: int, *, txin = inputs[txin_index] sighash = txin.sighash if txin.sighash is not None else Sighash.ALL if not Sighash.is_valid(sighash): - raise Exception("SIGHASH_FLAG not supported!") + raise Exception(f"SIGHASH_FLAG ({sighash}) not supported!") nHashType = int_to_hex(sighash, 4) preimage_script = self.get_preimage_script(txin) if txin.is_segwit(): @@ -1966,6 +1972,8 @@ def serialize_preimage(self, txin_index: int, *, nSequence = int_to_hex(txin.nsequence, 4) preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType else: + if sighash != Sighash.ALL: + raise Exception(f"SIGHASH_FLAG ({sighash}) not supported! (for legacy sighash)") txins = var_int(len(inputs)) + ''.join(txin.serialize_to_network(script_sig=bfh(preimage_script) if txin_index==k else b"").hex() for k, txin in enumerate(inputs)) txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs) @@ -1994,12 +2002,11 @@ def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields= txin = self.inputs()[txin_index] txin.validate_data(for_signing=True) sighash = txin.sighash if txin.sighash is not None else Sighash.ALL - sighash_type = sighash.to_bytes(length=1, byteorder="big").hex() pre_hash = sha256d(bfh(self.serialize_preimage(txin_index, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields))) privkey = ecc.ECPrivkey(privkey_bytes) sig = privkey.sign_transaction(pre_hash) - sig = bh2u(sig) + sighash_type + sig = bh2u(sig) + Sighash.to_sigbytes(sighash).hex() return sig def is_complete(self) -> bool: From 373db76ac9328d6d1f98246d72f64c98c006f95e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Feb 2023 11:35:03 +0000 Subject: [PATCH 0171/1143] util: kill bh2u no longer useful, and the name is so confusing... --- electrum/bip32.py | 2 +- electrum/bitcoin.py | 20 +++++++------- electrum/blockchain.py | 4 +-- electrum/channel_db.py | 4 +-- electrum/commands.py | 6 ++--- electrum/ecc.py | 4 +-- .../kivy/uix/dialogs/lightning_channels.py | 5 ++-- .../uix/dialogs/lightning_open_channel.py | 3 +-- electrum/gui/qt/channel_details.py | 4 +-- electrum/gui/qt/channels_list.py | 2 +- electrum/gui/qt/main_window.py | 2 +- electrum/gui/qt/send_tab.py | 4 +-- electrum/keystore.py | 2 +- electrum/lnchannel.py | 8 +++--- electrum/lnhtlc.py | 2 +- electrum/lnonion.py | 2 +- electrum/lnpeer.py | 14 +++++----- electrum/lnrouter.py | 2 +- electrum/lnsweep.py | 24 ++++++++--------- electrum/lntransport.py | 2 +- electrum/lnutil.py | 26 +++++++++---------- electrum/lnverifier.py | 4 +-- electrum/lnwatcher.py | 4 +-- electrum/lnworker.py | 16 ++++++------ electrum/mnemonic.py | 4 +-- electrum/paymentrequest.py | 2 +- electrum/plugins/bitbox02/bitbox02.py | 4 +-- electrum/plugins/coldcard/coldcard.py | 2 +- electrum/plugins/cosigner_pool/qt.py | 4 +-- electrum/plugins/keepkey/keepkey.py | 4 +-- electrum/plugins/keepkey/qt.py | 3 +-- electrum/plugins/revealer/revealer.py | 4 +-- electrum/plugins/safe_t/qt.py | 3 +-- electrum/plugins/safe_t/safe_t.py | 4 +-- electrum/plugins/trezor/qt.py | 3 +-- electrum/plugins/trezor/trezor.py | 4 +-- electrum/scripts/ln_features.py | 6 ++--- electrum/synchronizer.py | 4 +-- electrum/tests/test_bitcoin.py | 20 +++++++------- electrum/tests/test_blockchain.py | 2 +- electrum/tests/test_lnpeer.py | 4 +-- electrum/tests/test_lnrouter.py | 2 +- electrum/tests/test_lnutil.py | 6 ++--- electrum/tests/test_mnemonic.py | 10 +++---- electrum/tests/test_network.py | 3 +-- electrum/tests/test_transaction.py | 4 +-- electrum/tests/test_wallet_vertical.py | 6 ++--- electrum/transaction.py | 20 +++++++------- electrum/util.py | 13 +--------- electrum/verifier.py | 4 +-- electrum/wallet.py | 2 +- electrum/x509.py | 6 ++--- 52 files changed, 151 insertions(+), 168 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 78938d47e..796777081 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -5,7 +5,7 @@ import hashlib from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional -from .util import bfh, bh2u, BitcoinException +from .util import bfh, BitcoinException from . import constants from . import ecc from .crypto import hash_160, hmac_oneshot diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index ecea83900..525fb5a5a 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -28,7 +28,7 @@ import enum from enum import IntEnum, Enum -from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict, is_hex_str +from .util import bfh, BitcoinException, assert_bytes, to_bytes, inv_dict, is_hex_str from . import version from . import segwit_addr from . import constants @@ -198,7 +198,7 @@ def hex(self) -> str: def rev_hex(s: str) -> str: - return bh2u(bfh(s)[::-1]) + return bfh(s)[::-1].hex() def int_to_hex(i: int, length: int=1) -> str: @@ -238,7 +238,7 @@ def script_num_to_hex(i: int) -> str: elif neg: result[-1] |= 0x80 - return bh2u(result) + return result.hex() def var_int(i: int) -> str: @@ -288,11 +288,11 @@ def push_script(data: str) -> str: if data_len == 0 or data_len == 1 and data[0] == 0: return opcodes.OP_0.hex() elif data_len == 1 and data[0] <= 16: - return bh2u(bytes([opcodes.OP_1 - 1 + data[0]])) + return bytes([opcodes.OP_1 - 1 + data[0]]).hex() elif data_len == 1 and data[0] == 0x81: return opcodes.OP_1NEGATE.hex() - return _op_push(data_len) + bh2u(data) + return _op_push(data_len) + data.hex() def make_op_return(x:bytes) -> bytes: @@ -310,7 +310,7 @@ def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: if type(item) is int: item = script_num_to_hex(item) elif isinstance(item, (bytes, bytearray)): - item = bh2u(item) + item = item.hex() else: assert is_hex_str(item) witness += witness_push(item) @@ -366,7 +366,7 @@ def dust_threshold(network: 'Network' = None) -> int: def hash_encode(x: bytes) -> str: - return bh2u(x[::-1]) + return x[::-1].hex() def hash_decode(x: str) -> bytes: @@ -464,7 +464,7 @@ def address_to_script(addr: str, *, net=None) -> str: return construct_script([witver, bytes(witprog)]) addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == net.ADDRTYPE_P2PKH: - script = pubkeyhash_to_p2pkh_script(bh2u(hash_160_)) + script = pubkeyhash_to_p2pkh_script(hash_160_.hex()) elif addrtype == net.ADDRTYPE_P2SH: script = construct_script([opcodes.OP_HASH160, hash_160_, opcodes.OP_EQUAL]) else: @@ -519,7 +519,7 @@ def address_to_scripthash(addr: str, *, net=None) -> str: def script_to_scripthash(script: str) -> str: h = sha256(bfh(script))[0:32] - return bh2u(bytes(reversed(h))) + return h[::-1].hex() def public_key_to_p2pk_script(pubkey: str) -> str: return construct_script([pubkey, opcodes.OP_CHECKSIG]) @@ -613,7 +613,7 @@ def DecodeBase58Check(psz: Union[bytes, str]) -> bytes: csum_found = vchRet[-4:] csum_calculated = sha256d(payload)[0:4] if csum_calculated != csum_found: - raise InvalidChecksum(f'calculated {bh2u(csum_calculated)}, found {bh2u(csum_found)}') + raise InvalidChecksum(f'calculated {csum_calculated.hex()}, found {csum_found.hex()}') else: return payload diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 43564e353..569f0338e 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -29,7 +29,7 @@ from .bitcoin import hash_encode, int_to_hex, rev_hex from .crypto import sha256d from . import constants -from .util import bfh, bh2u, with_lock +from .util import bfh, with_lock from .simple_config import SimpleConfig from .logging import get_logger, Logger @@ -408,7 +408,7 @@ def _swap_with_parent(self) -> bool: # swap parameters self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain] self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint - self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE])) + self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(parent_data[:HEADER_SIZE].hex()) self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash # parent's new name os.replace(child_old_name, parent.path()) diff --git a/electrum/channel_db.py b/electrum/channel_db.py index bb2213570..b14bef7de 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -38,7 +38,7 @@ from .sql_db import SqlDB, sql from . import constants, util -from .util import bh2u, profiler, get_headers_dir, is_ip_address, json_normalize +from .util import profiler, get_headers_dir, is_ip_address, json_normalize from .logging import Logger from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID, validate_features, IncompatibleOrInsaneFeatures, InvalidGossipMsg) @@ -396,7 +396,7 @@ def add_channel_announcements(self, msg_payloads, *, trusted=True): if short_channel_id in self._channels: continue if constants.net.rev_genesis_bytes() != msg['chain_hash']: - self.logger.info("ChanAnn has unexpected chain_hash {}".format(bh2u(msg['chain_hash']))) + self.logger.info("ChanAnn has unexpected chain_hash {}".format(msg['chain_hash'].hex())) continue try: channel_info = ChannelInfo.from_msg(msg) diff --git a/electrum/commands.py b/electrum/commands.py index 0ca35feff..960bae252 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -41,7 +41,7 @@ import os from .import util, ecc -from .util import (bfh, bh2u, format_satoshis, json_decode, json_normalize, +from .util import (bfh, format_satoshis, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes, parse_max_spend) from . import bitcoin from .bitcoin import is_address, hash_160, COIN @@ -1126,7 +1126,7 @@ async def lnpay(self, invoice, timeout=120, wallet: Abstract_Wallet = None): @command('wl') async def nodeid(self, wallet: Abstract_Wallet = None): listen_addr = self.config.get('lightning_listen') - return bh2u(wallet.lnworker.node_keypair.pubkey) + (('@' + listen_addr) if listen_addr else '') + return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '') @command('wl') async def list_channels(self, wallet: Abstract_Wallet = None): @@ -1142,7 +1142,7 @@ async def list_channels(self, wallet: Abstract_Wallet = None): 'channel_point': chan.funding_outpoint.to_str(), 'state': chan.get_state().name, 'peer_state': chan.peer_state.name, - 'remote_pubkey': bh2u(chan.node_id), + 'remote_pubkey': chan.node_id.hex(), 'local_balance': chan.balance(LOCAL)//1000, 'remote_balance': chan.balance(REMOTE)//1000, 'local_ctn': chan.get_latest_ctn(LOCAL), diff --git a/electrum/ecc.py b/electrum/ecc.py index 8af09d491..687711d10 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -32,7 +32,7 @@ CFUNCTYPE, POINTER, cast ) -from .util import bfh, bh2u, assert_bytes, to_bytes, InvalidPassword, profiler, randrange +from .util import bfh, assert_bytes, to_bytes, InvalidPassword, profiler, randrange from .crypto import (sha256d, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot) from . import constants from .logging import get_logger @@ -221,7 +221,7 @@ def get_public_key_bytes(self, compressed=True) -> bytes: return header + x + y def get_public_key_hex(self, compressed=True) -> str: - return bh2u(self.get_public_key_bytes(compressed)) + return self.get_public_key_bytes(compressed).hex() def point(self) -> Tuple[Optional[int], Optional[int]]: x = self.x() diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py index 6b812070e..66dfa21cd 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_channels.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_channels.py @@ -5,7 +5,6 @@ from kivy.factory import Factory from kivy.uix.popup import Popup -from electrum.util import bh2u from electrum.logging import Logger from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id from electrum.lnchannel import AbstractChannel, Channel, ChannelState, ChanCloseOption @@ -465,8 +464,8 @@ def __init__(self, chan: Channel, app: 'ElectrumWindow', **kwargs): self.app = app self.chan = chan self.title = _('Channel details') - self.node_id = bh2u(chan.node_id) - self.channel_id = bh2u(chan.channel_id) + self.node_id = chan.node_id.hex() + self.channel_id = chan.channel_id.hex() self.funding_txid = chan.funding_outpoint.txid self.short_id = format_short_channel_id(chan.short_channel_id) self.capacity = self.app.format_amount_and_units(chan.get_capacity()) diff --git a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py index 3383558f7..76a956fbd 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py @@ -6,7 +6,6 @@ from electrum.gui import messages from electrum.gui.kivy.i18n import _ from electrum.lnaddr import lndecode -from electrum.util import bh2u from electrum.bitcoin import COIN import electrum.simple_config as config from electrum.logging import Logger @@ -150,7 +149,7 @@ def open(self, *args, **kwargs): if not fee: fee = config.FEERATE_FALLBACK_STATIC_FEE self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2) # FIXME magic number?! - self.pubkey = bh2u(self.lnaddr.pubkey.serialize()) + self.pubkey = self.lnaddr.pubkey.serialize().hex() if self.msg: self.app.show_info(self.msg) diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py index f49a851cc..65d8a3f0f 100644 --- a/electrum/gui/qt/channel_details.py +++ b/electrum/gui/qt/channel_details.py @@ -7,7 +7,7 @@ from electrum.util import EventListener from electrum.i18n import _ -from electrum.util import bh2u, format_time +from electrum.util import format_time from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction from electrum.lnchannel import htlcsum, Channel, AbstractChannel, HTLCWithStatus from electrum.lnaddr import LnAddr, lndecode @@ -86,7 +86,7 @@ def make_htlc_item(self, i: UpdateAddHtlc, direction: Direction) -> HTLCItem: it = HTLCItem(_('Sent HTLC with ID {}' if Direction.SENT == direction else 'Received HTLC with ID {}').format(i.htlc_id)) it.appendRow([HTLCItem(_('Amount')),HTLCItem(self.format_msat(i.amount_msat))]) it.appendRow([HTLCItem(_('CLTV expiry')),HTLCItem(str(i.cltv_expiry))]) - it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(bh2u(i.payment_hash))]) + it.appendRow([HTLCItem(_('Payment hash')),HTLCItem(i.payment_hash.hex())]) return it def make_model(self, htlcs: Sequence[HTLCWithStatus]) -> QtGui.QStandardItemModel: diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 530c54d6b..de3a5ba66 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -11,7 +11,7 @@ QToolTip) from PyQt5.QtGui import QFont, QStandardItem, QBrush, QPainter, QIcon, QHelpEvent -from electrum.util import bh2u, NotEnoughFunds, NoDynamicFeeEstimates +from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates from electrum.i18n import _ from electrum.lnchannel import AbstractChannel, PeerState, ChannelBackup, Channel, ChannelState, ChanCloseOption from electrum.wallet import Abstract_Wallet diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 18ee80232..79fdec3ea 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -57,7 +57,7 @@ from electrum.i18n import _ from electrum.util import (format_time, get_asyncio_loop, UserCancelled, profiler, - bh2u, bfh, InvalidPassword, + bfh, InvalidPassword, UserFacingException, FailedToParsePaymentIdentifier, get_new_wallet_name, send_exception_to_crash_reporter, AddTransactionException, BITCOIN_BIP21_URI_SCHEME, os_chmod) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 2bb64ef51..4145295b1 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -15,7 +15,7 @@ from electrum import lnutil from electrum.plugin import run_hook from electrum.i18n import _ -from electrum.util import (get_asyncio_loop, bh2u, FailedToParsePaymentIdentifier, +from electrum.util import (get_asyncio_loop, FailedToParsePaymentIdentifier, InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, NotEnoughFunds, NoDynamicFeeEstimates, InvoiceError, parse_max_spend) from electrum.invoices import PR_PAID, Invoice @@ -396,7 +396,7 @@ def set_bolt11(self, invoice: str): self.show_error(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") return - pubkey = bh2u(lnaddr.pubkey.serialize()) + pubkey = lnaddr.pubkey.serialize().hex() for k,v in lnaddr.tags: if k == 'd': description = v diff --git a/electrum/keystore.py b/electrum/keystore.py index 2b7a98607..e0419fb4f 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -42,7 +42,7 @@ SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160, CiphertextFormatError) from .util import (InvalidPassword, WalletFileException, - BitcoinException, bh2u, bfh, inv_dict, is_hex_str) + BitcoinException, bfh, inv_dict, is_hex_str) from .mnemonic import Mnemonic, Wordlist, seed_type, is_seed from .plugin import run_hook from .logging import Logger diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index d0670a676..a7db8d5fa 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -35,7 +35,7 @@ from . import ecc from . import constants, util -from .util import bfh, bh2u, chunks, TxMinedInfo +from .util import bfh, chunks, TxMinedInfo from .invoices import PR_PAID from .bitcoin import redeem_script_to_address from .crypto import sha256, sha256d @@ -1525,8 +1525,8 @@ def make_closing_tx(self, local_script: bytes, remote_script: bytes, }, local_amount_msat=self.balance(LOCAL), remote_amount_msat=self.balance(REMOTE) if not drop_remote else 0, - local_script=bh2u(local_script), - remote_script=bh2u(remote_script), + local_script=local_script.hex(), + remote_script=remote_script.hex(), htlcs=[], dust_limit_sat=self.config[LOCAL].dust_limit_sat) @@ -1552,7 +1552,7 @@ def signature_fits(self, tx: PartialTransaction) -> bool: def force_close_tx(self) -> PartialTransaction: tx = self.get_latest_commitment(LOCAL) assert self.signature_fits(tx) - tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)}) + tx.sign({self.config[LOCAL].multisig_key.pubkey.hex(): (self.config[LOCAL].multisig_key.privkey, True)}) remote_sig = self.config[LOCAL].current_commitment_signature remote_sig = ecc.der_sig_from_sig_string(remote_sig) + Sighash.to_sigbytes(Sighash.ALL) tx.add_signature_to_txin(txin_idx=0, diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py index a7f2916c1..e71457585 100644 --- a/electrum/lnhtlc.py +++ b/electrum/lnhtlc.py @@ -3,7 +3,7 @@ import threading from .lnutil import SENT, RECEIVED, LOCAL, REMOTE, HTLCOwner, UpdateAddHtlc, Direction, FeeUpdate -from .util import bh2u, bfh, with_lock +from .util import bfh, with_lock if TYPE_CHECKING: from .json_db import StoredDict diff --git a/electrum/lnonion.py b/electrum/lnonion.py index 8b353aeb8..95c81ba0e 100644 --- a/electrum/lnonion.py +++ b/electrum/lnonion.py @@ -30,7 +30,7 @@ from . import ecc from .crypto import sha256, hmac_oneshot, chacha20_encrypt -from .util import bh2u, profiler, xor_bytes, bfh +from .util import profiler, xor_bytes, bfh from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH, NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag) from .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 301269ba2..7cd1a01d5 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -21,7 +21,7 @@ from . import ecc from .ecc import sig_string_from_r_and_s, der_sig_from_sig_string from . import constants -from .util import (bh2u, bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup, +from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup, UnrelatedTransactionException) from . import transaction from .bitcoin import make_op_return @@ -1020,7 +1020,7 @@ async def on_open_channel(self, payload): # -> funding signed funding_idx = funding_created['funding_output_index'] - funding_txid = bh2u(funding_created['funding_txid'][::-1]) + funding_txid = funding_created['funding_txid'][::-1].hex() channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx) constraints = ChannelConstraints( capacity=funding_sat, @@ -1157,7 +1157,7 @@ def are_datalossprotect_fields_valid() -> bool: if our_pcs != their_claim_of_our_last_per_commitment_secret: self.logger.error( f"channel_reestablish ({chan.get_id_for_log()}): " - f"(DLP) local PCS mismatch: {bh2u(our_pcs)} != {bh2u(their_claim_of_our_last_per_commitment_secret)}") + f"(DLP) local PCS mismatch: {our_pcs.hex()} != {their_claim_of_our_last_per_commitment_secret.hex()}") return False assert chan.is_static_remotekey_enabled() return True @@ -1167,7 +1167,7 @@ def are_datalossprotect_fields_valid() -> bool: if they_are_ahead: self.logger.warning( f"channel_reestablish ({chan.get_id_for_log()}): " - f"remote is ahead of us! They should force-close. Remote PCP: {bh2u(their_local_pcp)}") + f"remote is ahead of us! They should force-close. Remote PCP: {their_local_pcp.hex()}") # data_loss_protect_remote_pcp is used in lnsweep chan.set_data_loss_protect_remote_pcp(their_next_local_ctn - 1, their_local_pcp) chan.set_state(ChannelState.WE_ARE_TOXIC) @@ -1311,7 +1311,7 @@ def send_channel_ready(self, chan: Channel): self.mark_open(chan) def on_channel_ready(self, chan: Channel, payload): - self.logger.info(f"on_channel_ready. channel: {bh2u(chan.channel_id)}") + self.logger.info(f"on_channel_ready. channel: {chan.channel_id.hex()}") # save remote alias for use in invoices scid_alias = payload.get('channel_ready_tlvs', {}).get('short_channel_id', {}).get('alias') if scid_alias: @@ -2185,11 +2185,11 @@ def choose_new_fee(our_fee, our_fee_range, their_fee, their_fee_range, their_pre closing_tx.add_signature_to_txin( txin_idx=0, signing_pubkey=chan.config[LOCAL].multisig_key.pubkey.hex(), - sig=bh2u(der_sig_from_sig_string(our_sig) + Sighash.to_sigbytes(Sighash.ALL))) + sig=(der_sig_from_sig_string(our_sig) + Sighash.to_sigbytes(Sighash.ALL)).hex()) closing_tx.add_signature_to_txin( txin_idx=0, signing_pubkey=chan.config[REMOTE].multisig_key.pubkey.hex(), - sig=bh2u(der_sig_from_sig_string(their_sig) + Sighash.to_sigbytes(Sighash.ALL))) + sig=(der_sig_from_sig_string(their_sig) + Sighash.to_sigbytes(Sighash.ALL)).hex()) # save local transaction and set state try: self.lnworker.wallet.adb.add_transaction(closing_tx) diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index 9793ca3aa..d6b8227f0 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -31,7 +31,7 @@ import attr from math import inf -from .util import profiler, with_lock, bh2u +from .util import profiler, with_lock from .logging import Logger from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures, NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE) diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index ad2b3bc21..b896863b0 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -5,7 +5,7 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable from enum import Enum, auto -from .util import bfh, bh2u +from .util import bfh from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness from .invoices import PR_PAID from . import ecc @@ -52,8 +52,8 @@ def create_sweeptxs_for_watchtower(chan: 'Channel', ctx: Transaction, per_commit txs = [] # to_local revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) - witness_script = bh2u(make_commitment_output_to_local_witness_script( - revocation_pubkey, to_self_delay, this_delayed_pubkey)) + witness_script = make_commitment_output_to_local_witness_script( + revocation_pubkey, to_self_delay, this_delayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', witness_script) output_idxs = ctx.get_output_idxs_from_address(to_local_address) if output_idxs: @@ -119,8 +119,8 @@ def create_sweeptx_for_their_revoked_ctx( txs = [] # to_local revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) - witness_script = bh2u(make_commitment_output_to_local_witness_script( - revocation_pubkey, to_self_delay, this_delayed_pubkey)) + witness_script = make_commitment_output_to_local_witness_script( + revocation_pubkey, to_self_delay, this_delayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', witness_script) output_idxs = ctx.get_output_idxs_from_address(to_local_address) if output_idxs: @@ -159,8 +159,8 @@ def create_sweeptx_for_their_revoked_htlc( this_delayed_pubkey = derive_pubkey(this_conf.delayed_basepoint.pubkey, pcp) # same witness script as to_local revocation_pubkey = ecc.ECPrivkey(other_revocation_privkey).get_public_key_bytes(compressed=True) - witness_script = bh2u(make_commitment_output_to_local_witness_script( - revocation_pubkey, to_self_delay, this_delayed_pubkey)) + witness_script = make_commitment_output_to_local_witness_script( + revocation_pubkey, to_self_delay, this_delayed_pubkey).hex() htlc_address = redeem_script_to_address('p2wsh', witness_script) # check that htlc_tx is a htlc if htlc_tx.outputs()[0].address != htlc_address: @@ -201,8 +201,8 @@ def create_sweeptxs_for_our_ctx( our_htlc_privkey = derive_privkey(secret=int.from_bytes(our_conf.htlc_basepoint.privkey, 'big'), per_commitment_point=our_pcp).to_bytes(32, 'big') our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True) - to_local_witness_script = bh2u(make_commitment_output_to_local_witness_script( - their_revocation_pubkey, to_self_delay, our_localdelayed_pubkey)) + to_local_witness_script = make_commitment_output_to_local_witness_script( + their_revocation_pubkey, to_self_delay, our_localdelayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script) # test if this is our_ctx found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address)) @@ -354,8 +354,8 @@ def create_sweeptxs_for_their_ctx( # to_local and to_remote addresses our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp) their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp) - witness_script = bh2u(make_commitment_output_to_local_witness_script( - our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey)) + witness_script = make_commitment_output_to_local_witness_script( + our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', witness_script) # test if this is their ctx found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address)) @@ -463,7 +463,7 @@ def create_htlctx_that_spends_from_our_ctx( commit=ctx, htlc=htlc, ctx_output_idx=ctx_output_idx, - name=f'our_ctx_{ctx_output_idx}_htlc_tx_{bh2u(htlc.payment_hash)}') + name=f'our_ctx_{ctx_output_idx}_htlc_tx_{htlc.payment_hash.hex()}') remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx) local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey)) txin = htlc_tx.inputs()[0] diff --git a/electrum/lntransport.py b/electrum/lntransport.py index a919688c5..494554406 100644 --- a/electrum/lntransport.py +++ b/electrum/lntransport.py @@ -15,7 +15,7 @@ from .lnutil import (get_ecdh, privkey_to_pubkey, LightningPeerConnectionClosed, HandshakeFailed, LNPeerAddr) from . import ecc -from .util import bh2u, MySocksProxy +from .util import MySocksProxy class HandshakeState(object): diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 90c079aa9..025f772c7 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -11,7 +11,7 @@ import attr from aiorpcx import NetAddress -from .util import bfh, bh2u, inv_dict, UserFacingException +from .util import bfh, inv_dict, UserFacingException from .util import list_enabled_bits from .util import ShortID as ShortChannelID from .util import format_short_id as format_short_channel_id @@ -430,7 +430,7 @@ def add_next_entry(self, hsh): this_bucket = self.buckets[i] e = shachain_derive(new_element, this_bucket.index) if e != this_bucket: - raise Exception("hash is not derivable: {} {} {}".format(bh2u(e.secret), bh2u(this_bucket.secret), this_bucket.index)) + raise Exception("hash is not derivable: {} {} {}".format(e.secret.hex(), this_bucket.secret.hex(), this_bucket.index)) self.buckets[bucket] = new_element self.storage['index'] = index - 1 @@ -474,7 +474,7 @@ def get_prefix(index, pos): to_index) ShachainElement = namedtuple("ShachainElement", ["secret", "index"]) -ShachainElement.__str__ = lambda self: "ShachainElement(" + bh2u(self.secret) + "," + str(self.index) + ")" +ShachainElement.__str__ = lambda self: f"ShachainElement({self.secret.hex()},{self.index})" def get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) -> bytes: """Generate per commitment secret.""" @@ -528,7 +528,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela delayed_pubkey=local_delayedpubkey, ) - p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) + p2wsh = bitcoin.redeem_script_to_address('p2wsh', script.hex()) weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT fee = local_feerate * weight fee = fee // 1000 * 1000 @@ -740,7 +740,7 @@ def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject: local_htlc_pubkey=htlc_pubkey, payment_hash=payment_hash, cltv_expiry=cltv_expiry) - htlc_address = redeem_script_to_address('p2wsh', bh2u(preimage_script)) + htlc_address = redeem_script_to_address('p2wsh', preimage_script.hex()) candidates = ctx.get_output_idxs_from_address(htlc_address) return {output_idx for output_idx in candidates if ctx.outputs()[output_idx].value == htlc.amount_msat // 1000} @@ -806,7 +806,7 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL htlc_tx_inputs = make_htlc_tx_inputs( commit.txid(), ctx_output_idx, amount_msat=amount_msat, - witness_script=bh2u(preimage_script)) + witness_script=preimage_script.hex()) if is_htlc_success: cltv_expiry = 0 htlc_tx = make_htlc_tx(cltv_expiry=cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output) @@ -814,7 +814,7 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, funding_pos: int, funding_txid: str, funding_sat: int) -> PartialTxInput: - pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)]) + pubkeys = sorted([local_funding_pubkey.hex(), remote_funding_pubkey.hex()]) # commitment tx input prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos) c_input = PartialTxInput(prevout=prevout) @@ -859,7 +859,7 @@ def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], lo non_htlc_outputs = [to_local, to_remote] htlc_outputs = [] for script, htlc in htlcs: - addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) + addr = bitcoin.redeem_script_to_address('p2wsh', script.hex()) htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)), value=htlc.amount_msat // 1000)) @@ -985,13 +985,13 @@ def make_commitment_output_to_local_witness_script( def make_commitment_output_to_local_address( revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str: local_script = make_commitment_output_to_local_witness_script(revocation_pubkey, to_self_delay, delayed_pubkey) - return bitcoin.redeem_script_to_address('p2wsh', bh2u(local_script)) + return bitcoin.redeem_script_to_address('p2wsh', local_script.hex()) def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str: - return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey)) + return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex()) def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): - tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)}) + tx.sign({local_config.multisig_key.pubkey.hex(): (local_config.multisig_key.privkey, True)}) sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey] sig_64 = sig_string_from_der_sig(sig[:-1]) return sig_64 @@ -1000,7 +1000,7 @@ def funding_output_script(local_config, remote_config) -> str: return funding_output_script_from_keys(local_config.multisig_key.pubkey, remote_config.multisig_key.pubkey) def funding_output_script_from_keys(pubkey1: bytes, pubkey2: bytes) -> str: - pubkeys = sorted([bh2u(pubkey1), bh2u(pubkey2)]) + pubkeys = sorted([pubkey1.hex(), pubkey2.hex()]) return transaction.multisig_script(pubkeys, 2) @@ -1450,7 +1450,7 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]: # invoice? invoice = lndecode(connect_contents) nodeid_bytes = invoice.pubkey.serialize() - nodeid_hex = bh2u(nodeid_bytes) + nodeid_hex = nodeid_bytes.hex() except: # node id as hex? nodeid_hex = connect_contents diff --git a/electrum/lnverifier.py b/electrum/lnverifier.py index 9505f1390..63c3f6984 100644 --- a/electrum/lnverifier.py +++ b/electrum/lnverifier.py @@ -32,7 +32,7 @@ from . import bitcoin from . import ecc from . import constants -from .util import bh2u, bfh, NetworkJobOnDefaultServer +from .util import bfh, NetworkJobOnDefaultServer from .lnutil import funding_output_script_from_keys, ShortChannelID from .verifier import verify_tx_is_in_block, MerkleVerificationFailure from .transaction import Transaction @@ -105,7 +105,7 @@ async def _verify_some_channels(self): continue self.started_verifying_channel.add(short_channel_id) await self.taskgroup.spawn(self.verify_channel(block_height, short_channel_id)) - #self.logger.info(f'requested short_channel_id {bh2u(short_channel_id)}') + #self.logger.info(f'requested short_channel_id {short_channel_id.hex()}') async def verify_channel(self, block_height: int, short_channel_id: ShortChannelID): # we are verifying channel announcements as they are from untrusted ln peers. diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index b22acddb2..e2d95a580 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -11,7 +11,7 @@ from . import util from .sql_db import SqlDB, sql from .wallet_db import WalletDB -from .util import bh2u, bfh, log_exceptions, ignore_exceptions, TxMinedInfo, random_shuffled_copy +from .util import bfh, log_exceptions, ignore_exceptions, TxMinedInfo, random_shuffled_copy from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE from .transaction import Transaction, TxOutpoint from .transaction import match_script_against_template @@ -69,7 +69,7 @@ def create_database(self): def get_sweep_tx(self, funding_outpoint, prevout): c = self.conn.cursor() c.execute("SELECT tx FROM sweep_txs WHERE funding_outpoint=? AND prevout=?", (funding_outpoint, prevout)) - return [Transaction(bh2u(r[0])) for r in c.fetchall()] + return [Transaction(r[0].hex()) for r in c.fetchall()] @sql def list_sweep_tx(self): diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 78df09282..f83166df3 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -40,7 +40,7 @@ from .transaction import get_script_type_from_output_script from .crypto import sha256 from .bip32 import BIP32Node -from .util import bh2u, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions +from .util import bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions from .crypto import chacha20_encrypt, chacha20_decrypt from .util import ignore_exceptions, make_aiohttp_session from .util import timestamp_to_datetime, random_shuffled_copy @@ -499,12 +499,12 @@ async def add_peer(self, connect_str: str) -> Peer: if self.uses_trampoline(): addr = trampolines_by_id().get(node_id) if not addr: - raise ConnStringFormatError(_('Address unknown for node:') + ' ' + bh2u(node_id)) + raise ConnStringFormatError(_('Address unknown for node:') + ' ' + node_id.hex()) host, port = addr.host, addr.port else: addrs = self.channel_db.get_node_addresses(node_id) if not addrs: - raise ConnStringFormatError(_('Don\'t know any addresses for node:') + ' ' + bh2u(node_id)) + raise ConnStringFormatError(_('Don\'t know any addresses for node:') + ' ' + node_id.hex()) host, port, timestamp = self.choose_preferred_address(list(addrs)) port = int(port) # Try DNS-resolving the host (if needed). This is simply so that @@ -891,7 +891,7 @@ def get_onchain_history(self): tx_height = self.wallet.adb.get_tx_height(funding_txid) self._labels_cache[funding_txid] = _('Open channel') + ' ' + chan.get_id_for_log() item = { - 'channel_id': bh2u(chan.channel_id), + 'channel_id': chan.channel_id.hex(), 'type': 'channel_opening', 'label': self.get_label_for_txid(funding_txid), 'txid': funding_txid, @@ -912,7 +912,7 @@ def get_onchain_history(self): tx_height = self.wallet.adb.get_tx_height(closing_txid) self._labels_cache[closing_txid] = _('Close channel') + ' ' + chan.get_id_for_log() item = { - 'channel_id': bh2u(chan.channel_id), + 'channel_id': chan.channel_id.hex(), 'txid': closing_txid, 'label': self.get_label_for_txid(closing_txid), 'type': 'channel_closure', @@ -1309,7 +1309,7 @@ async def pay_to_node( code, data = failure_msg.code, failure_msg.data self.logger.info(f"UPDATE_FAIL_HTLC. code={repr(code)}. " f"decoded_data={failure_msg.decode_data()}. data={data.hex()!r}") - self.logger.info(f"error reported by {bh2u(erring_node_id)}") + self.logger.info(f"error reported by {erring_node_id.hex()}") if code == OnionFailureCode.MPP_TIMEOUT: raise PaymentFailure(failure_msg.code_name()) # trampoline @@ -1847,12 +1847,12 @@ def add_request( def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True): assert sha256(preimage) == payment_hash - self.preimages[bh2u(payment_hash)] = bh2u(preimage) + self.preimages[payment_hash.hex()] = preimage.hex() if write_to_disk: self.wallet.save_db() def get_preimage(self, payment_hash: bytes) -> Optional[bytes]: - r = self.preimages.get(bh2u(payment_hash)) + r = self.preimages.get(payment_hash.hex()) return bfh(r) if r else None def get_payment_info(self, payment_hash: bytes) -> Optional[PaymentInfo]: diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index 5db4a4a99..4134575ac 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -30,7 +30,7 @@ from typing import Sequence, Dict from types import MappingProxyType -from .util import resource_path, bfh, bh2u, randrange +from .util import resource_path, bfh, randrange from .crypto import hmac_oneshot from . import version from .logging import Logger @@ -224,7 +224,7 @@ def make_seed(self, *, seed_type=None, num_bits=None) -> str: def is_new_seed(x: str, prefix=version.SEED_PREFIX) -> bool: x = normalize_text(x) - s = bh2u(hmac_oneshot(b"Seed version", x.encode('utf8'), hashlib.sha512)) + s = hmac_oneshot(b"Seed version", x.encode('utf8'), hashlib.sha512).hex() return s.startswith(prefix) diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 043f7352f..554b71327 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -39,7 +39,7 @@ sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'contrib/generate_payreqpb2.sh'") from . import bitcoin, constants, ecc, util, transaction, x509, rsakey -from .util import bh2u, bfh, make_aiohttp_session +from .util import bfh, make_aiohttp_session from .invoices import Invoice, get_id_from_onchain_outputs from .crypto import sha256 from .bitcoin import address_to_script diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 6f8462295..cb7e614a1 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -10,7 +10,7 @@ from electrum.keystore import Hardware_KeyStore from electrum.transaction import PartialTransaction, Sighash from electrum.wallet import Standard_Wallet, Multisig_Wallet, Deterministic_Wallet -from electrum.util import bh2u, UserFacingException +from electrum.util import UserFacingException from electrum.base_wizard import ScriptTypeNotSupported, BaseWizard from electrum.logging import get_logger from electrum.plugin import Device, DeviceInfo, runs_in_hwd_thread @@ -524,7 +524,7 @@ def sign_transaction( if len(sigs) != len(tx.inputs()): raise Exception("Incorrect number of inputs signed.") # Should never occur sighash = Sighash.to_sigbytes(Sighash.ALL).hex() - signatures = [bh2u(ecc.der_sig_from_sig_string(x[1])) + sighash for x in sigs] + signatures = [ecc.der_sig_from_sig_string(x[1]).hex() + sighash for x in sigs] tx.update_signatures(signatures) def sign_message(self, keypath: str, message: bytes, script_type: str) -> bytes: diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index c68b7a542..15d57f374 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -14,7 +14,7 @@ from electrum.keystore import Hardware_KeyStore, KeyStoreWithMPK from electrum.transaction import PartialTransaction from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet -from electrum.util import bfh, bh2u, versiontuple, UserFacingException +from electrum.util import bfh, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from electrum.logging import get_logger diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py index d47dd4da3..05fab0bee 100644 --- a/electrum/plugins/cosigner_pool/qt.py +++ b/electrum/plugins/cosigner_pool/qt.py @@ -39,7 +39,7 @@ from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.wallet import Multisig_Wallet, Abstract_Wallet -from electrum.util import bh2u, bfh +from electrum.util import bfh from electrum.logging import Logger from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog @@ -156,7 +156,7 @@ def __init__(self, wallet: 'Multisig_Wallet', window: 'ElectrumWindow'): for key, keystore in wallet.keystores.items(): xpub = keystore.get_master_public_key() # type: str pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True) - _hash = bh2u(crypto.sha256d(pubkey)) + _hash = crypto.sha256d(pubkey).hex() if not keystore.is_watching_only(): self.keys.append((key, _hash)) else: diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index a2807fbcf..7bbb0d7ae 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -3,7 +3,7 @@ import sys from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING -from electrum.util import bfh, bh2u, UserCancelled, UserFacingException +from electrum.util import bfh, UserCancelled, UserFacingException from electrum.bip32 import BIP32Node from electrum import constants from electrum.i18n import _ @@ -331,7 +331,7 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] sighash = Sighash.to_sigbytes(Sighash.ALL).hex() - signatures = [(bh2u(x) + sighash) for x in signatures] + signatures = [(x.hex() + sighash) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index e70d43025..00c893e91 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -12,7 +12,6 @@ OkButton, CloseButton) from electrum.i18n import _ from electrum.plugin import hook -from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -345,7 +344,7 @@ def task(): def update(features): self.features = features set_label_enabled() - bl_hash = bh2u(features.bootloader_hash) + bl_hash = features.bootloader_hash.hex() bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) noyes = [_("No"), _("Yes")] endis = [_("Enable Passphrases"), _("Disable Passphrases")] diff --git a/electrum/plugins/revealer/revealer.py b/electrum/plugins/revealer/revealer.py index 33df769b2..1ca1b8fc6 100644 --- a/electrum/plugins/revealer/revealer.py +++ b/electrum/plugins/revealer/revealer.py @@ -4,7 +4,7 @@ from typing import NamedTuple, Optional, Dict, Tuple from electrum.plugin import BasePlugin -from electrum.util import to_bytes, bh2u, bfh +from electrum.util import to_bytes, bfh from .hmac_drbg import DRBG @@ -92,7 +92,7 @@ def get_noise_map(cls, versioned_seed: VersionedSeed) -> Dict[Tuple[int, int], i @classmethod def gen_random_versioned_seed(cls): version = cls.LATEST_VERSION - hex_seed = bh2u(os.urandom(16)) + hex_seed = os.urandom(16).hex() checksum = cls.code_hashid(version + hex_seed) return VersionedSeed(version=version.upper(), seed=hex_seed.upper(), diff --git a/electrum/plugins/safe_t/qt.py b/electrum/plugins/safe_t/qt.py index 92e7b3ac8..07d36ffd1 100644 --- a/electrum/plugins/safe_t/qt.py +++ b/electrum/plugins/safe_t/qt.py @@ -12,7 +12,6 @@ OkButton, CloseButton, getOpenFileName) from electrum.i18n import _ from electrum.plugin import hook -from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -221,7 +220,7 @@ def update(features): self.features = features set_label_enabled() if features.bootloader_hash: - bl_hash = bh2u(features.bootloader_hash) + bl_hash = features.bootloader_hash.hex() bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) else: bl_hash = "N/A" diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 5b1bae5c1..376bbfa60 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -3,7 +3,7 @@ import sys from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING -from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException +from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException from electrum.bip32 import BIP32Node from electrum import constants from electrum.i18n import _ @@ -301,7 +301,7 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] sighash = Sighash.to_sigbytes(Sighash.ALL).hex() - signatures = [(bh2u(x) + sighash) for x in signatures] + signatures = [(x.hex() + sighash) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread diff --git a/electrum/plugins/trezor/qt.py b/electrum/plugins/trezor/qt.py index 72674267e..a555dc626 100644 --- a/electrum/plugins/trezor/qt.py +++ b/electrum/plugins/trezor/qt.py @@ -11,7 +11,6 @@ OkButton, CloseButton, PasswordLineEdit, getOpenFileName) from electrum.i18n import _ from electrum.plugin import hook -from electrum.util import bh2u from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -487,7 +486,7 @@ def update(features): self.features = features set_label_enabled() if features.bootloader_hash: - bl_hash = bh2u(features.bootloader_hash) + bl_hash = features.bootloader_hash.hex() bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]]) else: bl_hash = "N/A" diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 5fdb3f50a..db52bd53f 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -2,7 +2,7 @@ import sys from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING -from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException +from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path from electrum import constants from electrum.i18n import _ @@ -371,7 +371,7 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): serialize=False, prev_txes=prev_tx) sighash = Sighash.to_sigbytes(Sighash.ALL).hex() - signatures = [(bh2u(x) + sighash) for x in signatures] + signatures = [(x.hex() + sighash) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread diff --git a/electrum/scripts/ln_features.py b/electrum/scripts/ln_features.py index 773eb84e4..8b6303fae 100644 --- a/electrum/scripts/ln_features.py +++ b/electrum/scripts/ln_features.py @@ -14,7 +14,7 @@ from electrum import constants from electrum.daemon import Daemon from electrum.wallet import create_new_wallet -from electrum.util import create_and_start_event_loop, log_exceptions, bh2u, bfh +from electrum.util import create_and_start_event_loop, log_exceptions, bfh from electrum.lnutil import LnFeatures logger = get_logger(__name__) @@ -77,9 +77,9 @@ async def worker(work_queue: asyncio.Queue, results_queue: asyncio.Queue, flag): # handle ipv4/ipv6 if ':' in addr[0]: - connect_str = f"{bh2u(work['pk'])}@[{addr.host}]:{addr.port}" + connect_str = f"{work['pk'].hex()}@[{addr.host}]:{addr.port}" else: - connect_str = f"{bh2u(work['pk'])}@{addr.host}:{addr.port}" + connect_str = f"{work['pk'].hex()}@{addr.host}:{addr.port}" print(f"worker connecting to {connect_str}") try: diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py index 48aa3db98..1525d96f3 100644 --- a/electrum/synchronizer.py +++ b/electrum/synchronizer.py @@ -32,7 +32,7 @@ from . import util from .transaction import Transaction, PartialTransaction -from .util import bh2u, make_aiohttp_session, NetworkJobOnDefaultServer, random_shuffled_copy, OldTaskGroup +from .util import make_aiohttp_session, NetworkJobOnDefaultServer, random_shuffled_copy, OldTaskGroup from .bitcoin import address_to_scripthash, is_address from .logging import Logger from .interface import GracefulDisconnect, NetworkTimeout @@ -51,7 +51,7 @@ def history_status(h): status = '' for tx_hash, height in h: status += tx_hash + ':%d:' % height - return bh2u(hashlib.sha256(status.encode('ascii')).digest()) + return hashlib.sha256(status.encode('ascii')).digest().hex() class SynchronizerBase(NetworkJobOnDefaultServer): diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index ecfa4bf0b..524aeb6a7 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -18,7 +18,7 @@ normalize_bip32_derivation, is_all_public_derivation) from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS from electrum import ecc, crypto, constants -from electrum.util import bfh, bh2u, InvalidPassword, randrange +from electrum.util import bfh, InvalidPassword, randrange from electrum.storage import WalletStorage from electrum.keystore import xtype_from_derivation @@ -450,17 +450,17 @@ def test_script_num_to_hex(self): def test_push_script(self): # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#push-operators - self.assertEqual(push_script(''), bh2u(bytes([opcodes.OP_0]))) - self.assertEqual(push_script('07'), bh2u(bytes([opcodes.OP_7]))) - self.assertEqual(push_script('10'), bh2u(bytes([opcodes.OP_16]))) - self.assertEqual(push_script('81'), bh2u(bytes([opcodes.OP_1NEGATE]))) + self.assertEqual(push_script(''), bytes([opcodes.OP_0]).hex()) + self.assertEqual(push_script('07'), bytes([opcodes.OP_7]).hex()) + self.assertEqual(push_script('10'), bytes([opcodes.OP_16]).hex()) + self.assertEqual(push_script('81'), bytes([opcodes.OP_1NEGATE]).hex()) self.assertEqual(push_script('11'), '0111') self.assertEqual(push_script(75 * '42'), '4b' + 75 * '42') - self.assertEqual(push_script(76 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('4c' + 76 * '42'))) - self.assertEqual(push_script(100 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('64' + 100 * '42'))) - self.assertEqual(push_script(255 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA1]) + bfh('ff' + 255 * '42'))) - self.assertEqual(push_script(256 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0001' + 256 * '42'))) - self.assertEqual(push_script(520 * '42'), bh2u(bytes([opcodes.OP_PUSHDATA2]) + bfh('0802' + 520 * '42'))) + self.assertEqual(push_script(76 * '42'), (bytes([opcodes.OP_PUSHDATA1]) + bfh('4c' + 76 * '42')).hex()) + self.assertEqual(push_script(100 * '42'), (bytes([opcodes.OP_PUSHDATA1]) + bfh('64' + 100 * '42')).hex()) + self.assertEqual(push_script(255 * '42'), (bytes([opcodes.OP_PUSHDATA1]) + bfh('ff' + 255 * '42')).hex()) + self.assertEqual(push_script(256 * '42'), (bytes([opcodes.OP_PUSHDATA2]) + bfh('0001' + 256 * '42')).hex()) + self.assertEqual(push_script(520 * '42'), (bytes([opcodes.OP_PUSHDATA2]) + bfh('0802' + 520 * '42')).hex()) def test_add_number_to_script(self): # https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#numbers diff --git a/electrum/tests/test_blockchain.py b/electrum/tests/test_blockchain.py index a58c688fe..cc57d21ef 100644 --- a/electrum/tests/test_blockchain.py +++ b/electrum/tests/test_blockchain.py @@ -5,7 +5,7 @@ from electrum import constants, blockchain from electrum.simple_config import SimpleConfig from electrum.blockchain import Blockchain, deserialize_header, hash_header -from electrum.util import bh2u, bfh, make_dir +from electrum.util import bfh, make_dir from . import ElectrumTestCase diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 1aa2510bb..36bb167a1 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -23,7 +23,7 @@ from electrum import simple_config, lnutil from electrum.lnaddr import lnencode, LnAddr, lndecode from electrum.bitcoin import COIN, sha256 -from electrum.util import bh2u, NetworkRetryManager, bfh, OldTaskGroup, EventListener +from electrum.util import NetworkRetryManager, bfh, OldTaskGroup, EventListener from electrum.lnpeer import Peer from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey from electrum.lnutil import PaymentFailure, LnFeatures, HTLCOwner @@ -1377,7 +1377,7 @@ def test_close_upfront_shutdown_script(self): # create upfront shutdown script for bob, alice doesn't use upfront # shutdown script bob_uss_pub = lnutil.privkey_to_pubkey(os.urandom(32)) - bob_uss_addr = bitcoin.pubkey_to_address('p2wpkh', bh2u(bob_uss_pub)) + bob_uss_addr = bitcoin.pubkey_to_address('p2wpkh', bob_uss_pub.hex()) bob_uss = bfh(bitcoin.address_to_script(bob_uss_addr)) # bob commits to close to bob_uss diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py index 1f466bd38..b31e950de 100644 --- a/electrum/tests/test_lnrouter.py +++ b/electrum/tests/test_lnrouter.py @@ -5,7 +5,7 @@ import asyncio from electrum import util -from electrum.util import bh2u, bfh +from electrum.util import bfh from electrum.lnutil import ShortChannelID from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet, process_onion_packet, _decode_onion_error, decode_onion_error, diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 299c8e557..e3dff0d60 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -10,7 +10,7 @@ get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features, IncompatibleLightningFeatures, ChannelType) -from electrum.util import bh2u, bfh, MyEncoder +from electrum.util import bfh, MyEncoder from electrum.transaction import Transaction, PartialTransaction, Sighash from electrum.lnworker import LNWallet @@ -575,7 +575,7 @@ def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, r htlc_output_txid=our_commit_tx.txid(), htlc_output_index=htlc_output_index, amount_msat=amount_msat, - witness_script=bh2u(htlc)) + witness_script=htlc.hex()) our_htlc_tx = make_htlc_tx( cltv_expiry=cltv_timeout, inputs=our_htlc_tx_inputs, @@ -724,7 +724,7 @@ def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remo assert type(privkey) is bytes assert len(pubkey) == 33 assert len(privkey) == 33 - tx.sign({bh2u(pubkey): (privkey[:-1], True)}) + tx.sign({pubkey.hex(): (privkey[:-1], True)}) sighash = Sighash.to_sigbytes(Sighash.ALL).hex() tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + sighash) diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py index 606f9dcdb..6273388e6 100644 --- a/electrum/tests/test_mnemonic.py +++ b/electrum/tests/test_mnemonic.py @@ -6,7 +6,7 @@ from electrum import mnemonic from electrum import slip39 from electrum import old_mnemonic -from electrum.util import bh2u, bfh +from electrum.util import bfh from electrum.mnemonic import is_new_seed, is_old_seed, seed_type from electrum.version import SEED_PREFIX_SW, SEED_PREFIX @@ -104,20 +104,20 @@ def test_mnemonic_to_seed_basic(self): # note: not a valid electrum seed seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic='foobar', passphrase='none') self.assertEqual('741b72fd15effece6bfe5a26a52184f66811bd2be363190e07a42cca442b1a5bb22b3ad0eb338197287e6d314866c7fba863ac65d3f156087a5052ebc7157fce', - bh2u(seed)) + seed.hex()) def test_mnemonic_to_seed(self): for test_name, test in SEED_TEST_CASES.items(): if test.words_hex is not None: - self.assertEqual(test.words_hex, bh2u(test.words.encode('utf8')), msg=test_name) + self.assertEqual(test.words_hex, test.words.encode('utf8').hex(), msg=test_name) self.assertTrue(is_new_seed(test.words, prefix=test.seed_version), msg=test_name) m = mnemonic.Mnemonic(lang=test.lang) if test.entropy is not None: self.assertEqual(test.entropy, m.mnemonic_decode(test.words), msg=test_name) if test.passphrase_hex is not None: - self.assertEqual(test.passphrase_hex, bh2u(test.passphrase.encode('utf8')), msg=test_name) + self.assertEqual(test.passphrase_hex, test.passphrase.encode('utf8').hex(), msg=test_name) seed = mnemonic.Mnemonic.mnemonic_to_seed(mnemonic=test.words, passphrase=test.passphrase) - self.assertEqual(test.bip32_seed, bh2u(seed), msg=test_name) + self.assertEqual(test.bip32_seed, seed.hex(), msg=test_name) def test_random_seeds(self): iters = 10 diff --git a/electrum/tests/test_network.py b/electrum/tests/test_network.py index 66d905b76..9d92091d2 100644 --- a/electrum/tests/test_network.py +++ b/electrum/tests/test_network.py @@ -7,7 +7,6 @@ from electrum import blockchain from electrum.interface import Interface, ServerAddr from electrum.crypto import sha256 -from electrum.util import bh2u from electrum import util from . import ElectrumTestCase @@ -105,7 +104,7 @@ def mock_connect(height): def mock_fork(self, bad_header): forkpoint = bad_header['block_height'] b = blockchain.Blockchain(config=self.config, forkpoint=forkpoint, parent=None, - forkpoint_hash=bh2u(sha256(str(forkpoint))), prev_hash=bh2u(sha256(str(forkpoint-1)))) + forkpoint_hash=sha256(str(forkpoint)).hex(), prev_hash=sha256(str(forkpoint-1)).hex()) return b def test_chain_false_during_binary(self): diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py index 3b33ac6e6..92ecc4dcd 100644 --- a/electrum/tests/test_transaction.py +++ b/electrum/tests/test_transaction.py @@ -5,7 +5,7 @@ PartialTransaction, TxOutpoint, PartialTxInput, PartialTxOutput, Sighash, match_script_against_template, SCRIPTPUBKEY_TEMPLATE_ANYSEGWIT) -from electrum.util import bh2u, bfh +from electrum.util import bfh from electrum.bitcoin import (deserialize_privkey, opcodes, construct_script, construct_witness) from electrum.ecc import ECPrivkey @@ -30,7 +30,7 @@ def test_compact_size(self): with self.assertRaises(transaction.SerializationError): s.write_compact_size(-1) - self.assertEqual(bh2u(s.input), + self.assertEqual(s.input.hex(), '0001fcfdfd00fdfffffe00000100feffffffffff0000000001000000ffffffffffffffffff') for v in values: self.assertEqual(s.read_compact_size(), v) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 58f61606a..9a7285daa 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -14,7 +14,7 @@ from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text, Abstract_Wallet) from electrum.util import ( - bfh, bh2u, NotEnoughFunds, UnrelatedTransactionException, + bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException) from electrum.transaction import (TxOutput, Transaction, PartialTransaction, PartialTxOutput, PartialTxInput, tx_from_any, TxOutpoint) @@ -445,7 +445,7 @@ def test_bip32_extended_version_bytes(self, mock_save_db): self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) bip32_seed = keystore.bip39_to_seed(seed_words, '') self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9', - bh2u(bip32_seed)) + bip32_seed.hex()) def create_keystore_from_bip32seed(xtype): ks = keystore.BIP32_KeyStore({}) @@ -643,7 +643,7 @@ def test_bip32_extended_version_bytes(self, mock_save_db): self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) bip32_seed = keystore.bip39_to_seed(seed_words, '') self.assertEqual('0df68c16e522eea9c1d8e090cfb2139c3b3a2abed78cbcb3e20be2c29185d3b8df4e8ce4e52a1206a688aeb88bfee249585b41a7444673d1f16c0d45755fa8b9', - bh2u(bip32_seed)) + bip32_seed.hex()) def create_keystore_from_bip32seed(xtype): ks = keystore.BIP32_KeyStore({}) diff --git a/electrum/transaction.py b/electrum/transaction.py index e718841ed..92c18c321 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -42,7 +42,7 @@ from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node -from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str, parse_max_spend +from .util import profiler, to_bytes, bfh, chunks, is_hex_str, parse_max_spend from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, @@ -650,7 +650,7 @@ def __init__(self, raw): self._cached_network_ser = raw.strip() if raw else None assert is_hex_str(self._cached_network_ser) elif isinstance(raw, (bytes, bytearray)): - self._cached_network_ser = bh2u(raw) + self._cached_network_ser = raw.hex() else: raise Exception(f"cannot initialize transaction from {raw}") self._inputs = None # type: List[TxInput] @@ -863,7 +863,7 @@ def get_preimage_script(cls, txin: 'PartialTxInput') -> str: return multisig_script(pubkeys, txin.num_sig) elif txin.script_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: pubkey = pubkeys[0] - pkh = bh2u(hash_160(bfh(pubkey))) + pkh = hash_160(bfh(pubkey)).hex() return bitcoin.pubkeyhash_to_p2pkh_script(pkh) elif txin.script_type == 'p2pk': pubkey = pubkeys[0] @@ -874,9 +874,9 @@ def get_preimage_script(cls, txin: 'PartialTxInput') -> str: def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: inputs = self.inputs() outputs = self.outputs() - hashPrevouts = bh2u(sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs))) - hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.nsequence, 4) for txin in inputs)))) - hashOutputs = bh2u(sha256d(bfh(''.join(o.serialize_to_network().hex() for o in outputs)))) + hashPrevouts = sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs)).hex() + hashSequence = sha256d(bfh(''.join(int_to_hex(txin.nsequence, 4) for txin in inputs))).hex() + hashOutputs = sha256d(bfh(''.join(o.serialize_to_network().hex() for o in outputs))).hex() return BIP143SharedTxDigestFields(hashPrevouts=hashPrevouts, hashSequence=hashSequence, hashOutputs=hashOutputs) @@ -949,7 +949,7 @@ def txid(self) -> Optional[str]: except UnknownTxinType: # we might not know how to construct scriptSig for some scripts return None - self._cached_txid = bh2u(sha256d(bfh(ser))[::-1]) + self._cached_txid = sha256d(bfh(ser))[::-1].hex() return self._cached_txid def wtxid(self) -> Optional[str]: @@ -961,7 +961,7 @@ def wtxid(self) -> Optional[str]: except UnknownTxinType: # we might not know how to construct scriptSig/witness for some scripts return None - return bh2u(sha256d(bfh(ser))[::-1]) + return sha256d(bfh(ser))[::-1].hex() def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None: return # no-op @@ -1963,7 +1963,7 @@ def serialize_preimage(self, txin_index: int, *, if (sighash & 0x1f) != Sighash.SINGLE and (sighash & 0x1f) != Sighash.NONE: hashOutputs = bip143_shared_txdigest_fields.hashOutputs elif (sighash & 0x1f) == Sighash.SINGLE and txin_index < len(outputs): - hashOutputs = bh2u(sha256d(outputs[txin_index].serialize_to_network())) + hashOutputs = sha256d(outputs[txin_index].serialize_to_network()).hex() else: hashOutputs = '00' * 32 outpoint = txin.prevout.serialize_to_network().hex() @@ -2006,7 +2006,7 @@ def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields= bip143_shared_txdigest_fields=bip143_shared_txdigest_fields))) privkey = ecc.ECPrivkey(privkey_bytes) sig = privkey.sign_transaction(pre_hash) - sig = bh2u(sig) + Sighash.to_sigbytes(sighash).hex() + sig = sig.hex() + Sighash.to_sigbytes(sighash).hex() return sig def is_complete(self) -> bool: diff --git a/electrum/util.py b/electrum/util.py index 0ad3a8cab..447c6fe85 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -585,17 +585,6 @@ def to_bytes(something, encoding='utf8') -> bytes: bfh = bytes.fromhex -def bh2u(x: bytes) -> str: - """ - str with hex representation of a bytes-like object - - >>> x = bytes((1, 2, 10)) - >>> bh2u(x) - '01020A' - """ - return x.hex() - - def xor_bytes(a: bytes, b: bytes) -> bytes: size = min(len(a), len(b)) return ((int.from_bytes(a[:size], "big") ^ int.from_bytes(b[:size], "big")) @@ -1036,7 +1025,7 @@ def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e if 'sig' in out: try: - out['sig'] = bh2u(bitcoin.base_decode(out['sig'], base=58)) + out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex() except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e if 'lightning' in out: diff --git a/electrum/verifier.py b/electrum/verifier.py index 73e7977e9..c5eb8c000 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -26,7 +26,7 @@ import aiorpcx -from .util import bh2u, TxMinedInfo, NetworkJobOnDefaultServer +from .util import TxMinedInfo, NetworkJobOnDefaultServer from .crypto import sha256d from .bitcoin import hash_decode, hash_encode from .transaction import Transaction @@ -153,7 +153,7 @@ def hash_merkle_root(cls, merkle_branch: Sequence[str], tx_hash: str, leaf_pos_i if len(item) != 32: raise MerkleVerificationFailure('all merkle branch items have to 32 bytes long') inner_node = (item + h) if (index & 1) else (h + item) - cls._raise_if_valid_tx(bh2u(inner_node)) + cls._raise_if_valid_tx(inner_node.hex()) h = sha256d(inner_node) index >>= 1 if index != 0: diff --git a/electrum/wallet.py b/electrum/wallet.py index 913611663..e882c9865 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -57,7 +57,7 @@ format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, - Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex, parse_max_spend) + Fiat, bfh, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex, parse_max_spend) from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE from .bitcoin import COIN, TYPE_ADDRESS from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold diff --git a/electrum/x509.py b/electrum/x509.py index 78ae26cf0..68cf92b94 100644 --- a/electrum/x509.py +++ b/electrum/x509.py @@ -28,7 +28,7 @@ from datetime import datetime from . import util -from .util import profiler, bh2u +from .util import profiler from .logging import get_logger @@ -272,10 +272,10 @@ def __init__(self, b): # Subject Key Identifier r = value.root() value = value.get_value_of_type(r, 'OCTET STRING') - self.SKI = bh2u(value) + self.SKI = value.hex() elif oid == '2.5.29.35': # Authority Key Identifier - self.AKI = bh2u(value.get_sequence()[0]) + self.AKI = value.get_sequence()[0].hex() else: pass From 8a4c06b692b04fbefa47d901b7ab698a1c028dbb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Feb 2023 14:04:03 +0000 Subject: [PATCH 0172/1143] swaps: small refactor and add unit tests for claim tx --- electrum/submarine_swaps.py | 66 ++++++++++++++++++----------- electrum/tests/test_sswaps.py | 78 +++++++++++++++++++++++++++++++++++ electrum/util.py | 4 ++ 3 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 electrum/tests/test_sswaps.py diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 1ffeca188..36746ce5d 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -14,7 +14,7 @@ is_segwit_address, construct_witness) from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey -from .util import log_exceptions +from .util import log_exceptions, BelowDustLimit from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address from .bitcoin import dust_threshold from .logging import Logger @@ -29,6 +29,7 @@ from .wallet import Abstract_Wallet from .lnwatcher import LNWalletWatcher from .lnworker import LNWallet + from .simple_config import SimpleConfig API_URL_MAINNET = 'https://swaps.electrum.org/api' @@ -116,7 +117,6 @@ def create_claim_tx( *, txin: PartialTxInput, witness_script: bytes, - preimage: Union[bytes, int], # 0 if timing out forward-swap address: str, amount_sat: int, locktime: int, @@ -124,11 +124,12 @@ def create_claim_tx( """Create tx to either claim successful reverse-swap, or to get refunded for timed-out forward-swap. """ + assert txin.address is not None if is_segwit_address(txin.address): txin.script_type = 'p2wsh' txin.script_sig = b'' else: - txin.script_type = 'p2wsh-p2sh' + txin.script_type = 'p2wsh-p2sh' # TODO rm?? txin.redeem_script = bytes.fromhex(p2wsh_nested_script(witness_script.hex())) txin.script_sig = bytes.fromhex(push_script(txin.redeem_script.hex())) txin.witness_script = witness_script @@ -217,33 +218,21 @@ async def _claim_swap(self, swap: SwapData) -> None: if not swap.is_reverse and delta < 0: # too early for refund return - # FIXME the mining fee should depend on swap.is_reverse. - # the txs are not the same size... - amount_sat = txin.value_sats() - self.get_claim_fee() - if amount_sat < dust_threshold(): + try: + tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) + except BelowDustLimit: self.logger.info('utxo value below dust threshold') continue - if swap.is_reverse: # successful reverse swap - preimage = swap.preimage - locktime = 0 - else: # timing out forward swap - preimage = 0 - locktime = swap.locktime - tx = create_claim_tx( - txin=txin, - witness_script=swap.redeem_script, - preimage=preimage, - address=swap.receive_address, - amount_sat=amount_sat, - locktime=locktime, - ) - self.sign_tx(tx, swap) self.logger.info(f'adding claim tx {tx.txid()}') self.wallet.adb.add_transaction(tx) swap.spending_txid = tx.txid() def get_claim_fee(self): - return self.wallet.config.estimate_fee(136, allow_fallback_to_static_rates=True) + return self._get_claim_fee(config=self.wallet.config) + + @classmethod + def _get_claim_fee(cls, *, config: 'SimpleConfig'): + return config.estimate_fee(136, allow_fallback_to_static_rates=True) def get_swap(self, payment_hash: bytes) -> Optional[SwapData]: # for history @@ -650,7 +639,8 @@ def add_txin_info(self, txin: PartialTxInput) -> None: witness = [sig_dummy, preimage, witness_script] txin.witness_sizehint = len(bytes.fromhex(construct_witness(witness))) - def sign_tx(self, tx: PartialTransaction, swap: SwapData) -> None: + @classmethod + def sign_tx(cls, tx: PartialTransaction, swap: SwapData) -> None: preimage = swap.preimage if swap.is_reverse else 0 witness_script = swap.redeem_script txin = tx.inputs()[0] @@ -663,6 +653,34 @@ def sign_tx(self, tx: PartialTransaction, swap: SwapData) -> None: witness = [sig, preimage, witness_script] txin.witness = bytes.fromhex(construct_witness(witness)) + @classmethod + def _create_and_sign_claim_tx( + cls, + *, + txin: PartialTxInput, + swap: SwapData, + config: 'SimpleConfig', + ) -> PartialTransaction: + # FIXME the mining fee should depend on swap.is_reverse. + # the txs are not the same size... + amount_sat = txin.value_sats() - cls._get_claim_fee(config=config) + if amount_sat < dust_threshold(): + raise BelowDustLimit() + if swap.is_reverse: # successful reverse swap + locktime = 0 + # preimage will be set in sign_tx + else: # timing out forward swap + locktime = swap.locktime + tx = create_claim_tx( + txin=txin, + witness_script=swap.redeem_script, + address=swap.receive_address, + amount_sat=amount_sat, + locktime=locktime, + ) + cls.sign_tx(tx, swap) + return tx + def max_amount_forward_swap(self) -> Optional[int]: """ returns None if we cannot swap """ max_swap_amt_ln = self.get_max_amount() diff --git a/electrum/tests/test_sswaps.py b/electrum/tests/test_sswaps.py new file mode 100644 index 000000000..377fa31ec --- /dev/null +++ b/electrum/tests/test_sswaps.py @@ -0,0 +1,78 @@ +from electrum import SimpleConfig +from electrum.util import bfh +from electrum.transaction import PartialTxInput, TxOutpoint +from electrum.submarine_swaps import SwapManager, SwapData + +from . import TestCaseForTestnet + + +class TestSwapTxs(TestCaseForTestnet): + + def setUp(self): + super().setUp() + self.config = SimpleConfig({'electrum_path': self.electrum_path}) + self.config.set_key('dynamic_fees', False) + self.config.set_key('fee_per_kb', 1000) + + def test_claim_tx_for_successful_reverse_swap(self): + swap_data = SwapData( + is_reverse=True, + locktime=2420532, + onchain_amount=198694, + lightning_amount=200000, + redeem_script=bytes.fromhex('8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac'), + preimage=bytes.fromhex('f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a36'), + prepay_hash=None, + privkey=bytes.fromhex('58fd0018a9a2737d1d6b81d380df96bf0c858473a9592015508a270a7c9b1d8d'), + lockup_address='tb1q2pvugjl4w56rqw4c7zg0q6mmmev0t5jjy3qzg7sl766phh9fxjxsrtl77t', + receive_address='tb1ql0adrj58g88xgz375yct63rclhv29hv03u0mel', + funding_txid='897eea7f53e917323e7472d7a2e3099173f7836c57f1b6850f5cbdfe8085dbf9', + spending_txid=None, + is_redeemed=False, + ) + txin = PartialTxInput( + prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0), + ) + txin._trusted_value_sats = swap_data.onchain_amount + txin._trusted_address = swap_data.lockup_address + tx = SwapManager._create_and_sign_claim_tx( + txin=txin, + swap=swap_data, + config=self.config, + ) + self.assertEqual( + "02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019e07030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f034730440220156d62534a4e8247eef6bb185c89c4013353c017e45d41ce634976b9d7122c6202202ddb593983fd789cf2166038411425c119d087bc37ec7f8b51bebf603e428fbb0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000", + str(tx) + ) + + def test_claim_tx_for_timing_out_forward_swap(self): + swap_data = SwapData( + is_reverse=False, + locktime=2420537, + onchain_amount=130000, + lightning_amount=129014, + redeem_script=bytes.fromhex('a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac'), + preimage=bytes.fromhex('116f62c3283e4eb0b947a9cb672f1de7321d2c2373d12cd010500adffc32b1f2'), + prepay_hash=None, + privkey=bytes.fromhex('8d30dead21f5a7a6eeab7456a9a9d449511e942abef9302153cfff84e436614c'), + lockup_address='tb1qte2qwev6qvmrhsddac82tnskmjg02ntn73xqg2rjt0qx2xpz693sw2ljzg', + receive_address='tb1qj76twx886pkfcs7d808n0yzsgxm33wqlwe0dt0', + funding_txid='08ecdcb19ab38fc1288c97da546b8c90549be2348ef306f476dcf6e505158706', + spending_txid=None, + is_redeemed=False, + ) + txin = PartialTxInput( + prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0), + ) + txin._trusted_value_sats = swap_data.onchain_amount + txin._trusted_address = swap_data.lockup_address + tx = SwapManager._create_and_sign_claim_tx( + txin=txin, + swap=swap_data, + config=self.config, + ) + self.assertEqual( + "0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff0148fb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f034730440220254e054fc195801aca3d62641a0f27d888f44d1dd66760ae5c3418502e82c141022014305da98daa27d665310115845d2fa6d4dc612d910a186db2624aa558bff9fe010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400", + str(tx) + ) + diff --git a/electrum/util.py b/electrum/util.py index 447c6fe85..b7ec0c97d 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -144,6 +144,10 @@ def __str__(self): return _('Dynamic fee estimates not available') +class BelowDustLimit(Exception): + pass + + class InvalidPassword(Exception): def __init__(self, message: Optional[str] = None): self.message = message From 72e1be6f5eff75a5311c406dcd3d2f4244c94755 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Feb 2023 14:10:03 +0000 Subject: [PATCH 0173/1143] swaps: rm support for p2wsh-p2sh lockup scripts - unused - the client was already refusing to fund such lockup addresses (if the server asked) - no existing unit tests for it, and as the choice is up to the server, it is hard to create tests - no clear reason to want to use p2sh-nested scripts here, aside from curiosity --- electrum/submarine_swaps.py | 10 ++-------- electrum/tests/test_sswaps.py | 2 -- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 36746ce5d..cd62f98d8 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -124,14 +124,8 @@ def create_claim_tx( """Create tx to either claim successful reverse-swap, or to get refunded for timed-out forward-swap. """ - assert txin.address is not None - if is_segwit_address(txin.address): - txin.script_type = 'p2wsh' - txin.script_sig = b'' - else: - txin.script_type = 'p2wsh-p2sh' # TODO rm?? - txin.redeem_script = bytes.fromhex(p2wsh_nested_script(witness_script.hex())) - txin.script_sig = bytes.fromhex(push_script(txin.redeem_script.hex())) + txin.script_type = 'p2wsh' + txin.script_sig = b'' txin.witness_script = witness_script txout = PartialTxOutput.from_address_and_value(address, amount_sat) tx = PartialTransaction.from_io([txin], [txout], version=2, locktime=locktime) diff --git a/electrum/tests/test_sswaps.py b/electrum/tests/test_sswaps.py index 377fa31ec..3011b2eca 100644 --- a/electrum/tests/test_sswaps.py +++ b/electrum/tests/test_sswaps.py @@ -34,7 +34,6 @@ def test_claim_tx_for_successful_reverse_swap(self): prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0), ) txin._trusted_value_sats = swap_data.onchain_amount - txin._trusted_address = swap_data.lockup_address tx = SwapManager._create_and_sign_claim_tx( txin=txin, swap=swap_data, @@ -65,7 +64,6 @@ def test_claim_tx_for_timing_out_forward_swap(self): prevout=TxOutpoint(txid=bfh(swap_data.funding_txid), out_idx=0), ) txin._trusted_value_sats = swap_data.onchain_amount - txin._trusted_address = swap_data.lockup_address tx = SwapManager._create_and_sign_claim_tx( txin=txin, swap=swap_data, From c5bdd5007ceb9c95a0e7db92736c1c06975f0120 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 18 Feb 2023 06:44:30 +0000 Subject: [PATCH 0174/1143] tests: rework testnet Inheritance was overkill here, and now we can use inheritance for new functionality X without having to create classes for all combinations of {X, is_testnet}. --- electrum/tests/__init__.py | 27 +++++++++++++------------- electrum/tests/test_bitcoin.py | 7 ++++--- electrum/tests/test_commands.py | 5 +++-- electrum/tests/test_invoices.py | 9 +++++---- electrum/tests/test_lnmsg.py | 5 +++-- electrum/tests/test_lnpeer.py | 5 +++-- electrum/tests/test_lnrouter.py | 5 +++-- electrum/tests/test_psbt.py | 14 ++++++++----- electrum/tests/test_sswaps.py | 5 +++-- electrum/tests/test_transaction.py | 5 +++-- electrum/tests/test_verifier.py | 5 +++-- electrum/tests/test_wallet_vertical.py | 22 +++++++++++++-------- 12 files changed, 66 insertions(+), 48 deletions(-) diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py index 5a2a25348..899a08c8e 100644 --- a/electrum/tests/__init__.py +++ b/electrum/tests/__init__.py @@ -36,8 +36,21 @@ def tearDown(self): class ElectrumTestCase(SequentialTestCase): """Base class for our unit tests.""" + TESTNET = False # maxDiff = None + @classmethod + def setUpClass(cls): + super().setUpClass() + if cls.TESTNET: + constants.set_testnet() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + if cls.TESTNET: + constants.set_mainnet() + def setUp(self): super().setUp() self.asyncio_loop, self._stop_loop, self._loop_thread = util.create_and_start_event_loop() @@ -50,20 +63,6 @@ def tearDown(self): shutil.rmtree(self.electrum_path) -class TestCaseForTestnet(ElectrumTestCase): - """Class that runs member tests in testnet mode""" - - @classmethod - def setUpClass(cls): - super().setUpClass() - constants.set_testnet() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - constants.set_mainnet() - - def as_testnet(func): """Function decorator to run a single unit test in testnet mode. diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 524aeb6a7..574cd8bd1 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -25,7 +25,6 @@ from electrum import ecc_fast from . import ElectrumTestCase -from . import TestCaseForTestnet from . import FAST_TESTS @@ -653,7 +652,8 @@ def test_bech32_decode(self): segwit_addr.bech32_decode('1p2gdwpf')) -class Test_bitcoin_testnet(TestCaseForTestnet): +class Test_bitcoin_testnet(ElectrumTestCase): + TESTNET = True def test_address_to_script(self): # bech32/bech32m native segwit @@ -941,7 +941,8 @@ def test_version_bytes(self): self.assertTrue(xkey_b58.startswith(xpub_headers_b58[xtype])) -class Test_xprv_xpub_testnet(TestCaseForTestnet): +class Test_xprv_xpub_testnet(ElectrumTestCase): + TESTNET = True def test_version_bytes(self): xprv_headers_b58 = { diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index 99ab167c6..845592c71 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -9,7 +9,7 @@ from electrum.simple_config import SimpleConfig from electrum.transaction import Transaction, TxOutput, tx_from_any -from . import TestCaseForTestnet, ElectrumTestCase +from . import ElectrumTestCase from .test_wallet_vertical import WalletIntegrityHelper @@ -124,7 +124,8 @@ def test_export_private_key_deterministic(self, mock_save_db): cmds._run('getprivatekeys', (['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'],), wallet=wallet)) -class TestCommandsTestnet(TestCaseForTestnet): +class TestCommandsTestnet(ElectrumTestCase): + TESTNET = True def setUp(self): super().setUp() diff --git a/electrum/tests/test_invoices.py b/electrum/tests/test_invoices.py index bf701cb56..6048141fb 100644 --- a/electrum/tests/test_invoices.py +++ b/electrum/tests/test_invoices.py @@ -1,7 +1,7 @@ import os import time -from . import TestCaseForTestnet +from . import ElectrumTestCase from electrum.simple_config import SimpleConfig from electrum.wallet import restore_wallet_from_text, Standard_Wallet, Abstract_Wallet @@ -11,18 +11,19 @@ from electrum.util import TxMinedInfo -class TestWalletPaymentRequests(TestCaseForTestnet): +class TestWalletPaymentRequests(ElectrumTestCase): """test 'incoming' invoices""" + TESTNET = True def setUp(self): - TestCaseForTestnet.setUp(self) + super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.wallet1_path = os.path.join(self.electrum_path, "somewallet1") self.wallet2_path = os.path.join(self.electrum_path, "somewallet2") self._orig_get_cur_time = Invoice._get_cur_time def tearDown(self): - TestCaseForTestnet.tearDown(self) + super().tearDown() Invoice._get_cur_time = staticmethod(self._orig_get_cur_time) def create_wallet2(self) -> Standard_Wallet: diff --git a/electrum/tests/test_lnmsg.py b/electrum/tests/test_lnmsg.py index 57e8f2ea6..d83e7fc0a 100644 --- a/electrum/tests/test_lnmsg.py +++ b/electrum/tests/test_lnmsg.py @@ -10,10 +10,11 @@ from electrum.lnutil import ShortChannelID, LnFeatures from electrum import constants -from . import TestCaseForTestnet +from . import ElectrumTestCase -class TestLNMsg(TestCaseForTestnet): +class TestLNMsg(ElectrumTestCase): + TESTNET = True def test_write_bigsize_int(self): self.assertEqual(bfh("00"), write_bigsize_int(0)) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 36bb167a1..7342d3d1b 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -44,7 +44,7 @@ from .test_lnchannel import create_test_channels from .test_bitcoin import needs_test_with_all_chacha20_implementations -from . import TestCaseForTestnet +from . import ElectrumTestCase def keypair(): priv = ECPrivkey.generate_random_key().get_secret_bytes() @@ -392,7 +392,8 @@ class PaymentDone(Exception): pass class SuccessfulTest(Exception): pass -class TestPeer(TestCaseForTestnet): +class TestPeer(ElectrumTestCase): + TESTNET = True @classmethod def setUpClass(cls): diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py index b31e950de..be66ab5cf 100644 --- a/electrum/tests/test_lnrouter.py +++ b/electrum/tests/test_lnrouter.py @@ -15,7 +15,7 @@ from electrum.simple_config import SimpleConfig from electrum.lnrouter import PathEdge, LiquidityHintMgr, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH, DEFAULT_PENALTY_BASE_MSAT, fee_for_edge_msat -from . import TestCaseForTestnet +from . import ElectrumTestCase from .test_bitcoin import needs_test_with_all_chacha20_implementations @@ -27,7 +27,8 @@ def node(character: str) -> bytes: return b'\x02' + f'{character}'.encode() * 32 -class Test_LNRouter(TestCaseForTestnet): +class Test_LNRouter(ElectrumTestCase): + TESTNET = True cdb = None diff --git a/electrum/tests/test_psbt.py b/electrum/tests/test_psbt.py index 8ef0f2331..3a9655838 100644 --- a/electrum/tests/test_psbt.py +++ b/electrum/tests/test_psbt.py @@ -5,11 +5,12 @@ from electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream, SerializationError, PSBTInputConsistencyFailure) -from . import ElectrumTestCase, TestCaseForTestnet +from . import ElectrumTestCase -class TestValidPSBT(TestCaseForTestnet): +class TestValidPSBT(ElectrumTestCase): # test cases from BIP-0174 + TESTNET = True def test_valid_psbt_001(self): # Case: PSBT with one P2PKH input. Outputs are empty @@ -97,8 +98,9 @@ def test_valid_psbt__input_with_both_witness_utxo_and_nonwitness_utxo(self): self.assertEqual(1, len(tx.inputs())) -class TestInvalidPSBT(TestCaseForTestnet): +class TestInvalidPSBT(ElectrumTestCase): # test cases from BIP-0174 + TESTNET = True def test_invalid_psbt_001(self): # Case: Network transaction, not PSBT format @@ -232,8 +234,9 @@ def test_invalid_psbt__input_with_both_witness_utxo_and_nonwitness_utxo_that_are tx = tx_from_any(bytes.fromhex('70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096990000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a0100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000')) -class TestPSBTSignerChecks(TestCaseForTestnet): +class TestPSBTSignerChecks(ElectrumTestCase): # test cases from BIP-0174 + TESTNET = True @unittest.skip("the check this test is testing is intentionally disabled in transaction.py") def test_psbt_fails_signer_checks_001(self): @@ -269,8 +272,9 @@ def test_psbt_fails_signer_checks_004(self): tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSrSIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') -class TestPSBTComplexChecks(TestCaseForTestnet): +class TestPSBTComplexChecks(ElectrumTestCase): # test cases from BIP-0174 + TESTNET = True def test_psbt_combiner_unknown_fields(self): tx1 = tx_from_any("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00") diff --git a/electrum/tests/test_sswaps.py b/electrum/tests/test_sswaps.py index 3011b2eca..3ce2906e0 100644 --- a/electrum/tests/test_sswaps.py +++ b/electrum/tests/test_sswaps.py @@ -3,10 +3,11 @@ from electrum.transaction import PartialTxInput, TxOutpoint from electrum.submarine_swaps import SwapManager, SwapData -from . import TestCaseForTestnet +from . import ElectrumTestCase -class TestSwapTxs(TestCaseForTestnet): +class TestSwapTxs(ElectrumTestCase): + TESTNET = True def setUp(self): super().setUp() diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py index 92ecc4dcd..c1bb1bb6f 100644 --- a/electrum/tests/test_transaction.py +++ b/electrum/tests/test_transaction.py @@ -11,7 +11,7 @@ from electrum.ecc import ECPrivkey from .test_bitcoin import disable_ecdsa_r_value_grinding -from . import ElectrumTestCase, TestCaseForTestnet +from . import ElectrumTestCase signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" @@ -857,7 +857,8 @@ def test_txid_bitcoin_core_0092(self): # txns from Bitcoin Core ends <--- -class TestTransactionTestnet(TestCaseForTestnet): +class TestTransactionTestnet(ElectrumTestCase): + TESTNET = True def test_spending_op_cltv_p2sh(self): # from https://github.com/brianddk/reddit/blob/8ca383c9e00cb5a4c1201d1bab534d5886d3cb8f/python/elec-p2sh-hodl.py diff --git a/electrum/tests/test_verifier.py b/electrum/tests/test_verifier.py index 009de2ab9..786544108 100644 --- a/electrum/tests/test_verifier.py +++ b/electrum/tests/test_verifier.py @@ -5,7 +5,7 @@ from electrum.util import bfh from electrum.verifier import SPV, InnerNodeOfSpvProofIsValidTx -from . import TestCaseForTestnet +from . import ElectrumTestCase MERKLE_BRANCH = [ @@ -19,9 +19,10 @@ assert len(VALID_64_BYTE_TX) == 128 -class VerifierTestCase(TestCaseForTestnet): +class VerifierTestCase(ElectrumTestCase): # these tests are regarding the attack described in # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-June/016105.html + TESTNET = True def test_verify_ok_t_tx(self): """Actually mined 64 byte tx should not raise.""" diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 9a7285daa..c1c395487 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -23,7 +23,6 @@ from electrum.plugins.trustedcoin import trustedcoin -from . import TestCaseForTestnet from . import ElectrumTestCase @@ -610,7 +609,8 @@ def test_slip39_groups_256bit_bip49_p2sh_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '32tvTmBLfLofu8ps4SWpUJC4fS699jiWvC') -class TestWalletKeystoreAddressIntegrityForTestnet(TestCaseForTestnet): +class TestWalletKeystoreAddressIntegrityForTestnet(ElectrumTestCase): + TESTNET = True def setUp(self): super().setUp() @@ -697,7 +697,8 @@ def create_keystore_from_bip32seed(xtype): self.assertEqual(w.get_change_addresses()[0], 'tb1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3ts5777jl') -class TestWalletSending(TestCaseForTestnet): +class TestWalletSending(ElectrumTestCase): + TESTNET = True def setUp(self): super().setUp() @@ -2692,7 +2693,8 @@ def test_export_psbt_with_xpubs__singlesig(self, mock_save_db): tx.serialize_as_bytes().hex()) -class TestWalletOfflineSigning(TestCaseForTestnet): +class TestWalletOfflineSigning(ElectrumTestCase): + TESTNET = True def setUp(self): super().setUp() @@ -3399,7 +3401,8 @@ def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_save_db): self.assertEqual('4376fa5f1f6cb37b1f3956175d3bd4ef6882169294802b250a3c672f3ff431c1', tx.wtxid()) -class TestWalletHistory_SimpleRandomOrder(TestCaseForTestnet): +class TestWalletHistory_SimpleRandomOrder(ElectrumTestCase): + TESTNET = True transactions = { "0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67": "01000000029d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2000000008b483045022100a146a2078a318c1266e42265a369a8eef8993750cb3faa8dd80754d8d541d5d202207a6ab8864986919fd1a7fd5854f1e18a8a0431df924d7a878ec3dc283e3d75340141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff9d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2010000008a47304402201c7fa37b74a915668b0244c01f14a9756bbbec1031fb69390bcba236148ab37e02206151581f9aa0e6758b503064c1e661a726d75c6be3364a5a121a8c12cf618f64014104dc28da82e141416aaf771eb78128d00a55fdcbd13622afcbb7a3b911e58baa6a99841bfb7b99bcb7e1d47904fda5d13fdf9675cdbbe73e44efcc08165f49bac6feffffff02b0183101000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac005a6202000000001976a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac54c51200", "2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d": "010000000132201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a050000006a47304402201d20bb5629a35b84ff9dd54788b98e265623022894f12152ac0e6158042550fe02204e98969e1f7043261912dd0660d3da64e15acf5435577fc02a00eccfe76b323f012103a336ad86546ab66b6184238fe63bb2955314be118b32fa45dd6bd9c4c5875167fdffffff0254959800000000001976a9148d2db0eb25b691829a47503006370070bc67400588ac80969800000000001976a914f96669095e6df76cfdf5c7e49a1909f002e123d088ace8ca1200", @@ -3460,7 +3463,8 @@ def test_restoring_old_wallet_txorder3(self, mock_save_db): self.assertEqual(27633300, sum(w.get_balance())) -class TestWalletHistory_EvilGapLimit(TestCaseForTestnet): +class TestWalletHistory_EvilGapLimit(ElectrumTestCase): + TESTNET = True transactions = { # txn A: "511a35e240f4c8855de4c548dad932d03611a37e94e9203fdb6fc79911fe1dd4": "010000000001018aacc3c8f98964232ebb74e379d8ff4e800991eecfcf64bd1793954f5e50a8790100000000fdffffff0340420f0000000000160014dbf321e905d544b54b86a2f3ed95b0ac66a3ddb0ff0514000000000016001474f1c130d3db22894efb3b7612b2c924628d0d7e80841e000000000016001488492707677190c073b6555fb08d37e91bbb75d802483045022100cf2904e09ea9d2670367eccc184d92fcb8a9b9c79a12e4efe81df161077945db02203530276a3401d944cf7a292e0660f36ee1df4a1c92c131d2c0d31d267d52524901210215f523a412a5262612e1a5ef9842dc864b0d73dc61fb4c6bfd480a867bebb1632e181400", @@ -3517,7 +3521,8 @@ def test_restoring_wallet_txorder1(self, mock_save_db): self.assertEqual(9999788, sum(w.get_balance())) -class TestWalletHistory_DoubleSpend(TestCaseForTestnet): +class TestWalletHistory_DoubleSpend(ElectrumTestCase): + TESTNET = True transactions = { # txn A: "a3849040f82705151ba12a4389310b58a17b78025d81116a3338595bdefa1625": "020000000001011b7eb29921187b40209c234344f57a3365669c8883a3d511fbde5155f11f64d10000000000fdffffff024c400f0000000000160014b50d21483fb5e088db90bf766ea79219fb377fef40420f0000000000160014aaf5fc4a6297375c32403a9c2768e7029c8dbd750247304402206efd510954b289829f8f778163b98a2a4039deb93c3b0beb834b00cd0add14fd02201c848315ddc52ced0350a981fe1a7f3cbba145c7a43805db2f126ed549eaa500012103083a50d63264743456a3e812bfc91c11bd2a673ba4628c09f02d78f62157e56d788d1700", @@ -3567,7 +3572,8 @@ def test_restoring_wallet_with_manual_delete(self, mock_save_db): self.assertEqual(999890, sum(w.get_balance())) -class TestImportedWallet(TestCaseForTestnet): +class TestImportedWallet(ElectrumTestCase): + TESTNET = True transactions = { # txn A funds addr1: "0e350564ee7ed4ffce24a998b538f7f3ebbab6fcb4bb331f8bb6b9d86d86fcd8": "02000000000101470cfc737af6bf917ce35bf7224b1021ef87349cd7f150464e6a0e3ee0cf6f1a0400000000fdffffff0261de0c0000000000160014f6aa7ea83b54335553ece4de88b3e9af6fb4ff0b92b78b00000000001600141dfacc496a9c98227631e3df4796baf3ba8254120247304402201a1b70f27ffcaeecaebad147117e9f4f541e3c630112c395e8237b5f1404f9170220600c96b92a55f8ee99da3fcaf9ca5595468742107651c5cea5798b0e672c7a5b012103ccaf45a46ead9648fc60ba0476f3f820d73fbf75f7d9af626d0512a042c1fc9a41091e00", From 9ad2c9138d1825223bc9903e246ea9761d296044 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 18 Feb 2023 06:56:43 +0000 Subject: [PATCH 0175/1143] tests: rm SequentialTestCase to further simplify inheritance --- electrum/tests/__init__.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py index 899a08c8e..d081731ba 100644 --- a/electrum/tests/__init__.py +++ b/electrum/tests/__init__.py @@ -19,25 +19,14 @@ electrum.logging._configure_stderr_logging() -# some unit tests are modifying globals... -class SequentialTestCase(unittest.TestCase): - - test_lock = threading.Lock() - - def setUp(self): - super().setUp() - self.test_lock.acquire() - - def tearDown(self): - super().tearDown() - self.test_lock.release() - - -class ElectrumTestCase(SequentialTestCase): +class ElectrumTestCase(unittest.TestCase): """Base class for our unit tests.""" TESTNET = False - # maxDiff = None + # maxDiff = None # for debugging + + # some unit tests are modifying globals... so we run sequentially: + _test_lock = threading.Lock() @classmethod def setUpClass(cls): @@ -52,6 +41,7 @@ def tearDownClass(cls): constants.set_mainnet() def setUp(self): + self._test_lock.acquire() super().setUp() self.asyncio_loop, self._stop_loop, self._loop_thread = util.create_and_start_event_loop() self.electrum_path = tempfile.mkdtemp() @@ -59,8 +49,9 @@ def setUp(self): def tearDown(self): self.asyncio_loop.call_soon_threadsafe(self._stop_loop.set_result, 1) self._loop_thread.join(timeout=1) - super().tearDown() shutil.rmtree(self.electrum_path) + super().tearDown() + self._test_lock.release() def as_testnet(func): From 9a5496cfd8bdc400686d7246c0f67fe00a2fb2af Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 20 Feb 2023 10:06:26 +0100 Subject: [PATCH 0176/1143] Qt: remove redundant history_list update --- electrum/gui/qt/main_window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 79fdec3ea..dedb4f5c0 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2483,7 +2483,6 @@ def update_fiat(self): self.send_tab.fiat_send_e.setVisible(b) self.receive_tab.fiat_receive_e.setVisible(b) self.history_model.refresh('update_fiat') - self.history_list.update() self.address_list.refresh_headers() self.address_list.update() self.update_status() From f5eabaff5508ac779175045aead83a3efc9d37fd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Feb 2023 12:00:38 +0000 Subject: [PATCH 0177/1143] ci: also run unit tests with PYTHONASYNCIODEBUG=1 This can reveal additional asyncio-related bugs, and due to also enabling the full "debug mode", maybe more. --- .cirrus.yml | 10 +++++++++- tox.ini | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.cirrus.yml b/.cirrus.yml index 1f03e32ee..34cb5e60f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -17,9 +17,15 @@ task: - env: ELECTRUM_PYTHON_VERSION: 3.10 - env: - ELECTRUM_PYTHON_VERSION: 3 + ELECTRUM_PYTHON_VERSION: 3.11 - env: ELECTRUM_PYTHON_VERSION: rc + - name: Tox Python 3 debug mode + env: + ELECTRUM_PYTHON_VERSION: 3 + # enable additional checks: + PYTHONASYNCIODEBUG: "1" + PYTHONDEVMODE: "1" - name: Tox PyPy allow_failures: true env: @@ -38,6 +44,8 @@ task: - apt-get -y install libsecp256k1-0 - pip install -r $ELECTRUM_REQUIREMENTS_CI tox_script: + - export PYTHONASYNCIODEBUG + - export PYTHONDEVMODE - tox coveralls_script: - if [ ! -z "$COVERALLS_REPO_TOKEN" ] ; then coveralls ; fi diff --git a/tox.ini b/tox.ini index f36336f92..367518c06 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,9 @@ deps= pytest coverage +passenv= + PYTHONASYNCIODEBUG + PYTHONDEVMODE commands= coverage run --source=electrum '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' -m pytest -v coverage report From dcd158dfdc82863c95e3738bad0ec1b11e9156af Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Feb 2023 15:14:20 +0000 Subject: [PATCH 0178/1143] tox.ini: (trivial) reformat tabs->spaces --- tox.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 367518c06..85492ff08 100644 --- a/tox.ini +++ b/tox.ini @@ -2,13 +2,13 @@ [testenv] deps= - pytest - coverage + pytest + coverage passenv= - PYTHONASYNCIODEBUG - PYTHONDEVMODE + PYTHONASYNCIODEBUG + PYTHONDEVMODE commands= - coverage run --source=electrum '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' -m pytest -v - coverage report + coverage run --source=electrum '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' -m pytest -v + coverage report extras= - tests + tests From 3ebf1e44bfee412993a3dadee0e667b531bd9b7a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Feb 2023 15:16:30 +0000 Subject: [PATCH 0179/1143] tox.ini: allow running tox from local dev env Some folders e.g. contrib/*/fresh_clone should not be searched. --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 85492ff08..58e09e963 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,9 @@ passenv= PYTHONASYNCIODEBUG PYTHONDEVMODE commands= - coverage run --source=electrum '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' -m pytest -v + coverage run --source=electrum \ + '--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*,electrum/tests/*' \ + -m pytest electrum/tests -v coverage report extras= tests From d4338fb5030201f8201c1a8a16697cb765492dd4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 18 Feb 2023 10:01:21 +0000 Subject: [PATCH 0180/1143] tests: clean-up use of asyncio --- electrum/tests/__init__.py | 31 +-- electrum/tests/test_bitcoin.py | 125 +++++++----- electrum/tests/test_commands.py | 82 ++++---- electrum/tests/test_invoices.py | 14 +- electrum/tests/test_lnpeer.py | 193 +++++++++--------- electrum/tests/test_lnrouter.py | 16 +- electrum/tests/test_lntransport.py | 9 +- electrum/tests/test_network.py | 28 +-- electrum/tests/test_storage_upgrade.py | 264 ++++++++++++------------- electrum/tests/test_wallet.py | 30 ++- electrum/tests/test_wallet_vertical.py | 172 ++++++++-------- electrum/util.py | 23 ++- electrum/wallet.py | 10 +- 13 files changed, 513 insertions(+), 484 deletions(-) diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py index d081731ba..ed26800ff 100644 --- a/electrum/tests/__init__.py +++ b/electrum/tests/__init__.py @@ -1,3 +1,4 @@ +import asyncio import unittest import threading import tempfile @@ -18,8 +19,10 @@ electrum.logging._configure_stderr_logging() +electrum.util.AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = True -class ElectrumTestCase(unittest.TestCase): + +class ElectrumTestCase(unittest.IsolatedAsyncioTestCase): """Base class for our unit tests.""" TESTNET = False @@ -43,12 +46,9 @@ def tearDownClass(cls): def setUp(self): self._test_lock.acquire() super().setUp() - self.asyncio_loop, self._stop_loop, self._loop_thread = util.create_and_start_event_loop() self.electrum_path = tempfile.mkdtemp() def tearDown(self): - self.asyncio_loop.call_soon_threadsafe(self._stop_loop.set_result, 1) - self._loop_thread.join(timeout=1) shutil.rmtree(self.electrum_path) super().tearDown() self._test_lock.release() @@ -59,12 +59,19 @@ def as_testnet(func): NOTE: this is inherently sequential; tests running in parallel would break things """ - def run_test(*args, **kwargs): - old_net = constants.net - try: - constants.set_testnet() - func(*args, **kwargs) - finally: - constants.net = old_net + old_net = constants.net + if asyncio.iscoroutinefunction(func): + async def run_test(*args, **kwargs): + try: + constants.set_testnet() + return await func(*args, **kwargs) + finally: + constants.net = old_net + else: + def run_test(*args, **kwargs): + try: + constants.set_testnet() + return func(*args, **kwargs) + finally: + constants.net = old_net return run_test - diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 574cd8bd1..08e881ecd 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -1,3 +1,4 @@ +import asyncio import base64 import sys @@ -35,27 +36,43 @@ def needs_test_with_all_aes_implementations(func): NOTE: this is inherently sequential; tests running in parallel would break things """ - def run_test(*args, **kwargs): - if FAST_TESTS: # if set, only run tests once, using fastest implementation - func(*args, **kwargs) - return - has_cryptodome = crypto.HAS_CRYPTODOME - has_cryptography = crypto.HAS_CRYPTOGRAPHY - has_pyaes = crypto.HAS_PYAES - try: - if has_pyaes: - (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, False, True - func(*args, **kwargs) # pyaes - if has_cryptodome: - (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = True, False, False - func(*args, **kwargs) # cryptodome - if has_cryptography: - (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, True, False - func(*args, **kwargs) # cryptography - finally: - crypto.HAS_CRYPTODOME = has_cryptodome - crypto.HAS_CRYPTOGRAPHY = has_cryptography - crypto.HAS_PYAES = has_pyaes + if FAST_TESTS: # if set, only run tests once, using fastest implementation + return func + has_cryptodome = crypto.HAS_CRYPTODOME + has_cryptography = crypto.HAS_CRYPTOGRAPHY + has_pyaes = crypto.HAS_PYAES + if asyncio.iscoroutinefunction(func): + async def run_test(*args, **kwargs): + try: + if has_pyaes: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, False, True + await func(*args, **kwargs) # pyaes + if has_cryptodome: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = True, False, False + await func(*args, **kwargs) # cryptodome + if has_cryptography: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, True, False + await func(*args, **kwargs) # cryptography + finally: + crypto.HAS_CRYPTODOME = has_cryptodome + crypto.HAS_CRYPTOGRAPHY = has_cryptography + crypto.HAS_PYAES = has_pyaes + else: + def run_test(*args, **kwargs): + try: + if has_pyaes: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, False, True + func(*args, **kwargs) # pyaes + if has_cryptodome: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = True, False, False + func(*args, **kwargs) # cryptodome + if has_cryptography: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY, crypto.HAS_PYAES) = False, True, False + func(*args, **kwargs) # cryptography + finally: + crypto.HAS_CRYPTODOME = has_cryptodome + crypto.HAS_CRYPTOGRAPHY = has_cryptography + crypto.HAS_PYAES = has_pyaes return run_test @@ -66,22 +83,34 @@ def needs_test_with_all_chacha20_implementations(func): NOTE: this is inherently sequential; tests running in parallel would break things """ - def run_test(*args, **kwargs): - if FAST_TESTS: # if set, only run tests once, using fastest implementation - func(*args, **kwargs) - return - has_cryptodome = crypto.HAS_CRYPTODOME - has_cryptography = crypto.HAS_CRYPTOGRAPHY - try: - if has_cryptodome: - (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False - func(*args, **kwargs) # cryptodome - if has_cryptography: - (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True - func(*args, **kwargs) # cryptography - finally: - crypto.HAS_CRYPTODOME = has_cryptodome - crypto.HAS_CRYPTOGRAPHY = has_cryptography + if FAST_TESTS: # if set, only run tests once, using fastest implementation + return func + has_cryptodome = crypto.HAS_CRYPTODOME + has_cryptography = crypto.HAS_CRYPTOGRAPHY + if asyncio.iscoroutinefunction(func): + async def run_test(*args, **kwargs): + try: + if has_cryptodome: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False + await func(*args, **kwargs) # cryptodome + if has_cryptography: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True + await func(*args, **kwargs) # cryptography + finally: + crypto.HAS_CRYPTODOME = has_cryptodome + crypto.HAS_CRYPTOGRAPHY = has_cryptography + else: + def run_test(*args, **kwargs): + try: + if has_cryptodome: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = True, False + func(*args, **kwargs) # cryptodome + if has_cryptography: + (crypto.HAS_CRYPTODOME, crypto.HAS_CRYPTOGRAPHY) = False, True + func(*args, **kwargs) # cryptography + finally: + crypto.HAS_CRYPTODOME = has_cryptodome + crypto.HAS_CRYPTOGRAPHY = has_cryptography return run_test @@ -93,13 +122,21 @@ def disable_ecdsa_r_value_grinding(func): NOTE: this is inherently sequential; tests running in parallel would break things """ - def run_test(*args, **kwargs): - is_grinding = ecc.ENABLE_ECDSA_R_VALUE_GRINDING - try: - ecc.ENABLE_ECDSA_R_VALUE_GRINDING = False - func(*args, **kwargs) - finally: - ecc.ENABLE_ECDSA_R_VALUE_GRINDING = is_grinding + is_grinding = ecc.ENABLE_ECDSA_R_VALUE_GRINDING + if asyncio.iscoroutinefunction(func): + async def run_test(*args, **kwargs): + try: + ecc.ENABLE_ECDSA_R_VALUE_GRINDING = False + return await func(*args, **kwargs) + finally: + ecc.ENABLE_ECDSA_R_VALUE_GRINDING = is_grinding + else: + def run_test(*args, **kwargs): + try: + ecc.ENABLE_ECDSA_R_VALUE_GRINDING = False + return func(*args, **kwargs) + finally: + ecc.ENABLE_ECDSA_R_VALUE_GRINDING = is_grinding return run_test diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index 845592c71..ff27a0db8 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -53,7 +53,7 @@ def test_eval_bool(self): self.assertTrue(eval_bool("true")) self.assertTrue(eval_bool("1")) - def test_convert_xkey(self): + async def test_convert_xkey(self): cmds = Commands(config=self.config) xpubs = { ("xpub6CCWFbvCbqF92kGwm9nV7t7RvVoQUKaq5USMdyVP6jvv1NgN52KAX6NNYCeE8Ca7JQC4K5tZcnQrubQcjJ6iixfPs4pwAQJAQgTt6hBjg11", "standard"), @@ -62,7 +62,7 @@ def test_convert_xkey(self): } for xkey1, xtype1 in xpubs: for xkey2, xtype2 in xpubs: - self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) + self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2)) xprvs = { ("xprv9yD9r6PJmTgqpGCUf8FUkkAhNTxv4rryiFWkqb5mYQPw8aMDXUzuyJ3tgv5vUqYkdK1E6Q5jKxPss4HkMBYV4q8AfG8t7rxgyS4xQX4ndAm", "standard"), @@ -71,40 +71,40 @@ def test_convert_xkey(self): } for xkey1, xtype1 in xprvs: for xkey2, xtype2 in xprvs: - self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) + self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_encrypt_decrypt(self, mock_save_db): + async def test_encrypt_decrypt(self, mock_save_db): wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN', path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] cmds = Commands(config=self.config) cleartext = "asdasd this is the message" pubkey = "021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da" - ciphertext = cmds._run('encrypt', (pubkey, cleartext)) - self.assertEqual(cleartext, cmds._run('decrypt', (pubkey, ciphertext), wallet=wallet)) + ciphertext = await cmds.encrypt(pubkey, cleartext) + self.assertEqual(cleartext, await cmds.decrypt(pubkey, ciphertext, wallet=wallet)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_export_private_key_imported(self, mock_save_db): + async def test_export_private_key_imported(self, mock_save_db): wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] cmds = Commands(config=self.config) # single address tests with self.assertRaises(Exception): - cmds._run('getprivatekeys', ("asdasd",), wallet=wallet) # invalid addr, though might raise "not in wallet" + await cmds.getprivatekeys("asdasd", wallet=wallet) # invalid addr, though might raise "not in wallet" with self.assertRaises(Exception): - cmds._run('getprivatekeys', ("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23",), wallet=wallet) # not in wallet + await cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23", wallet=wallet) # not in wallet self.assertEqual("p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL", - cmds._run('getprivatekeys', ("bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw",), wallet=wallet)) + await cmds.getprivatekeys("bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw", wallet=wallet)) # list of addresses tests with self.assertRaises(Exception): - cmds._run('getprivatekeys', (['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd'],), wallet=wallet) + await cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd'], wallet=wallet) self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], - cmds._run('getprivatekeys', (['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'],), wallet=wallet)) + await cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], wallet=wallet)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_export_private_key_deterministic(self, mock_save_db): + async def test_export_private_key_deterministic(self, mock_save_db): wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver', gap_limit=2, path='if_this_exists_mocking_failed_648151893', @@ -112,16 +112,16 @@ def test_export_private_key_deterministic(self, mock_save_db): cmds = Commands(config=self.config) # single address tests with self.assertRaises(Exception): - cmds._run('getprivatekeys', ("asdasd",), wallet=wallet) # invalid addr, though might raise "not in wallet" + await cmds.getprivatekeys("asdasd", wallet=wallet) # invalid addr, though might raise "not in wallet" with self.assertRaises(Exception): - cmds._run('getprivatekeys', ("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23",), wallet=wallet) # not in wallet + await cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23", wallet=wallet) # not in wallet self.assertEqual("p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2", - cmds._run('getprivatekeys', ("bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af",), wallet=wallet)) + await cmds.getprivatekeys("bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af", wallet=wallet)) # list of addresses tests with self.assertRaises(Exception): - cmds._run('getprivatekeys', (['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd'],), wallet=wallet) + await cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd'], wallet=wallet) self.assertEqual(['p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], - cmds._run('getprivatekeys', (['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'],), wallet=wallet)) + await cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], wallet=wallet)) class TestCommandsTestnet(ElectrumTestCase): @@ -131,7 +131,7 @@ def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - def test_convert_xkey(self): + async def test_convert_xkey(self): cmds = Commands(config=self.config) xpubs = { ("tpubD8p5qNfjczgTGbh9qgNxsbFgyhv8GgfVkmp3L88qtRm5ibUYiDVCrn6WYfnGey5XVVw6Bc5QNQUZW5B4jFQsHjmaenvkFUgWtKtgj5AdPm9", "standard"), @@ -140,7 +140,7 @@ def test_convert_xkey(self): } for xkey1, xtype1 in xpubs: for xkey2, xtype2 in xpubs: - self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) + self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2)) xprvs = { ("tprv8c83gxdVUcznP8fMx2iNUBbaQgQC7MUbBUDG3c6YU9xgt7Dn5pfcgHUeNZTAvuYmNgVHjyTzYzGWwJr7GvKCm2FkPaaJipyipbfJeB3tdPW", "standard"), @@ -149,9 +149,9 @@ def test_convert_xkey(self): } for xkey1, xtype1 in xprvs: for xkey2, xtype2 in xprvs: - self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) + self.assertEqual(xkey2, await cmds.convert_xkey(xkey1, xtype2)) - def test_serialize(self): + async def test_serialize(self): cmds = Commands(config=self.config) jsontx = { "inputs": [ @@ -170,9 +170,9 @@ def test_serialize(self): ] } self.assertEqual("0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000feffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb0247304402206367fb2ddd723985f5f51e0f2435084c0a66f5c26f4403a75d3dd417b71a20450220545dc3637bcb49beedbbdf5063e05cad63be91af4f839886451c30ecd6edf1d20121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000", - cmds._run('serialize', (jsontx,))) + await cmds.serialize(jsontx)) - def test_serialize_custom_nsequence(self): + async def test_serialize_custom_nsequence(self): cmds = Commands(config=self.config) jsontx = { "inputs": [ @@ -192,24 +192,24 @@ def test_serialize_custom_nsequence(self): ] } self.assertEqual("0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000fdffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb0247304402201c551df0458528d19ba1dd79b134dcf0055f7b029dfc3d0d024e6253d069d13e02206d03cfc85a6fc648acb6fc6be630e4567d1dd00ddbcdee551ee0711414e2f33f0121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000", - cmds._run('serialize', (jsontx,))) + await cmds.serialize(jsontx)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_getprivatekeyforpath(self, mock_save_db): + async def test_getprivatekeyforpath(self, mock_save_db): wallet = restore_wallet_from_text('north rent dawn bunker hamster invest wagon market romance pig either squeeze', gap_limit=2, path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] cmds = Commands(config=self.config) self.assertEqual("p2wpkh:cUzm7zPpWgLYeURgff4EsoMjhskCpsviBH4Y3aZcrBX8UJSRPjC2", - cmds._run('getprivatekeyforpath', ([0, 10000],), wallet=wallet)) + await cmds.getprivatekeyforpath([0, 10000], wallet=wallet)) self.assertEqual("p2wpkh:cUzm7zPpWgLYeURgff4EsoMjhskCpsviBH4Y3aZcrBX8UJSRPjC2", - cmds._run('getprivatekeyforpath', ("m/0/10000",), wallet=wallet)) + await cmds.getprivatekeyforpath("m/0/10000", wallet=wallet)) self.assertEqual("p2wpkh:cQAj4WGf1socCPCJNMjXYCJ8Bs5JUAk5pbDr4ris44QdgAXcV24S", - cmds._run('getprivatekeyforpath', ("m/5h/100000/88h/7",), wallet=wallet)) + await cmds.getprivatekeyforpath("m/5h/100000/88h/7", wallet=wallet)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_payto(self, mock_save_db): + async def test_payto(self, mock_save_db): wallet = restore_wallet_from_text('disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic', gap_limit=2, path='if_this_exists_mocking_failed_648151893', @@ -221,8 +221,7 @@ def test_payto(self, mock_save_db): wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) cmds = Commands(config=self.config) - tx_str = cmds._run( - 'payto', (), + tx_str = await cmds.payto( destination="tb1qsyzgpwa0vg2940u5t6l97etuvedr5dejpf9tdy", amount="0.00123456", feerate=50, @@ -237,7 +236,7 @@ def test_payto(self, mock_save_db): tx_str) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_paytomany_multiple_max_spends(self, mock_save_db): + async def test_paytomany_multiple_max_spends(self, mock_save_db): wallet = restore_wallet_from_text('kit virtual quantum festival fortune inform ladder saddle filter soldier start ghost', gap_limit=2, path='if_this_exists_mocking_failed_648151893', @@ -249,8 +248,7 @@ def test_paytomany_multiple_max_spends(self, mock_save_db): wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) cmds = Commands(config=self.config) - tx_str = cmds._run( - 'paytomany', (), + tx_str = await cmds.paytomany( outputs=[["tb1qk3g0t9pw5wctkzz7gh6k3ljfuukn729s67y54e", 0.002], ["tb1qr7evucrllljtryam6y2k3ntmlptq208pghql2h", "2!"], ["tb1qs3msqp0n0qade2haanjw2dkaa5lm77vwvce00h", 0.003], @@ -265,15 +263,15 @@ def test_paytomany_multiple_max_spends(self, mock_save_db): tx_str) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_signtransaction_without_wallet(self, mock_save_db): + async def test_signtransaction_without_wallet(self, mock_save_db): cmds = Commands(config=self.config) unsigned_tx = "70736274ff0100a0020000000221d3645ba44f33fff6fe2666dc080279bc34b531c66888729712a80b204a32a10100000000fdffffffdd7f90d51acf98dc45ad7489316a983868c75e16bf14ffeb9eae01603a7b4da40100000000fdffffff02e8030000000000001976a9149a9ec2b35a7660c80dae38dd806fdf9b0fde68fd88ac74c11000000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88aca79b1d00000100e102000000018ba8cf9f0ff0b44c389e4a1cd25c0770636d95ccef161e313542647d435a5fd0000000006a4730440220373b3989905177f2e36d7e3d02b967d03092747fe7bbd3ba7b2c24623a88538c02207be79ee1d981060c2be6783f4946ce1bda1f64671b349ef14a4a6fecc047a71e0121030de43c5ed4c6272d20ce3becf3fb7afd5c3ccfb5d58ddfdf3047981e0b005e0dfdffffff02c0010700000000001976a9141cd3eb65bce2cae9f54544b65e46b3ad1f0b187288ac40420f00000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88ac979b1d00000100e102000000014e39236158716e91b0b2170ebe9d6b359d139e9ebfff163f2bafd0bec9890d04000000006a473044022070340deb95ca25ef86c4c7a9539b5c8f7b8351941635450311f914cd9c2f45ea02203fa7576e032ab5ae4763c78f5c2124573213c956286fd766582d9462515dc6540121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffff02e8030000000000001976a91490350959750b3b38e451df16bd5957b7649bf5d288acac840100000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88ac979b1d00000000" privkey = "cVtE728tULSA4gut4QWxo218q6PRsXHQAv84SXix83cuvScvGd1H" self.assertEqual("020000000221d3645ba44f33fff6fe2666dc080279bc34b531c66888729712a80b204a32a1010000006a47304402205b30e188e30c846f98dacc714c16b7cd3a58a3fa24973d289683c9d32813e24c0220153855a29e96fb083084417ba3e3873ccaeb08435dad93773ab60716f94a36160121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffffdd7f90d51acf98dc45ad7489316a983868c75e16bf14ffeb9eae01603a7b4da4010000006a473044022010daa3dadf53bdcb071c6eff6b8787e3f675ed61feb4fef72d0bf9d99c0162f802200e73abd880b6f2ee5fe8c0abab731f1dddeb0f60df5e050a79c365bd718da1c80121033f6737e40a3a6087bc58bc5b82b427f9ed26d710b8fe2f70bfdd3d62abebcf74fdffffff02e8030000000000001976a9149a9ec2b35a7660c80dae38dd806fdf9b0fde68fd88ac74c11000000000001976a914f0dc093f7fb1b76cfd06610d5359d6595676cc2b88aca79b1d00", - cmds._run('signtransaction_with_privkey', (), tx=unsigned_tx, privkey=privkey)) + await cmds.signtransaction_with_privkey(tx=unsigned_tx, privkey=privkey)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_signtransaction_with_wallet(self, mock_save_db): + async def test_signtransaction_with_wallet(self, mock_save_db): wallet = restore_wallet_from_text('bitter grass shiver impose acquire brush forget axis eager alone wine silver', gap_limit=2, path='if_this_exists_mocking_failed_648151893', @@ -291,10 +289,10 @@ def test_signtransaction_with_wallet(self, mock_save_db): unsigned_tx = "cHNidP8BAHECAAAAAQOSwZQOLsnyNykZyjiHMn/luYuGYCLMebq1y+1aU9KtAAAAAAD+////AigjAAAAAAAAFgAUaQtZqBQGAvsjzCkE7OnMTa82EFIwGw8AAAAAABYAFKwOLSKSAL/7IWftb9GWrvnWh9i7AAAAAAABAN8BAAAAAUV22sziZMJNgYh2Qrcm9dZKp4JbIbNQx7daV/M32mhFAQAAAGtIMEUCIQCj+LYVXHGpitmYbt1hYbINJPrZm2RjwjtGOFbA7lSCbQIgD2BgF/2YdpbrvlIA2u3eki7uJkMloYTVu9qWW6UWCCEBIQLlxHPAUdrjEEPDNSZtDvicHaqy802IXMdwayZ/MmnGCf////8CQEIPAAAAAAAWABSKKL3bf2GGS9z1iyrRPVrrOrw8QqLduQ4AAAAAGXapFMOElQNCy2+N9VF1tIWGg4sDEw+tiKwAAAAAIgYDD67ptKJbfbggI8qYkZJxLN1MtT09kzhZHHkJ5YGuHAwQsuNafQAAAIAAAAAAAAAAAAAiAgKFhOeJ459BORsvJ4UsoYq+wGpUEcIb41D+1h7scSDeUxCy41p9AAAAgAEAAAAAAAAAAAA=" self.assertEqual("020000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff022823000000000000160014690b59a8140602fb23cc2904ece9cc4daf361052301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb02473044022027e1e37172e52b2d84106663cff5bcf6e447dcb41f6483f99584cfb4de2785f4022005c72f6324ad130c78fca43fe5fc565526d1723f2c9dc3efea78f66d7ae9d4360121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000", - cmds._run('signtransaction', (), tx=unsigned_tx, wallet=wallet)) + await cmds.signtransaction(tx=unsigned_tx, wallet=wallet)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bumpfee(self, mock_save_db): + async def test_bumpfee(self, mock_save_db): wallet = restore_wallet_from_text('right nominee cheese afford exotic pilot mask illness rug fringe degree pottery', gap_limit=2, path='if_this_exists_mocking_failed_648151893', @@ -307,7 +305,7 @@ def test_bumpfee(self, mock_save_db): cmds = Commands(config=self.config) tx = "02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0fe8b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa02473044022063c05e2347f16251922830ccc757231247b3c2970c225f988e9204844a1ab7b802204652d2c4816707e3d3bea2609b83b079001a435bad2a99cc2e730f276d07070c012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee050721f00" self.assertEqual("02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0f84b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa0247304402203aa63539b673a3bd70a76482b17f35f8843974fab28f84143a00450789010bc40220779c2ce2d0217f973f1f6c9f718e19fc7ebd14dd8821a962f002437cda3082ec012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee000000000", - cmds._run('bumpfee', (), tx=tx, new_fee_rate='1.6', wallet=wallet)) + await cmds.bumpfee(tx=tx, new_fee_rate='1.6', wallet=wallet)) self.assertEqual("02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0f84b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa0247304402203aa63539b673a3bd70a76482b17f35f8843974fab28f84143a00450789010bc40220779c2ce2d0217f973f1f6c9f718e19fc7ebd14dd8821a962f002437cda3082ec012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee000000000", - cmds._run('bumpfee', (), tx=tx, new_fee_rate='1.6', from_coins="9a8cba08b79d73d8b602e8748a2c8a09cdd200a0393561f68e05af69fc3d72b9:1", wallet=wallet)) + await cmds.bumpfee(tx=tx, new_fee_rate='1.6', from_coins="9a8cba08b79d73d8b602e8748a2c8a09cdd200a0393561f68e05af69fc3d72b9:1", wallet=wallet)) diff --git a/electrum/tests/test_invoices.py b/electrum/tests/test_invoices.py index 6048141fb..3bdf9d847 100644 --- a/electrum/tests/test_invoices.py +++ b/electrum/tests/test_invoices.py @@ -37,7 +37,7 @@ def create_wallet2(self) -> Standard_Wallet: wallet2.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) return wallet2 - def test_wallet_with_ln_creates_payreq_and_gets_paid_on_ln(self): + async def test_wallet_with_ln_creates_payreq_and_gets_paid_on_ln(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=2, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet @@ -54,7 +54,7 @@ def test_wallet_with_ln_creates_payreq_and_gets_paid_on_ln(self): wallet1.lnworker.set_request_status(bytes.fromhex(pr.rhash), PR_PAID) self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr)) - def test_wallet_with_ln_creates_payreq_and_gets_paid_onchain(self): + async def test_wallet_with_ln_creates_payreq_and_gets_paid_onchain(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=2, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet @@ -84,7 +84,7 @@ def test_wallet_with_ln_creates_payreq_and_gets_paid_onchain(self): wallet1.adb.add_verified_tx(tx.txid(), tx_info) self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr)) - def test_wallet_without_ln_creates_payreq_and_gets_paid_onchain(self): + async def test_wallet_without_ln_creates_payreq_and_gets_paid_onchain(self): text = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=2, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet @@ -114,7 +114,7 @@ def test_wallet_without_ln_creates_payreq_and_gets_paid_onchain(self): wallet1.adb.add_verified_tx(tx.txid(), tx_info) self.assertEqual(PR_PAID, wallet1.get_invoice_status(pr)) - def test_wallet_gets_paid_onchain_in_the_past(self): + async def test_wallet_gets_paid_onchain_in_the_past(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=2, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet @@ -143,7 +143,7 @@ def test_wallet_gets_paid_onchain_in_the_past(self): wallet1.adb.add_verified_tx(tx.txid(), tx_info) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr)) - def test_wallet_reuse_unused_fallback_onchain_addr_when_getting_paid_with_lightning(self): + async def test_wallet_reuse_unused_fallback_onchain_addr_when_getting_paid_with_lightning(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=5, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet @@ -198,7 +198,7 @@ def test_wallet_reuse_unused_fallback_onchain_addr_when_getting_paid_with_lightn self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr4)) self.assertEqual(addr4, pr4.get_address()) - def test_wallet_reuse_addr_of_expired_request(self): + async def test_wallet_reuse_addr_of_expired_request(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=3, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet @@ -226,7 +226,7 @@ def test_wallet_reuse_addr_of_expired_request(self): self.assertEqual(addr2, pr2.get_address()) self.assertFalse(pr2.has_expired()) - def test_wallet_get_request_by_addr(self): + async def test_wallet_get_request_by_addr(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text(text, path=self.wallet1_path, gap_limit=3, config=self.config) wallet1 = d['wallet'] # type: Standard_Wallet diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 7342d3d1b..187a5139c 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -404,17 +404,16 @@ def setUp(self): super().setUp() self._lnworkers_created = [] # type: List[MockLNWallet] - def tearDown(self): - async def cleanup_lnworkers(): - async with OldTaskGroup() as group: - for lnworker in self._lnworkers_created: - await group.spawn(lnworker.stop()) + async def asyncTearDown(self): + # clean up lnworkers + async with OldTaskGroup() as group: for lnworker in self._lnworkers_created: - shutil.rmtree(lnworker._user_dir) - self._lnworkers_created.clear() - run(cleanup_lnworkers()) + await group.spawn(lnworker.stop()) + for lnworker in self._lnworkers_created: + shutil.rmtree(lnworker._user_dir) + self._lnworkers_created.clear() electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {} - super().tearDown() + await super().asyncTearDown() def prepare_peers( self, alice_channel: Channel, bob_channel: Channel, @@ -548,7 +547,7 @@ def prepare_invoice( lnaddr2 = lndecode(invoice) # unlike lnaddr1, this now has a pubkey set return lnaddr2, invoice - def test_reestablish(self): + async def test_reestablish(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) for chan in (alice_channel, bob_channel): @@ -561,13 +560,11 @@ async def reestablish(): self.assertEqual(bob_channel.peer_state, PeerState.GOOD) gath.cancel() gath = asyncio.gather(reestablish(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p1.htlc_switch()) - async def f(): + with self.assertRaises(asyncio.CancelledError): await gath - with self.assertRaises(concurrent.futures.CancelledError): - run(f()) @needs_test_with_all_chacha20_implementations - def test_reestablish_with_old_state(self): + async def test_reestablish_with_old_state(self): random_seed = os.urandom(32) alice_channel, bob_channel = create_test_channels(random_seed=random_seed) alice_channel_0, bob_channel_0 = create_test_channels(random_seed=random_seed) # these are identical @@ -578,10 +575,8 @@ async def pay(): self.assertEqual(result, True) gath.cancel() gath = asyncio.gather(pay(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) - async def f(): + with self.assertRaises(asyncio.CancelledError): await gath - with self.assertRaises(concurrent.futures.CancelledError): - run(f()) p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel_0, bob_channel) for chan in (alice_channel_0, bob_channel): chan.peer_state = PeerState.DISCONNECTED @@ -590,10 +585,8 @@ async def reestablish(): p1.reestablish_channel(alice_channel_0), p2.reestablish_channel(bob_channel)) gath = asyncio.gather(reestablish(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) - async def f(): + with self.assertRaises(lnutil.RemoteMisbehaving): await gath - with self.assertRaises(electrum.lnutil.RemoteMisbehaving): - run(f()) self.assertEqual(alice_channel_0.peer_state, PeerState.BAD) self.assertEqual(bob_channel._state, ChannelState.FORCE_CLOSING) @@ -612,7 +605,7 @@ def _send_fake_htlc(peer: Peer, chan: Channel) -> UpdateAddHtlc: ) return htlc - def test_reestablish_replay_messages_rev_then_sig(self): + async def test_reestablish_replay_messages_rev_then_sig(self): """ See https://github.com/lightning/bolts/pull/810#issue-728299277 @@ -664,9 +657,9 @@ async def f(): self.assertEqual(chan_BA.peer_state, PeerState.GOOD) raise SuccessfulTest() with self.assertRaises(SuccessfulTest): - run(f()) + await f() - def test_reestablish_replay_messages_sig_then_rev(self): + async def test_reestablish_replay_messages_sig_then_rev(self): """ See https://github.com/lightning/bolts/pull/810#issue-728299277 @@ -719,9 +712,9 @@ async def f(): self.assertEqual(chan_BA.peer_state, PeerState.GOOD) raise SuccessfulTest() with self.assertRaises(SuccessfulTest): - run(f()) + await f() - def _test_simple_payment(self, trampoline: bool): + async def _test_simple_payment(self, trampoline: bool): """Alice pays Bob a single HTLC via direct channel.""" alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -754,18 +747,18 @@ async def f(): 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey), } with self.assertRaises(PaymentDone): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_simple_payment(self): - self._test_simple_payment(trampoline=False) + async def test_simple_payment(self): + await self._test_simple_payment(trampoline=False) @needs_test_with_all_chacha20_implementations - def test_simple_payment_trampoline(self): - self._test_simple_payment(trampoline=True) + async def test_simple_payment_trampoline(self): + await self._test_simple_payment(trampoline=True) @needs_test_with_all_chacha20_implementations - def test_payment_race(self): + async def test_payment_race(self): """Alice and Bob pay each other simultaneously. They both send 'update_add_htlc' and receive each other's update before sending 'commitment_signed'. Neither party should fulfill @@ -837,11 +830,11 @@ async def f(): await asyncio.sleep(0.01) await group.spawn(pay()) with self.assertRaises(PaymentDone): - run(f()) + await f() #@unittest.skip("too expensive") #@needs_test_with_all_chacha20_implementations - def test_payments_stresstest(self): + async def test_payments_stresstest(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL) @@ -859,17 +852,15 @@ async def many_payments(): await group.spawn(single_payment(pay_req)) gath.cancel() gath = asyncio.gather(many_payments(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) - async def f(): + with self.assertRaises(asyncio.CancelledError): await gath - with self.assertRaises(concurrent.futures.CancelledError): - run(f()) self.assertEqual(alice_init_balance_msat - num_payments * payment_value_msat, alice_channel.balance(HTLCOwner.LOCAL)) self.assertEqual(alice_init_balance_msat - num_payments * payment_value_msat, bob_channel.balance(HTLCOwner.REMOTE)) self.assertEqual(bob_init_balance_msat + num_payments * payment_value_msat, bob_channel.balance(HTLCOwner.LOCAL)) self.assertEqual(bob_init_balance_msat + num_payments * payment_value_msat, alice_channel.balance(HTLCOwner.REMOTE)) @needs_test_with_all_chacha20_implementations - def test_payment_multihop(self): + async def test_payment_multihop(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) peers = graph.peers.values() async def pay(lnaddr, pay_req): @@ -888,10 +879,10 @@ async def f(): lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True) await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_payment_multihop_with_preselected_path(self): + async def test_payment_multihop_with_preselected_path(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) peers = graph.peers.values() async def pay(pay_req): @@ -933,10 +924,10 @@ async def f(): lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True) await group.spawn(pay(pay_req)) with self.assertRaises(PaymentDone): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_payment_multihop_temp_node_failure(self): + async def test_payment_multihop_temp_node_failure(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) graph.workers['bob'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) graph.workers['carol'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) @@ -958,10 +949,10 @@ async def f(): lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True) await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_payment_multihop_route_around_failure(self): + async def test_payment_multihop_route_around_failure(self): # Alice will pay Dave. Alice first tries A->C->D route, due to lower fees, but Carol # will fail the htlc and get blacklisted. Alice will then try A->B->D and succeed. graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) @@ -996,10 +987,10 @@ async def f(): self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_payment_with_temp_channel_failure_and_liquidity_hints(self): + async def test_payment_with_temp_channel_failure_and_liquidity_hints(self): # prepare channels such that a temporary channel failure happens at c->d graph_definition = GRAPH_DEFINITIONS['square_graph'].copy() graph_definition['alice']['channels']['carol']['local_balance_msat'] = 200_000_000 @@ -1058,9 +1049,9 @@ async def f(): lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], amount_msat=amount_to_pay, include_routing_hints=True) await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): - run(f()) + await f() - def _run_mpp(self, graph, fail_kwargs, success_kwargs): + async def _run_mpp(self, graph, fail_kwargs, success_kwargs): """Tests a multipart payment scenario for failing and successful cases.""" self.assertEqual(500_000_000_000, graph.channels[('alice', 'bob')].balance(LOCAL)) self.assertEqual(500_000_000_000, graph.channels[('alice', 'carol')].balance(LOCAL)) @@ -1110,22 +1101,23 @@ async def f(kwargs): if fail_kwargs: with self.assertRaises(NoPathFound): - run(f(fail_kwargs)) + await f(fail_kwargs) if success_kwargs: with self.assertRaises(PaymentDone): - run(f(success_kwargs)) + await f(success_kwargs) @needs_test_with_all_chacha20_implementations - def test_payment_multipart_with_timeout(self): + async def test_payment_multipart_with_timeout(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) - self._run_mpp(graph, {'bob_forwarding': False}, {'bob_forwarding': True}) + await self._run_mpp(graph, {'bob_forwarding': False}, {'bob_forwarding': True}) @needs_test_with_all_chacha20_implementations - def test_payment_multipart(self): + async def test_payment_multipart(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) - self._run_mpp(graph, {'mpp_invoice': False}, {'mpp_invoice': True}) + await self._run_mpp(graph, {'mpp_invoice': False}, {'mpp_invoice': True}) - def _run_trampoline_payment(self, is_legacy, direct, drop_dave= []): + async def _run_trampoline_payment(self, is_legacy, direct, drop_dave=None): + if drop_dave is None: drop_dave = [] async def turn_on_trampoline_alice(): if graph.workers['alice'].network.channel_db: graph.workers['alice'].network.channel_db.stop() @@ -1178,29 +1170,29 @@ async def f(): graph.workers['carol'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey), } - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_payment_trampoline_legacy(self): + async def test_payment_trampoline_legacy(self): with self.assertRaises(PaymentDone): - self._run_trampoline_payment(is_legacy=True, direct=False) + await self._run_trampoline_payment(is_legacy=True, direct=False) @needs_test_with_all_chacha20_implementations - def test_payment_trampoline_e2e_direct(self): + async def test_payment_trampoline_e2e_direct(self): with self.assertRaises(PaymentDone): - self._run_trampoline_payment(is_legacy=False, direct=True) + await self._run_trampoline_payment(is_legacy=False, direct=True) @needs_test_with_all_chacha20_implementations - def test_payment_trampoline_e2e_indirect(self): + async def test_payment_trampoline_e2e_indirect(self): # must use two trampolines with self.assertRaises(PaymentDone): - self._run_trampoline_payment(is_legacy=False, direct=False, drop_dave=['bob']) + await self._run_trampoline_payment(is_legacy=False, direct=False, drop_dave=['bob']) # both trampolines drop dave with self.assertRaises(NoPathFound): - self._run_trampoline_payment(is_legacy=False, direct=False, drop_dave=['bob', 'carol']) + await self._run_trampoline_payment(is_legacy=False, direct=False, drop_dave=['bob', 'carol']) @needs_test_with_all_chacha20_implementations - def test_payment_multipart_trampoline_e2e(self): + async def test_payment_multipart_trampoline_e2e(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey), @@ -1210,26 +1202,26 @@ def test_payment_multipart_trampoline_e2e(self): # * a payment with one trial: fails, because # we need at least one trial because the initial fees are too low # * a payment with several trials: should succeed - self._run_mpp( + await self._run_mpp( graph, fail_kwargs={'alice_uses_trampoline': True, 'attempts': 1}, success_kwargs={'alice_uses_trampoline': True, 'attempts': 30}) @needs_test_with_all_chacha20_implementations - def test_payment_multipart_trampoline_legacy(self): + async def test_payment_multipart_trampoline_legacy(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey), graph.workers['carol'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey), } # trampoline-to-legacy: this is restricted, as there are no forwarders capable of doing this - self._run_mpp( + await self._run_mpp( graph, fail_kwargs={'alice_uses_trampoline': True, 'attempts': 30, 'disable_trampoline_receiving': True}, success_kwargs={}) @needs_test_with_all_chacha20_implementations - def test_fail_pending_htlcs_on_shutdown(self): + async def test_fail_pending_htlcs_on_shutdown(self): """Alice tries to pay Dave via MPP. Dave receives some HTLCs but not all. Dave shuts down (stops wallet). We test if Dave fails the pending HTLCs during shutdown. @@ -1270,19 +1262,19 @@ async def f(): await group.spawn(stop()) with self.assertRaises(SuccessfulTest): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_legacy_shutdown_low(self): - self._test_shutdown(alice_fee=100, bob_fee=150) + async def test_legacy_shutdown_low(self): + await self._test_shutdown(alice_fee=100, bob_fee=150) @needs_test_with_all_chacha20_implementations - def test_legacy_shutdown_high(self): - self._test_shutdown(alice_fee=2000, bob_fee=100) + async def test_legacy_shutdown_high(self): + await self._test_shutdown(alice_fee=2000, bob_fee=100) @needs_test_with_all_chacha20_implementations - def test_modern_shutdown_with_overlap(self): - self._test_shutdown( + async def test_modern_shutdown_with_overlap(self): + await self._test_shutdown( alice_fee=1, bob_fee=200, alice_fee_range={'min_fee_satoshis': 1, 'max_fee_satoshis': 10}, @@ -1300,7 +1292,7 @@ def test_modern_shutdown_with_overlap(self): # bob_fee_range={'min_fee_satoshis': 50, 'max_fee_satoshis': 300}) # )) - def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee_range=None): + async def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee_range=None): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) w1.network.config.set_key('test_shutdown_fee', alice_fee) @@ -1334,13 +1326,11 @@ async def set_settle(): await asyncio.sleep(0.1) w2.enable_htlc_settle = True gath = asyncio.gather(pay(), set_settle(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) - async def f(): + with self.assertRaises(asyncio.CancelledError): await gath - with self.assertRaises(concurrent.futures.CancelledError): - run(f()) @needs_test_with_all_chacha20_implementations - def test_warning(self): + async def test_warning(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -1349,13 +1339,11 @@ async def action(): await asyncio.wait_for(p2.initialized, 1) await p1.send_warning(alice_channel.channel_id, 'be warned!', close_connection=True) gath = asyncio.gather(action(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) - async def f(): - await gath with self.assertRaises(GracefulDisconnect): - run(f()) + await gath @needs_test_with_all_chacha20_implementations - def test_error(self): + async def test_error(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -1366,13 +1354,11 @@ async def action(): assert alice_channel.is_closed() gath.cancel() gath = asyncio.gather(action(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) - async def f(): - await gath with self.assertRaises(GracefulDisconnect): - run(f()) + await gath @needs_test_with_all_chacha20_implementations - def test_close_upfront_shutdown_script(self): + async def test_close_upfront_shutdown_script(self): alice_channel, bob_channel = create_test_channels() # create upfront shutdown script for bob, alice doesn't use upfront @@ -1411,7 +1397,7 @@ async def main_loop(peer): await gath with self.assertRaises(GracefulDisconnect): - run(test()) + await test() # bob sends the same upfront_shutdown_script has he announced alice_channel.config[HTLCOwner.REMOTE].upfront_shutdown_script = bob_uss @@ -1438,24 +1424,25 @@ async def main_loop(peer): coros = [close(), main_loop(p1), main_loop(p2)] gath = asyncio.gather(*coros) await gath - with self.assertRaises(concurrent.futures.CancelledError): - run(test()) - def test_channel_usage_after_closing(self): + with self.assertRaises(asyncio.CancelledError): + await test() + + async def test_channel_usage_after_closing(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel) lnaddr, pay_req = self.prepare_invoice(w2) lnaddr = w1._check_invoice(pay_req) - route, amount_msat = run(w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0:2] + route, amount_msat = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0:2] assert amount_msat == lnaddr.get_amount_msat() - run(w1.force_close_channel(alice_channel.channel_id)) + await w1.force_close_channel(alice_channel.channel_id) # check if a tx (commitment transaction) was broadcasted: assert q1.qsize() == 1 with self.assertRaises(NoPathFound) as e: - run(w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr)) + await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr) peer = w1.peers[route[0].node_id] # AssertionError is ok since we shouldn't use old routes, and the @@ -1477,10 +1464,10 @@ async def f(): ) await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) with self.assertRaises(PaymentFailure): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_sending_weird_messages_that_should_be_ignored(self): + async def test_sending_weird_messages_that_should_be_ignored(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -1509,10 +1496,10 @@ async def f(): await group.spawn(send_weird_messages()) with self.assertRaises(SuccessfulTest): - run(f()) + await f() @needs_test_with_all_chacha20_implementations - def test_sending_weird_messages__unknown_even_type(self): + async def test_sending_weird_messages__unknown_even_type(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -1538,11 +1525,11 @@ async def f(): await group.spawn(send_weird_messages()) with self.assertRaises(GracefulDisconnect): - run(f()) + await f() self.assertTrue(isinstance(failing_task.exception().__cause__, lnmsg.UnknownMandatoryMsgType)) @needs_test_with_all_chacha20_implementations - def test_sending_weird_messages__known_msg_with_insufficient_length(self): + async def test_sending_weird_messages__known_msg_with_insufficient_length(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -1568,9 +1555,5 @@ async def f(): await group.spawn(send_weird_messages()) with self.assertRaises(GracefulDisconnect): - run(f()) + await f() self.assertTrue(isinstance(failing_task.exception().__cause__, lnmsg.UnexpectedEndOfStream)) - - -def run(coro): - return asyncio.run_coroutine_threadsafe(coro, loop=util.get_asyncio_loop()).result() diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py index be66ab5cf..ff3aaefbd 100644 --- a/electrum/tests/test_lnrouter.py +++ b/electrum/tests/test_lnrouter.py @@ -3,6 +3,7 @@ import tempfile import shutil import asyncio +from typing import Optional from electrum import util from electrum.util import bfh @@ -30,18 +31,19 @@ def node(character: str) -> bytes: class Test_LNRouter(ElectrumTestCase): TESTNET = True - cdb = None + cdb = None # type: Optional[lnrouter.ChannelDB] def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) + self.assertIsNone(self.cdb) # sanity-check side effects from previous tests - def tearDown(self): + async def asyncTearDown(self): # if the test called prepare_graph(), channeldb needs to be cleaned up if self.cdb: self.cdb.stop() - asyncio.run_coroutine_threadsafe(self.cdb.stopped_event.wait(), self.asyncio_loop).result() - super().tearDown() + await self.cdb.stopped_event.wait() + await super().asyncTearDown() def prepare_graph(self): """ @@ -139,7 +141,7 @@ def add_chan_upd(payload): add_chan_upd({'short_channel_id': channel(7), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) add_chan_upd({'short_channel_id': channel(7), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0}) - def test_find_path_for_payment(self): + async def test_find_path_for_payment(self): self.prepare_graph() amount_to_send = 100000 @@ -156,7 +158,7 @@ def test_find_path_for_payment(self): self.assertEqual(node('b'), route[0].node_id) self.assertEqual(channel(3), route[0].short_channel_id) - def test_find_path_liquidity_hints(self): + async def test_find_path_liquidity_hints(self): self.prepare_graph() amount_to_send = 100000 @@ -213,7 +215,7 @@ def test_find_path_liquidity_hints(self): self.assertEqual(channel(4), path[1].short_channel_id) self.assertEqual(channel(7), path[2].short_channel_id) - def test_find_path_liquidity_hints_inflight_htlcs(self): + async def test_find_path_liquidity_hints_inflight_htlcs(self): self.prepare_graph() amount_to_send = 100000 diff --git a/electrum/tests/test_lntransport.py b/electrum/tests/test_lntransport.py index 13986b124..327c215fd 100644 --- a/electrum/tests/test_lntransport.py +++ b/electrum/tests/test_lntransport.py @@ -13,7 +13,7 @@ class TestLNTransport(ElectrumTestCase): @needs_test_with_all_chacha20_implementations - def test_responder(self): + async def test_responder(self): # local static ls_priv=bytes.fromhex('2121212121212121212121212121212121212121212121212121212121212121') # ephemeral @@ -39,11 +39,10 @@ async def read(self, num_bytes): assert num_bytes == 66 return bytes.fromhex('00b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139ba') transport = LNResponderTransport(ls_priv, Reader(), Writer()) - asyncio.run_coroutine_threadsafe( - transport.handshake(epriv=e_priv), self.asyncio_loop).result() + await transport.handshake(epriv=e_priv) @needs_test_with_all_chacha20_implementations - def test_loop(self): + async def test_loop(self): responder_shaked = asyncio.Event() server_shaked = asyncio.Event() responder_key = ECPrivkey.generate_random_key() @@ -98,4 +97,4 @@ async def f(): server.close() await server.wait_closed() - asyncio.run_coroutine_threadsafe(f(), self.asyncio_loop).result() + await f() diff --git a/electrum/tests/test_network.py b/electrum/tests/test_network.py index 9d92091d2..d9e3dcb97 100644 --- a/electrum/tests/test_network.py +++ b/electrum/tests/test_network.py @@ -52,12 +52,12 @@ def tearDownClass(cls): super().tearDownClass() constants.set_mainnet() - def setUp(self): - super().setUp() + async def asyncSetUp(self): + await super().asyncSetUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.interface = MockInterface(self.config) - def test_fork_noconflict(self): + async def test_fork_noconflict(self): blockchain.blockchains = {} self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}}) def mock_connect(height): @@ -68,11 +68,11 @@ def mock_connect(height): self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) ifa = self.interface - fut = asyncio.run_coroutine_threadsafe(ifa.sync_until(8, next_height=7), util.get_asyncio_loop()) - self.assertEqual(('fork', 8), fut.result()) + res = await ifa.sync_until(8, next_height=7) + self.assertEqual(('fork', 8), res) self.assertEqual(self.interface.q.qsize(), 0) - def test_fork_conflict(self): + async def test_fork_conflict(self): blockchain.blockchains = {7: {'check': lambda bad_header: False}} self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}}) def mock_connect(height): @@ -83,11 +83,11 @@ def mock_connect(height): self.interface.q.put_nowait({'block_height': 5, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'binary':1,'check':lambda x: True, 'connect': lambda x: True}}) ifa = self.interface - fut = asyncio.run_coroutine_threadsafe(ifa.sync_until(8, next_height=7), util.get_asyncio_loop()) - self.assertEqual(('fork', 8), fut.result()) + res = await ifa.sync_until(8, next_height=7) + self.assertEqual(('fork', 8), res) self.assertEqual(self.interface.q.qsize(), 0) - def test_can_connect_during_backward(self): + async def test_can_connect_during_backward(self): blockchain.blockchains = {} self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}}) def mock_connect(height): @@ -97,8 +97,8 @@ def mock_connect(height): self.interface.q.put_nowait({'block_height': 3, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 4, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}}) ifa = self.interface - fut = asyncio.run_coroutine_threadsafe(ifa.sync_until(8, next_height=4), util.get_asyncio_loop()) - self.assertEqual(('catchup', 5), fut.result()) + res = await ifa.sync_until(8, next_height=4) + self.assertEqual(('catchup', 5), res) self.assertEqual(self.interface.q.qsize(), 0) def mock_fork(self, bad_header): @@ -107,7 +107,7 @@ def mock_fork(self, bad_header): forkpoint_hash=sha256(str(forkpoint)).hex(), prev_hash=sha256(str(forkpoint-1)).hex()) return b - def test_chain_false_during_binary(self): + async def test_chain_false_during_binary(self): blockchain.blockchains = {} self.interface.q.put_nowait({'block_height': 8, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: False}}) mock_connect = lambda height: height == 3 @@ -118,8 +118,8 @@ def test_chain_false_during_binary(self): self.interface.q.put_nowait({'block_height': 5, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}}) self.interface.q.put_nowait({'block_height': 6, 'mock': {'catchup':1, 'check': lambda x: False, 'connect': lambda x: True}}) ifa = self.interface - fut = asyncio.run_coroutine_threadsafe(ifa.sync_until(8, next_height=6), util.get_asyncio_loop()) - self.assertEqual(('catchup', 7), fut.result()) + res = await ifa.sync_until(8, next_height=6) + self.assertEqual(('catchup', 7), res) self.assertEqual(self.interface.q.qsize(), 0) diff --git a/electrum/tests/test_storage_upgrade.py b/electrum/tests/test_storage_upgrade.py index c2dfd4712..bbac5089e 100644 --- a/electrum/tests/test_storage_upgrade.py +++ b/electrum/tests/test_storage_upgrade.py @@ -21,194 +21,194 @@ class TestStorageUpgrade(WalletTestCase): ########## - def test_upgrade_from_client_1_9_8_seeded(self): + async def test_upgrade_from_client_1_9_8_seeded(self): wallet_str = "{'addr_history':{'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf']}},'seed_version':4}" - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) # TODO pre-2.0 mixed wallets are not split currently - #def test_upgrade_from_client_1_9_8_mixed(self): + #async def test_upgrade_from_client_1_9_8_mixed(self): # wallet_str = "{'addr_history':{'15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc':[],'177hEYTccmuYH8u68pYfaLteTxwJrVgvJj':[],'1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC':[],'1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm':[],'1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj':[],'1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf':[],'1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs':[],'1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa':[]},'accounts_expanded':{},'master_public_key':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb','use_encryption':False,'seed':'2605aafe50a45bdf2eb155302437e678','accounts':{0:{0:['1DjtUCcQwwzA3GSPA7Kd79PMnri7tLDPYC','1PAgpPxnL42Hp3cWxmSfdChPqqGiM8g7zj','177hEYTccmuYH8u68pYfaLteTxwJrVgvJj','1PGEgaPG1XJqmuSj68GouotWeYkCtwo4wm','15V7MsQK2vjF5aEXLVG11qi2eZPZsXdnYc'],1:['1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs','1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa','1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf'],'mpk':'756d1fe6ded28d43d4fea902a9695feb785447514d6e6c3bdf369f7c3432fdde4409e4efbffbcf10084d57c5a98d1f34d20ac1f133bdb64fa02abf4f7bde1dfb'}},'imported_keys':{'15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA':'5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq','1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6':'L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U','1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr':'L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM'},'seed_version':4}" - # self._upgrade_storage(wallet_str, accounts=2) + # await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_0_4_seeded(self): + async def test_upgrade_from_client_2_0_4_seeded(self): wallet_str = '{"accounts":{"0":{"change":["03d8e267e8de7769b52a8727585b3c44b4e148b86b2c90e3393f78a75bd6aab83f","03f09b3562bec870b4eb8626c20d449ee85ef17ea896a6a82b454e092eef91b296","02df953880df9284715e8199254edcf3708c635adc92a90dbf97fbd64d1eb88a36"],"receiving":["02cd4d73d5e335dafbf5c9338f88ceea3d7511ab0f9b8910745ac940ff40913a30","0243ed44278a178101e0fb14d36b68e6e13d00fe3434edb56e4504ea6f5db2e467","0367c0aa3681ec3635078f79f8c78aa339f19e38d9e1c9e2853e30e66ade02cac3","0237d0fe142cff9d254a3bdd3254f0d5f72676b0099ba799764a993a0d0ba80111","020a899fd417527b3929c8f625c93b45392244bab69ff91b582ed131977d5cd91e","039e84264920c716909b88700ef380336612f48237b70179d0b523784de28101f7","03125452df109a51be51fe21e71c3a4b0bba900c9c0b8d29b4ee2927b51f570848","0291fa554217090bab96eeff63e1c6fdec37358ed597d18fa32c60c02a48878c8c","030b6354a4365bab55e86269fb76241fd69716f02090ead389e1fce13d474aa569","023dcba431d8887ab63595f0df1e978e4a5f1c3aac6670e43d03956448a229f740","0332a61cbe04fe027033369ce7569b860c24462878bdd8c0332c22a3f5fdcc1790","021249480422d93dba2aafcd4575e6f630c4e3a2a832dd8a15f884e1052b6836e4","02516e91dede15d3a15dd648591bb92e107b3a53d5bc34b286ab389ce1af3130aa","02e1da3dddd81fa6e4895816da9d4b8ab076d6ea8034b1175169c0f247f002f4cf","0390ef1e3fdbe137767f8b5abad0088b105eee8c39e075305545d405be3154757a","03fca30eb33c6e1ffa071d204ccae3060680856ae9b93f31f13dd11455e67ee85d","034f6efdbbe1bfa06b32db97f16ff3a0dd6cf92769e8d9795c465ff76d2fbcb794","021e2901009954f23d2bf3429d4a531c8ca3f68e9598687ef816f20da08ff53848","02d3ccf598939ff7919ee23d828d229f85e3e58842582bf054491c59c8b974aa6e","03a1daffa39f42c1aaae24b859773a170905c6ee8a6dab8c1bfbfc93f09b88f4db"],"xpub":"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K3PnX8QbR9EmUZQ7jRzLxm9pKf9k9nNbym2NFcQhDAjonwZ39jtWLYp6qk5UHotj13p2y7w1ZhhvvyV5eCcaPUrKofs9CXQ9"},"master_public_keys":{"x/":"xpub661MyMwAqRbcFsrzES8RWNiD7RxDqT4p8NjvTY9mLi8xdphQ9x1TiY8GnqCpQx4LqJBdcGeXrsAa2b2G7ZcjJcest9wHcqYfTqXmQja6vfV"},"seed":"seven direct thunder glare prevent please fatal blush buzz artefact gate vendor above","seed_version":11,"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_0_4_importedkeys(self): + async def test_upgrade_from_client_2_0_4_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_0_4_watchaddresses(self): + async def test_upgrade_from_client_2_0_4_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_0_4_trezor_singleacc(self): + async def test_upgrade_from_client_2_0_4_trezor_singleacc(self): wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_0_4_trezor_multiacc(self): + async def test_upgrade_from_client_2_0_4_trezor_multiacc(self): wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]]},"labels":{"0":"Main account","1":"acc1"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) + await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_0_4_multisig(self): + async def test_upgrade_from_client_2_0_4_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["03c3a8549f35d7842192e7e00afa25ef1c779d05f1c891ba7c30de968fb29e3e78","02e191e105bccf1b4562d216684632b9ec22c87e1457b537eb27516afa75c56831"],["03793397f02b3bd3d0f6f0dafc7d42b9701234a269805d89efbbc2181683368e4b","02153705b8e4df41dc9d58bc0360c79a9209b3fc289ec54118f0b149d5a3b3546d"],["02511e8cfb39c8ce1c790f26bcab68ba5d5f79845ec1c6a92b0ac9f331648d866a","02c29c1ea70e23d866204a11ec8d8ecd70d6f51f58dd8722824cacb1985d4d1870"]],"receiving":[["0283ce4f0f12811e1b27438a3edb784aeb600ca7f4769c9c49c3e704e216421d3e","03a1bbada7401cade3b25a23e354186c772e2ba4ac0d9c0447627f7d540eb9891d"],["0286b45a0bcaa215716cbc59a22b6b1910f9ebad5884f26f55c2bb38943ee8fdb6","02799680336c6bd19005588fad12256223cb8a416649d60ea5d164860c0872b931"],["039e2bf377709e41bba49fb1f3f873b9b87d50ae3b574604cd9b96402211ea1f36","02ef9ceaaf754ba46f015e1d704f1a06157cc4441da0cfaf096563b22ec225ca5f"],["025220baaca5bff1a5ffbf4d36e9fcc6f5d05f4af750ef29f6d88d9b5f95fef79a","02350c81bebfa3a894df69302a6601175731d443948a12d8ec7860981988e3803e"],["028fd6411534d722b625482659de54dd609f5b5c935ae8885ca24bfd3266210527","03b9c7780575f17e64f9dfd5947945b1dbdb65aecef562ac076335fd7aa09844e4"],["0353066065985ec06dbef33e7a081d9240023891a51c4e9eda7b3eb1b4af165e04","028c3fa7622e4c8bac07a2c549885a045532e67a934ca10e20729d0fdfe3a75339"],["02253b4eabf2834af86b409d5ca8e671de9a75c3937bff2dac9521c377ca195668","02d5e83c445684eb502049f48e621e1ca16e07e5dc4013c84d661379635f58877b"],["030d38e4c7a5c7c9551adcace3b70dcaa02bf841febd6dc308f3abd7b7bf2bdc49","0375a0b50cd7f3af51550207a766c5db326b2294f5a4b456a90190e4fbeb720d97"],["0327280215ba4a0d8c404085c4f6091906a9e1ada7ce4202a640ac701446095954","037cd9b5e6664d28a61e01626056cdb7e008815b365c8b65fa50ac44d6c1ad126e"],["02f80a80146674da828fc67a062d1ab47fb0714cf40ec5c517ee23ea71d3033474","03fd8ab9bc9458b87e0b7b2a46ea6b46de0a5f6ecaf1a204579698bfa881ff93ce"],["034965bd56c6ca97e0e5ffa79cdc1f15772fa625b76da84cc8adb1707e2e101775","033e13cb19d930025bfc801b829e64d12934f9f19df718f4ea6160a4fb61320a9c"],["034de271009a06d733de22601c3d3c6fe8b3ec5a44f49094ac002dc1c90a3b096d","023f0b2f653c0fdbdc292040fee363ceaa5828cfd8e012abcf6cd9bad2eaa3dc72"],["022aec8931c5b17bdcdd6637db34718db6f267cb0a55a611eb6602e15deb6ed4df","021de5d4bbb73b6dfab2c0df6970862b08130902ff3160f31681f34aecf39721f6"],["02a0e3b52293ec73f89174ff6e5082fcfebc45f2fdd9cfe12a6981aa120a7c1fa7","0371d41b5f18e8e1990043c1e52f998937bc7e81b8ace4ddfc5cd0d029e4c81894"],["030bc1cbe4d750067254510148e3af9bc84925cdd17db3b54d9bbf4a409b83719a","0371c4800364a8a32bfbda7ea7724c1f5bdbd794df8a6080a3bd3b52c52cf32402"],["0318c5cd5f19ff037e3dec3ce5ac1a48026f5a58c4129271b12ae22f8542bcd718","03b5c70db71d520d04f810742e7a5f42d810e94ec6cbf4b48fa6dd7b4d425e76c1"],["0213f68b86a8c4a0840fa88d9a06904c59292ec50172813b8cca62768f3b708811","0353037209eb400ba7fcfa9f296a8b2745e1bbcbfb28c4adebf74de2e0e6a58c00"],["028decff8a7f5a7982402d95b050fbc9958e449f154990bbfe0f553a1d4882fd03","025ecd14812876e885d8f54cab30d1c2a8ae6c6ed0847e96abd65a3700148d94e2"],["0267f8dab8fdc1df4231414f31cfeb58ce96f3471ba78328cd429263d151c81fed","03e0d01df1fd9e958a7324d29afefbc76793a40447a2625c494355c577727d69ba"],["03de3c4d173b27cdfdd8e56fbf3cd6ee8729b94209c20e5558ddd7a76281a37e2e","0218ccb595d7fa559f0bae1ea76d19526980b027fb9be009b6b486d8f8eb0e00d5"]],"xpub":"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e","xpub2":"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA"}},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2zA5ozHsbqT4BgTD45vGhx1edUg7tN4qp4LFbwCwEAGK3ZVaBaCRQnuy7AJ7qbPGxKiynNtGd7CzjBXEV4mEwStnPo98Xve"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFUEYv1psxyPnjiHhTYe85AwFRs5jShbpgrfQ9UXBmxantqgGT3oAVLiHDYoR3ruT3xRGcxsmBMJxyg94FGcxF86QnzYDc6e","x2/":"xpub661MyMwAqRbcGFd5DccFn4YW2HEdPhVZ2NEBAn416bvDFBi8HN5udmB6DkWpuXFtXaXZdq9UvMoiHxaauk6R1CZgKUR8vpng4LoudP4YVXA"},"seed":"start accuse bounce inhale crucial infant october radar enforce stage dumb spot account","seed_version":11,"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_1_1_seeded(self): + async def test_upgrade_from_client_2_1_1_seeded(self): wallet_str = '{"accounts":{"0":{"change":["03cbd39265f007d39045ccab5833e1ae16c357f9d35e67099d8e41940bf63ec330","03c94e9590d9bcd579caae15d062053e2820fe2a405c153dd4dca4618b7172ea6f","028a875b6f7e56f8cba66a1cec5dc1dfca9df79b7c92702d0a551c6c1b49d0f59b"],"receiving":["02fa100994f912df3e9538c244856828531f84e707f4d9eccfdd312c2e3ef7cf10","02fe230740aa27ace4f4b2e8b330cd57792051acf03652ae1622704d7eb7d4e5e4","03e3f65a991f417d69a732e040090c8c2f18baf09c3a9dc8aa465949aeb0b3271f","0382aa34a9cb568b14ebae35e69b3be6462d9ed8f30d48e0a6983e5af74fa441d3","03dfd8638e751e48fd42bf020874f49fbb5f54e96eff67d72eeeda3aa2f84f01c6","033904139de555bdf978e45931702c27837312ed726736eeff340ca6e0a439d232","03c6ca845d5bd9055f8889edcd53506cf714ac1042d9e059db630ec7e1af34133d","030b3bafc8a4ff8822951d4983f65b9bc43552c8181937188ba8c26e4c1d1be3ab","03828c371d3984ca5a248997a3e096ce21f9aeeb2f2a16457784b92a55e2aef288","033f42b4fbc434a587f6c6a0d10ac401f831a77c9e68453502a50fe278b6d9265c","0384e2c23268e2eb88c674c860519217af42fd6816273b299f0a6c39ddcc05bfa2","0257c60adde9edca8c14b6dd804004abc66bac17cc2acbb0490fcab8793289b921","02e2a67b1618a3a449f45296ea72a8fa9d8be6c58759d11d038c2fe034981efa73","02a9ef53a502b3a38c2849b130e2b20de9e89b023274463ea1a706ed92719724eb","037fc8802a11ba7ef06682908c24bcaedca1e2240111a1dd229bf713e2aa1d65a1","03ea0685fbd134545869234d1f219fff951bc3ec9e3e7e41d8b90283cd3f445470","0296bbe06cdee522b6ee654cc3592fce1795e9ff4dc0e2e2dea8acaf6d2d6b953b","036beac563bc85f9bc479a15d1937ea8e2c20637825a134c01d257d43addab217a","03389a4a6139de61a2e0e966b07d7b25b0c5f3721bf6fdcad20e7ae11974425bd9","026cffa2321319433518d75520c3a852542e0fa8b95e2cf4af92932a7c48ee9dbd"],"xpub":"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K3jsrbJYY4soqd3LdRfZFbAeLQUGwTNh3ejFZw7WxbYvkhAmPM88Swt1JwFX6DVGjPXeUcGcqa1XFuJPeiQaC9wiZ16PTKgQ"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGDxKhL5YS1kaB5B7q8H6xPZwCrgZ1iE2XXaiUeqD9MFEYRAuX7UNfdAED9yhAZdCB4ZS8dFrGDVU3x9ZK8uej8u8Pa2DLMq"},"pruned_txo":{},"seed":"flat toe story egg tide casino leave liquid strike cat busy knife absorb","seed_version":11,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_1_1_importedkeys(self): + async def test_upgrade_from_client_2_1_1_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_1_1_watchaddresses(self): + async def test_upgrade_from_client_2_1_1_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_1_1_trezor_singleacc(self): + async def test_upgrade_from_client_2_1_1_trezor_singleacc(self): wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_1_1_trezor_multiacc(self): + async def test_upgrade_from_client_2_1_1_trezor_multiacc(self): wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) + await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_1_1_multisig(self): + async def test_upgrade_from_client_2_1_1_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["03b5ca15f87baa1bb9d2508a9cf7cb596915a2749a6932bd71a5f353d72e2ff51e","03069d12bb7dc9fe7b8dab9ab2c7828173a4a4a5bacb10b9004854aef2ada2e440"],["036d7aeef82d50520f7d30d20a6b58a5e61c40949af4c147a105a8724478ba6339","021208a4a6c76934fbc2eed72a4a71713a5a093fb203ec3197edd1e4be8d9fb342"],["03ee5bd2bc7f9800b85f6f0a3fe8c23c797fa90d832f0332dfc72532e298dce54e","03474b76f33036673e1df73800b06d2df4b3617768c2b6a4f8a7f7d17c2b08cec3"]],"receiving":[["0288d4cc7e83b7028b8d2197c4efb490cb3dd248ee8683c715d9c59eb1884b2696","02c8ffee4ef168237f4a303dfe4957e328a8163c827cbe8ad07dcc24304b343869"],["022770e608e45981a31bad39a747a827ff4ce1eb28348fbe29ab776bdbf39346b4","03ebd247971aced7e2f49c495658ac5c32f764ebc4df5d033505e665f8d3f87b56"],["0256ede358326a99878d9de6c2c6a156548c266195fecea7906ddbb170da740f8d","02a500e7438d672c374713a9179fef03cbf075dd4c854566d6d9f4d899c01a4cf4"],["03fe2f59f10f6703bd3a43d0ae665ab72fb8b73b14f3a389b92e735e825fffdbe9","0255dd91624ba62481e432b9575729757b046501b8310b1dee915df6c4472f7979"],["0262c7c02f83196f6e3b9dd29e1bcad4834891b69ece12f628eea4379af6e701f8","0319ce2894fdf42bc87d45167a64b24ee2acdb5d45b6e4aadce4154a1479c8c58a"],["03bfb9ca9edab6650a908ffdcc0514f784aaccac466ba26c15340bc89a158d0b4c","03bcce80eed7b494f793b38b55cc25ae62e462ec7bf4d8ff6e4d583e8d04a4ac6d"],["0301dc9a41a44189e40c786048a0b6c13cc8865f3674fdf8e6cb2ab041eb71c0c7","020ded564880e7298068cf1498efcfb0f2306c6003e3de09f89030477ff7d02e18"],["03baffd970ecba170c31f48a95694a1063d14c834ccf2fdce0df46c3b81ab8edfb","0243ec650fc7c6642f7fb3b98e1df62f8b28b2e8722e79ccb271badba3545e8fc2"],["024be204a4bd321a727fb4a427189ae2f761f2a2c9898e9c37072e8a01026736d4","0239dc233c3e9e7c32287fdd7932c248650a36d8ab033875d272281297fadf292a"],["02197190b214c0215511d17e54e3e82cbe09f08e5ba2fb47aeafe01d8a88a8cb25","034a13cf01e26e9aa574f9ba37e75f6df260958154b0f6425e0242eacd5a3979c5"],["0226660fce4351019be974959b6b7dcf18d5aa280c6315af362ab60374b5283746","0304e49d2337a529ed8a647eceb555cd82e7e2546073568e30254530a61c174100"],["0324bb7d892dbe30930eb8de4b021f6d5d7e7da0c4ac9e3b95e1a2c684258d5d6c","02487aa272f0d3a86358064e080daf209ee501654e083f0917ad2aff3bbeb43424"],["03678b52056416da4baa8d51dca8eea534e38bd1d9328c8d01d5774c7107a0f9c1","0331deff043d709fc8171e08625a9adffba1bb614417b589a206c3a80eff86eddd"],["023a94d91c08c8c574199bc16e12789630c97cb990aeb5a54d938ff3c86786aabf","02d139837e34858f733e7e1b7d61b51d2730c57c274ed644ab80aff6e9e2fdef73"],["032f92dc11020035cd16995cfdc4bc6bef92bc4a06eb70c43474e6f7a782c9c0e1","0307d2c32713f010a0d0186e47670c6e46d7a7e623026f9ed99eb27cdae2ae4b49"],["02f66a91a024628d6f6969af2ed9ded087a88e9be86e4b3e5830868643244ec1ae","02f2a83ebb1fbbd04e59a93284e35320c74347176c0592512411a15efa7bf5fa44"],["03585bae6f04f2d3f927d79321b819cccf2bcd1d28d616aac9407c6c13d590dfbd","021f48f02b485b9b3223fca4fbc4dd823a8151053b8640b3766c37dfa99ba78006"],["02b28e2d6f1ac3fde4b34c938e83c0ef0d85fd540d8c33b33a109f4ebbc4a36a4d","030a25a960e28e751a95d3c0167fad496f9ec4bc307637c69b3bd6682930532736"],["03782c0dee8d279c547d26853e31d90bc7d098e16015c2cc334f2cc2a2964f2118","021fe4d6392dba40f1aa35fa9ec3ebfde710423f036482f6a5b3c47d0e149dfe47"],["0379b464b4f9cced0c71ee66c4fca1e61190bac9a6294242aabd4108f6a986a029","030a5802c5997ebae590147cb5eeba1690455c5d2a87306345586e808167072b50"]],"xpub":"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9","xpub2":"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2NvXPAX5QUcr1KHCRVyDMikGc7WMuS34y2BktAqJsq1eJvk7JWroKM8PdGa2FHWiTpAvH9nj6BkQos5XhJU5mfS12tdtBYy"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcErzzVC45mcZaZM7gpxh4iwfsQVuyTma3qpWuRi9ZRdL8ACqu25LP2jssmKmpEbnGohH9XnoZ1etW3TKaiy5dWnUuiN6LvD9","x2/":"xpub661MyMwAqRbcH4DqLo2tRYzSnnqjXk21aqNz3oAuYkr66YxucWgc2X8oLdS2KLPSKqrfZwStQYEpUp5jVxQfTBmEwtw3JaPRf6mq6JLD3Qr"},"pruned_txo":{},"seed":"snack oxygen clock very envelope staff table bus sense fiscal cereal pilot abuse","seed_version":11,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_2_0_seeded(self): + async def test_upgrade_from_client_2_2_0_seeded(self): wallet_str = '{"accounts":{"0":{"change":["038f4bae4a901fe5f2a30a06a09681fff6678e8efda4e881f71dcdc0fdb36dd1b8","032c628bec66fe98c3921b4fea6f18d241e6b23f4baf9e56c78b7a5262cd4cc412","0232b68a11cde50a49fb3155fe2c9e9cf7aa9f4bcb0f51c3963b13c997e40de40d"],"receiving":["0237246e68c6916c43c7c5aca1031df0c442439b80ceda07eaf72645a0597ed6aa","03f35bee973012909d839c9999137b7f2f3296c02791764da3f55561425bb1d53c","02fdbe9f95e2279045e6ef5f04172c6fe9476ba09d70aa0a8483347bfc10dee65e","026bc52dc91445594bb639c7a996d682ac74a4564381874b9d36cc5feea103d7a4","0319182796c6377447234eeee9fe62ce6b25b83a9c46965d9a02c579a23f9fa57a","02e23d202a45515ce509c8b9548a251de3ad8e64c92b24bb74b354c8d4d0dc85af","0307d7ccb51aa6860606bcbe008acc1aae5b53d19d0752a20a327b6ec164399b52","038a2362fde711e1a4b9c5f8fe1090a0a38aec3643c0c3d69b00660b213dc4bfb8","0396255ef7b75e5d8ffc18d01b9012a98141ee5458a68cde8b25c492c569a22ab8","02c7edf03d215b7d3478fb26e9375d541440f4a8b5c562c0eb98fab6215dbea731","024286902b95da3daf6ffb571d5465537dae5b4e00139e6465e440d6a26892158e","03aa0d3fa1fe190a24e14d6aabd9c163c7fe70707b00f7e0f9fa6b4d3a4e441149","03995d433093a2ae9dc305fe8664f6ab9143b2f7eaf6f31bc5fefdacb183699808","033c5da7c4c7a3479ddb569fecbcbb8725867370746c04ff5d2a84d1706607bbab","036a097331c285c83c4dab7d454170b60a94d8d9daa152b0af6af81dbd7f0cc440","033ed002ddf99c1e21cb8468d0f5512d71466ac5ba4003b33d71a181e3a696e3c5","02a6a0f30d1a341063a57a0549a3d16d9487b1d4e0d4bffadabdc62d1ad1a43f8f","02dcae71fc2e31013cf12ad78f9e16672eeb7c75e536f4f7d36adb54f9682884eb","028ef32bc57b95697dacdb29b724e3d0fa860ffdc33c295962b680d31b23232090","0314afd1ac2a4bf324d6e73f466a60f511d59088843f93c895507e7af1ccdb5a3b"],"xpub":"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2RXcXAtqKFsXxzkq3S2DJogzkkgptRntXy1LKAG9h6YBvw8JjSUogF1UNneyYgS5uYshMBemqr41XsC7bTr8Fjx1uAyLbPC"},"master_public_keys":{"x/":"xpub661MyMwAqRbcEuc5dCRqgPpGX2bKStk4g2cbZ96SSmKsQmLUrhaQEtrfnBMsXSUSyKWuCtjLiZ8zXrxcNeC2LR8gnZPrQJdmUEeofS2yuux"},"pruned_txo":{},"seed":"agree tongue gas total hollow clip wasp slender dolphin rebel ozone omit achieve","seed_version":11,"stored_height":0,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_2_0_importedkeys(self): + async def test_upgrade_from_client_2_2_0_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":489714,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_2_0_watchaddresses(self): + async def test_upgrade_from_client_2_2_0_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_2_0_trezor_singleacc(self): + async def test_upgrade_from_client_2_2_0_trezor_singleacc(self): wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_2_0_trezor_multiacc(self): + async def test_upgrade_from_client_2_2_0_trezor_multiacc(self): wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490006,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) + await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_2_0_multisig(self): + async def test_upgrade_from_client_2_2_0_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["037ba2d9d7446d54f1b46c902427e58a4b63915745de40f31db52e95e2eb8c559c","03aab9d4cb98fec92e1a9fc93b93f439b30cdb47cb3fae113779d0d26e85ceca7b"],["036c6cb5ed99f4d3c8d2dd594c0a791e266a443d57a51c3c7320e0e90cf040dad0","03f777561f36c795911e1e42b3b4babe473bcce32463eb9340b48d86fded8a226a"],["03de4acea515b1b3b6a2b574d08539ced475f86fdf00b43bff16ec43f6f8efc8b7","036ebfdd8ba75c94e0cb1819ecba464d04a77bab11c8fc2b7e90dd952092c01f0e"]],"receiving":[["03e768d9de027e4edaf0685abb240dde9af1188f5b5d2aa08773b0083972bdec74","0280eccb8edec0e6de521abba3831f51900e9d0655c59cddf054b72a70b520ddae"],["02f9c0b7e8fe426a45540027abca63c27109db47b5c86886b99db63450444bb460","03cb5cdcc26b0aa326bc895fcc38b63416880cdc404efbeab3ff14f849e4f4bd63"],["024d6267b9348a64f057b8e094649de36e45da586ef8ca5ecb7137f6294f6fd9e3","034c14b014eb28abfeaa0676b195bde158ab9b4c3806428e587a8a3c3c0f2d38bb"],["02bc3d5456aa836e9a155296be6a464dfa45eb2164dd0691c53c8a7a05b2cb7c42","03a374129009d7e407a5f185f74100554937c118faf3bbe4fe1cac31547f46effa"],["024808c2d17387cd6d466d13b278f76d4d04a7d31734f0708a8baf20ae8c363f9a","02e18dfc7f5ea9e8b6afe0853a9aba55861208b32f22c81aa4be0e6aee7951963d"],["0331bef7adca60ae484a12cc3c4b788d4296e0b52500731bf5dff1b935973d4768","025774c45aeac2ae87b7a67e79517ffb8264bdf1b56905a76e7e7579f875cbed55"],["020566e7351b4bfe6c0d7bda3af24267245a856af653dd00c482555f305b71a8e3","036545f66ad2fe95eeb0ec1feb501d552773e0910ec6056d6b827bc0bb970a1ecc"],["038dc34e68a49d2205f4934b739e510dca95961d0f8ab6f6cd9279d68048cfd93b","03810c50d1e2ff0e39179788e8506784bc214768884f6f71dc4323f6c29e25c888"],["035059ff052ab044fd807905067ec79b19177edcf1b1b969051dc0e6957b1e1eab","03d790376a0144860017bea5b5f2f0a9f184a55623e9a1e8f3670bf6aba273f4fb"],["02bb730d880b90e421d9ac97313b3c0eec6b12a8c778388d52a188af7dc026db43","030ae3ae865b805c3c11668b46ec4f324d50f6b5fbc2bb3a9ae0ddc4aea0d1487a"],["0306eeb93a37b7dcbb5c20146cfd3036e9a16e5b35ecfe77261a6e257ee0a7b178","03fb49f5f1d843ca6d62cee86fd4f79b6cc861f692e54576a9c937fdff13714be9"],["03f4c358e03bd234055c1873e77f451bea6b54167d36c005abeb704550fbe7bee1","03fc36f11d726fd4321f99177a0fff9b924ec6905d581a16436417d2ea884d3c80"],["024d68322a93f2924d6a0290ebe7481e29215f1c182bd8fdeb514ade8563321c87","02aa5502de7b402e064dfebc28cb09316a0f90eec333104c981f571b8bc69279e2"],["03cbda5b33a72be05b0e50ef7a9872e28d82d5a883e78a73703f53e40a5184f7a5","02ebf10a631436aa0fdef9c61e1f7d645aa149c67d3cb8d94d673eb3a994c36f86"],["0285891a0f1212efff208baf289fd6316f08615bee06c0b9385cc0baad60ebc08a","0356a6c4291f26a5b0c798f3d0b9837d065a50c9af7708f928c540017f150c40b6"],["02403988346d00e9b949a230647edbe5c03ce36b06c4c64da774a13aca0f49ce92","02717944f0bb32067fb0f858f7a7b422984c33d42fd5de9a055d00c33b72731426"],["02161a510f42bcc7cdd24e7541a0bdbcac08b1c63b491df1974c6d5cd977d57750","03006d73c0ab9fdd8867690d9282031995cfd094b5bdc3ff66f3832c5b8a9ca7f9"],["03d80ea710e1af299f1079dd528d6cdc5797faa310bafa90ca7c45ea44d5ba64f3","02b29e1170d6bec16ace70536565f1dff1480cba2a7545cfec7b522568a6ab5c38"],["02c3f6e8dea3cace7aab89d8258751827cb5791424c71fa82ae30192251ca11a28","02a43d2d952e1f3fb58c56dadabb39cf5ed437c566f504a79f2ade243abd2c9139"],["0308e96e38eb89ca5abaa6776a1968a1cbb33197ec91d40bb44bede61cb11a517f","034d0545444e5a5410872a3384cedd3fb198a8211bb391107e8e2c0b0b67932b20"]],"xpub":"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC","xpub2":"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K2iFCx5cDooeMmK1uy9Xb36T8c2uCruujNdTfaaJaF6DGNDcDKkX1U4V1XiEcvCqoNsQhMQUnp8ZvMgxDBDErtMACo2HtGgQ"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFCKg479EAwb6KLrQNcFSQKNjQRJpRFSiFRnp87cpntXkDUEvRtFTEARirm9584ML8sLBkF3gDBcyYgknnxCCrBMwPDDMQwC","x2/":"xpub661MyMwAqRbcFaEDoCANCiY9dhXvA8GgXFSLXYADmxmatLidGTxnVL6vuoFAMg9ugX8MTKjZPiP9uUPXusUji11LnWWLCw8Lzgx7pM5sg1s"},"pruned_txo":{},"seed":"such duck column calm verb sock used message army suffer humble olive abstract","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_3_2_seeded(self): + async def test_upgrade_from_client_2_3_2_seeded(self): wallet_str = '{"accounts":{"0":{"change":["03b37d18c0c52da686e8fd3cc5d242e62036ac2b38f101439227f9e15b46f88c42","026f946e309e64dcb4e62b00a12aee9ee14d26989880e690d8c307f45385958875","03c75552e48d1d44f966fb9cfe483b9479cc882edcf81e2faf92fba27c7bbecbc1","020965e9f1468ebda183fea500856c7e2afcc0ccdc3da9ccafc7548658d35d1fb3","03da778470ee52e0e22b34505a7cc4a154e67de67175e609a6466db4833a4623ed","0243f6bbb6fea8e0da750645b18973bc4bd107c224d136f26c7219aab6359c2705"],"receiving":["0376bf85c1bf8960947fe575adc0a3f3ba08f6172336a1099793efd0483b19e089","03f0fe0412a3710a5a8a1c2e01fe6065b7a902f1ccbf38cd7669806423860ad111","03eacb81482ba01a741b5ee8d52bb6e48647107ef9a638ca9a7b09f6d98964a456","03c8b598f6153a87fc37f693a148a7c1d32df30597404e6a162b3b5198d0f2ba33","03fefef3ee4f918e9cd3e56501018bcededc48090b33c15bf1a4c3155c8059610a","0390562881078a8b0d54d773d6134091e2da43c8a97f4f3088a92ca64d21fcf549","0366a0977bb35903390e6b86bbb6faa818e603954042e98fe954a4b8d81d815311","025d176af6047d959cfdd9842f35d31837034dd4269324dc771c698d28ad9ae3d6","02667adce009891ee872612f31cd23c5e94604567140b81d0eae847f5539c906d6","03de40832017ba85e8131c2af31079ab25a72646d28c8d2b6a39c98c4d1253ae2f","02854c17fdef156b1681f494dfc7a10c6a8033d0c577b287947b72ecada6e6386b","0283ff8f775ba77038f787b9bf667f538f186f861b003833600065b4ad8fd84362","03b0a4e9a6ffecd955bd0e2b169113b544a7cba1688dca6fce204552403dc28391","02445465cf40603506dbe7fa853bc1aae0d79ca90e57b6a7af6ffc1341c4ca8e2d","0220ea678e2541f809da75552c07f9e64863a254029446d6270e433a4434be2bd7","02640e87aab83bd84fe964eac72657b34d5ad924026f8d2222557c56580607808e","020fa9a0c3b335c6cdc6588b14c596dfae242547dd68e5c6bce6a9347152ff4021","03f7f052076dc35483c91033edef2cc93b54fb054fe3b36546800fa1a76b1d321a","030fd12243e1ffe1fc6ec3cdb7e020a467d3146d55d52af915552f2481a91657cd","02dd1a2becbc344a297b104e4bb41f7de4f5fcff1f3244e4bb124fbb6a70b5eb18"],"xpub":"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2JYf9F9kcyYQAGiXrTVmJsAYuixpsnA8uyVwCYCPk1NtzYuNmeLRLKcMYb3UoPgTocYsHsAje3mSjX4jp3Ci17VhuESjsBU"},"master_public_keys":{"x/":"xpub661MyMwAqRbcEnd8FGgkz7V8iJZ2FvDcg669i7NSS7h7nmq5k5WeHohNqosRSjx9CKiRxMgTidPWA5SJYsjrXhr1azR3boubNp24gZHUeY4"},"pruned_txo":{},"seed":"scheme grape nephew hen song purity pizza syrup must dentist bright grit accuse","seed_version":11,"stored_height":0,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_3_2_importedkeys(self): + async def test_upgrade_from_client_2_3_2_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":489715,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_3_2_watchaddresses(self): + async def test_upgrade_from_client_2_3_2_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_3_2_trezor_singleacc(self): + async def test_upgrade_from_client_2_3_2_trezor_singleacc(self): wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":0,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_3_2_trezor_multiacc(self): + async def test_upgrade_from_client_2_3_2_trezor_multiacc(self): wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"labels":{},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490008,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) + await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_3_2_multisig(self): + async def test_upgrade_from_client_2_3_2_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["03083942fe75c1345833faa4d31a635e088ca173047ddd6ef5b7f1395892ef339d","03c02f486ed1f0e6d1aefbdea293c8cb44b34a3c719849c45e52ef397e6540bbda"],["0326d9adb5488c6aba8238e26c6185f4d2f1b072673e33fb6b495d62dc800ff988","023634ebe9d7448af227be5c85e030656b353df81c7cf9d23bc2c7403b9af7509b"],["0223728d8dd019e2bd2156754c2136049a3d2a39bf2cb65965945f4c598fdb6db6","037b6d4df2dde500789f79aa2549e8a6cb421035cda485581f7851175e0c95d00e"],["03c47ade02def712ebbf142028d304971bec99ca53be8e668e9cf15ff0ef186e19","02e212ad25880f2c9be7dfd1966e4b6ae8b3ea40e09d482378b942ca2e716397b0"],["03dab42b0eaee6b0e0d982fbf03364b378f39a1b3a80e980460ae96930a10bff6c","02baf8778e83fbad7148f3860ce059b3d27002c323eab5957693fb8e529f2d757f"],["02fc3019e886b0ce171242ddedb5f8dcde87d80ad9f707edb8e6db66a4389bea49","0241b4e9394698af006814acf09bf301f79d6feb2e1831a7bc3e8097311b1a96dd"]],"receiving":[["023e2bf49bc40aeed95cb1697d8542354df8572a8f93f5abe1bcec917778cc9fc6","03cf4e80c4bf3779e402b85f268ada2384932651cc41e324e51fc69d6af55ae593"],["02d9ba257aa3aba2517bb889d1d5a2e435d10c9352b2330600decab8c8082db242","03de9e91769733f6943483167602dd3d439e34b7078186066af8e90ec58076c2a7"],["02ccdd5b486cefa658af0c49d85aefa3ab62f808335ffcd4b8d4197a3c50ab073c","03e80dbbd0fb93d01d6446d0af1c18c16d26bdbb2538d8bf7f2f68ce95ba857667"],["031605867287fe3b1fee55e07b2f513792374bb5baf30f316970c5bc095651a789","02c0802b96cee67d6acec5266eb3b491c303cea009d57a6bb7aee83cc602206ad5"],["037d07d30dec97da4ea09d568f96f0eb6cd86d02781a7adff16c1647e1bcd23260","03d856a53bc90be84810ce94c8aac0791c9a63379fd61790c11dae926647aa4eec"],["028887f2d54ffefc98e5a605c83bedba79367c2a4fe11b98ec6582896ffad79216","0259dab6dafe52306fe6e3686f27a36e0650c99789bb19cbcd0907db00957030a9"],["039d83064dd37681eaf7babe333b210685ba9fe63627e2f2d525c1fb9c4d84d772","03381011299678d6b72ff82d6a47ed414b9e35fcf97fc391b3ff1607fb0bf18617"],["03ace6ceb95c93a446ae9ff5211385433c9bbf5785d52b4899e80623586f354004","0369de6b20b87219b3a56ea8007c33091f090698301b89dd6132cf6ef24b7889a0"],["031ec2b1d53da6a162138fb8f4a1ec27d62c45c13dddecebbd55ad8a5d05397382","02417a3320e15c2a5f0345ac927a10d7218883170a9e64837e629d14f8f3de7c78"],["02b85c8b2f33b6a8a882c383368be8e0a91491ea57595b6a690f01041be5bef4fb","0383ad57c7899284e9497e9dccb1de5bf8559b87157f13fee5677dcf2fbeb7b782"],["03eaa9e3ea81b2fa6e636373d860c0014e67ac6363c9284e465384986c2ec77ee2","03b1bd0d6355d99e8cab6d177f10f05eb8ddd3e762871f176d78a79f14ae037826"],["03ecd1b458e7c2b71a6542f8e64c750358c1421542ffe7630cc3ecc6866d379dfe","02d5c5432ca5e4243430f73a69c180c23bda8c7c269d7b824a4463e3ac58850984"],["028098ae6e772460047cdd6694230dcfc44da8ceabcae0624225f2452be7ae26c4","02add86858446c8a59ed3132264a8141292cd4ece6653bf3605895cceb00ba30b9"],["02f580882255cda6fae954294164b26f2c4b6b2744c0930daaa7a9953275f2f410","02c09c5e369910d84057637157bdf1fb721387bb2867c3c2adb2d91711498bbe5e"],["025e628f78c95135669ab8b9178f4396b0b513cbeae9ca631ba5e5e8321a4a05bc","03476f35b4defcc67334a0ff2ce700fb55df39b0f7f4ff993907e21091f6a29a31"],["026fa6f3214dce2ad2325dae3cd8d6728ce62af1903e308797ff071129fe111eca","03d07eb26749caceca56ffe77d9837aaf2f657c028bd3575724b7e2f1a8b3261a5"],["03894311c920ef03295c3f1c8851f5dc9c77e903943940820b084953a0a92efcc3","0368b0b3774f9de81b9f10e884d819ccf22b3c0ed507d12ce2a13efc36d06cdc17"],["024f8a61c23aa4a13a3a9eb9519ed3ec734f54c5e71d55f1805e873c31a125c467","039e9c6708767bd563fcdca049c4d8a1acab4a051d4f804ae31b5e9de07942570f"],["038f9b8f4b9fe6af5ced879a16bb6d56d81831f11987d23b32716ca4331f6cbabf","035453374f020646f6eda9528543ec0363923a3b7bbb40bc9db34740245d0132e7"],["02e30cd68ae23b3b3239d4e98745660b08d7ce30f2f6296647af977268a23b6c86","02ee5e33d164f0ad6b63f0c412734c1960507286ad675a343df9f0479d21a86ecc"]],"xpub":"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG","xpub2":"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3"}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3gKU7sqAtVSxM8mrqm8ctmrcL3TahRCRy62EgYn2XPuLoJAGbBGvL4ArbPoAay5jo7L1UbBv15SsmrSKdTQSgDE351WSkm6"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcGAPwDuNBFdPguAcMFDrUFznD8RsCFkjQqtMPE66H5CDpecMJ9giZ1GVuZUpxhX4nFh1R3fzzr4hjDoxDSHymXVXQa7H1TjG","x2/":"xpub661MyMwAqRbcFMKuZtmYryCNiNvHAki74TizX3b6dxaREtjLMoqnLJbd1zQKjWwKEThmB4VRtwePAWHNk9G5nHvAEvMHDYemERPQ7bMjQE3"},"pruned_txo":{},"seed":"brick huge enforce behave cabin cram okay friend sketch actor casual barrel abuse","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_4_3_seeded(self): + async def test_upgrade_from_client_2_4_3_seeded(self): wallet_str = '{"accounts":{"0":{"change":["02707eb483e51d859b52605756aee6773ea74c148d415709467f0b2a965cd78648","0321cddfb60d7ac41fdf866b75e4ad0b85cc478a3a84dc2e8db17d9a2b9f61c3b5","0368b237dea621f6e1d580a264580380da95126e46c7324b601c403339e25a6de9","02334d75548225b421f556e39f50425da8b8a36960cce564db8001f7508fef49f6","02990b264de812802743a378e7846338411c3afab895cff35fb24a430fa6b43733","02bc3b39ca00a777e95d89f773428bad5051272b0df582f52eb8d6ebb5bb849383"],"receiving":["0286c9d9b59daa3845b2d96ce13ac0312baebaf318251bac6d634bcac5ff815d9d","0220b65829b3a030972be34559c4bb1fc91f8dfd7e1703ddb43da9aa28aa224864","02fe34b26938c29faee00d8d704eae92b7c97d487825892290309073dc85ae5374","03ea255ae2ba7169802543cf7af135783f4fca91924fd0285bdbe386d78a0ab87e","027115aeea786e2745812f2ec2ae8fee3d038d96c9556b1324ac50c913b83a9e6a","03627439bb701352e35d0cf8e00617d8e9bf329697e430b0a5d999370097e025b4","034120249c6b15d051525156845aefaa83988adf9ed1dd18b796217dcf9824b617","02dfeb0c89eee66026d7650ee618c2172551f97fdd9ed249e696c54734d26e39a3","037e031bb4e51beb5c739ba6ab64aa696e85457ea63cc56698b7d9b731fd1e8e61","0302ea6818525492adc5ed8cfd2966efd704915199559fe1c06d6651fd36533012","0349394140560d685d455595f697d17b44e832ec453b5a2f02a3f5ed66205f3d30","036815bf2437df00440b15cfa7123544648cf266247989e82540d6b1cae1589892","02f98568e8f0f4b780f005e538a7452a60b2c06a5d2e3a23fa26d88459d118ef56","02e36ccb8b05a2762a08f60541d1a5a136afd6a73119eea8c7c377cc8b07eb2e2f","031566539feb6f0a212cca2604906b1c1f5cfc5bf5d5206e0c695e37ef3a141fd2","025754e770bedeef6f4e932fa231b858b49d28183e1be6da23e597c67dd7785f19","03a29961f5fb9c197cffe743081a761442a3cf9ded0be2fa07ab67023a74c08d28","023184c1995a9f51af566c9c0b4da92d7fd4a5c59ff93c34a323e94671ddbe414a","029efdb15d3aec708b3af2aee34a9157ff731bec94e4f19f634ab43d3101e47bd8","03e16b13fe6bb9aa6dc4e331e19ab4d3d291a2670b97e6040e87a7c7309b243af9"],"xpub":"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV"}},"accounts_expanded":{},"master_private_keys":{"x/":"xprv9s21ZrQH143K2XEo8EzwtKy5mNSy41rvecdkfRfMvdBxNEaGtNSsMD8iwHsc91UxKtSrDHXex53NkMRRDwnm4PmqS7N35K8BR1KCD2qm5iE"},"master_public_keys":{"x/":"xpub661MyMwAqRbcF1KGEGXxFTupKQHTTUan1qZMTp4yUxiwF2uRRum7u1TCnaJRjaSBW4d42Fwfi6xfLvfRfgtDixekGDWK9CPWguR7YzXKKeV"},"seed":"smart fish version ocean category disagree hospital mystery survey chef kid latin about","seed_version":11,"use_encryption":false,"wallet_type":"standard"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_4_3_importedkeys(self): + async def test_upgrade_from_client_2_4_3_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"stored_height":477636,"use_encryption":false,"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_4_3_watchaddresses(self): + async def test_upgrade_from_client_2_4_3_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_4_3_trezor_singleacc(self): + async def test_upgrade_from_client_2_4_3_trezor_singleacc(self): wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":485855,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_4_3_trezor_multiacc(self): + async def test_upgrade_from_client_2_4_3_trezor_multiacc(self): wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]]},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490009,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor"}''' - self._upgrade_storage(wallet_str, accounts=2) + await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_4_3_multisig(self): + async def test_upgrade_from_client_2_4_3_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["03467a8bae231aff83aa01999ee4d3834894969df7a3b0753e23ae7a3aae089f6b","02180c539980494b4e59edbda5e5340be2f5fbf07e7c3898b0488950dda04f3476"],["03d8e18a428837e707f35d8e2da106da2e291b8acbf40ca0e7bf1ac102cda1de11","03fad368e3eb468a7fe721805c89f4405581854a58dcef7205a0ab9b903fd39c23"],["0331c9414d3eee5bee3c2dcab911537376148752af83471bf3b623c184562815d9","02dcd25d2752a6303f3a8366fae2d62a9ff46519d70da96380232fc9818ee7029e"],["03bb18a304533086e85782870413688eabef6a444a620bf679f77095b9d06f5a16","02f089ed84b0f7b6cb0547741a18517f2e67d7b5d4d4dd050490345831ce2aef9e"],["02dc6ebde88fdfeb2bcd69fce5c5c76db6409652c347d766b91671e37d0747e423","038086a75e36ac0d6e321b581464ea863ab0be9c77098b01d9bc8561391ed0c695"],["02a0b30b12f0c4417a4bef03cb64aa55e4de52326cf9ebe0714613b7375d48a22e","02c149adda912e8dc060e3bbe4020c96cff1a32e0c95098b2573e67b330e714df0"]],"m":2,"receiving":[["0254281a737060e919b071cb58cc16a3865e36ea65d08a7a50ba2e10b80ff326d5","0257421fa90b0f0bc75b67dd54ffa61dc421d583f307c58c48b719dd59078023e4"],["03854ce9bbc7813d535099658bcc6c671a2c25a269fdb044ee0ed5deb95da0d7e0","025379ca82313dde797e5aa3f222dddf0f7223cb271f79ecce2c8178bea3e33c62"],["03ae6ad5ffc75d71adc2ab87e3adc63fa8696a8656e1135adb5ae88ddb6d39089f","025ed8821f8b37aef69b1aabf89e4e405f09206c330c78e94206b21139ddafcc4f"],["033ea4d8b88d36d14a52983ae30d486254af2dfa1c7f8e04bc9d8e34b3ffe4b32a","02b441a3e47a338d89027755b81724219362b8d9b66142d32fcb91c9c7829d8c9f"],["029195704b9bbc3014452bbf07baa7bf6277dfefd9721aea8438f2671ba57b898b","022264503140f99b41c0269666ab6d16b2dad72865dbd2bf6153d45f5d11978e4d"],["037e3caa2d151123821dff34fd8a76ac0d56fa97c41127e9b330a115bf12d76674","02a4ae28e2011537de4cce0c47af4ac0484b38d408befcb731c3d752922fcd3c5b"],["02226853ca32e72b4771ccc47c0aae27c65ed0d25c525c1f673b913b97dca46cc5","027a9c855fc4e6b3f8495e77347a1e03c0298c6a86bd5a89800195bd445ae3e3bd"],["02890f7eee0766d2dde92f3146cd461ae0fa9caf07e1f3559d023a20349bae5e44","0380249f30829b3656c32064ddf657311159cecb36f9dbbf8e50e3d7279b70c57e"],["02ab9613fd5a67a3fdf6b6241d757ce92b2640d9d436e968742cb7c4ec4bb3e6e9","0204b29cc980b18dfb3a4f9ca6796c6be3e0aee2462719b4a787e31c8c5d79c8cf"],["029103b50ecc0cc818c1c97e8acb8ce3e1d86f67e49f60c8496683f15e753c3eed","0247abb2c5e4cde22eb59a203557c0bbe87e9c449e6c2973e693ac14d0d9cf3f28"],["02817c935c971e6e318ba9e25402df26ca016a4e532459be5841c2d83a5aa8a967","03331fe3a2e4aa3e2dc1d8d4afc5a88c57350806b905e593b5876c6b9cef71fd4d"],["03023c6797af5c9c3d7db2fbeb9d7236601fe5438036200f2f59d9b997d29ec123","023b1084f008cf2e9632967095958bb0bbd59e60a0537e6003d780c7ebccb2d4f5"],["0245e0bdebe483fef984e4e023eb34641e65909cd566eb6bd6c0bce592296265a1","0363bad4b477d551f46b19afcc10decf6a4c1200becb5b22c032c62e6d90b373b8"],["0379ba2f8c5e8e5e3f358615d230348fe8d7855ef9c0e1cf97aac4ec09dfe690aa","02ecda86ff40b286a3faadf9a5b361ab7a5beb50426296a8c0e3d222f404ae4380"],["02e090227c22efa7f60f290408ce9f779e27b39d4acec216111cc3a8b9594ab451","02144954ddabb55abcfe49ea703a4e909ab86db2f971a2e85fc006dffbdf85af52"],["025dc4bd1c4809470b5a14cf741519ad7f5f2ccd331b42e0afd2ce182cdf25f82d","03d292524190af850665c2255a785d66c59fea2b502d4037bb31fdde10ad9b043f"],["027e7c549f613ae9ba1d806c8c8256f870e1c7912e3e91cbb326d61fb20ac3a096","03fbbf15ee2b49878c022d0b30478b6a3acb61f24af6754b3f8bcb4d2e71968099"],["02c188eaf5391e52fdcd66f8522df5ae996e20c524577ac9ffa7a9a9af54508f7c","03fe28f1ea4a0f708fa2539988758efd5144a128cc12aed28285e4483382a6636a"],["03bea51abacd82d971f1ef2af58dcbd1b46cdfa5a3a107af526edf40ca3097b78d","02267d2c8d43034d03219bb5bc0af842fb08f028111fc363ec43ab3b631134228a"],["03c3a0ecdbf8f0a162434b0db53b3b51ce02886cbc20c52e19a42b5f681dac6ffb","02d1ede70e7b1520a6ccabd91488af24049f1f1cf2661c07d8d87aee31d5aec7c9"]],"xpubs":["xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ","xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH"]}},"accounts_expanded":{},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3NsvVsyjvVQv2XXFBc1UTY9QcuYnVHTFLyeAVsFo1FjJsBk48XK16jZLqRs1B5Sa6SCqYdA2XFvB9riBca2GyGccYGKKP6t"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcFrxPbuWkHdMeaZMjb4jKpm51RHxQ3czEDmyK3Qa3Z43niVzVjFyhJs6SrdXgQg56DHMDcC94a7MCtn9Pwh2bafhHGJbLWeH","x2/":"xpub661MyMwAqRbcFafkG2opdo3ou3zUEpFK3eKpWWYkdA5kfvootPkZzqvUV1rtLYRLdUxvXBZApzZpwyR2mXBd1hRtnc4LoaLTQWDFcPKnKiQ"},"pruned_txo":{},"seed":"angry work entry banana taste climb script fold level rate organ edge account","seed_version":11,"stored_height":490033,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2"}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_5_4_seeded(self): + async def test_upgrade_from_client_2_5_4_seeded(self): wallet_str = '{"accounts":{"0":{"change":["0253e61683b66ebf5a4916334adf1409ffe031016717868c9600d313e87538e745","021762e47578385ecedc03c7055da1713971c82df242920e7079afaf153cc37570","0303a8d6a35956c228aa95a17aab3dee0bca255e8b4f7e8155b23acef15cf4a974","02e881bc60018f9a6c566e2eb081a670f48d89b4a6615466788a4e2ce20246d4c6","02f0090e29817ef64c17f27bf6cdebc1222f7e11d7112073f45708e8d218340777","035b9c53b85fd0c2b434682675ac862bfcc7c5bb6993aee8e542f01d96ff485d67"],"receiving":["024fbc610bd51391794c40a7e04b0e4d4adeb6b0c0cc84ac0b3dad90544e428c47","024a2832afb0a366b149b6a64b648f0df0d28c15caa77f7bbf62881111d6915fe9","028cd24716179906bee99851a9062c6055ec298a3956b74631e30f5239a50cb328","039761647d7584ba83386a27875fe3d7715043c2817f4baca91e7a0c81d164d73d","02606fc2f0ce90edc495a617329b3c5c5cc46e36d36e6c66015b1615137278eabd","02191cc2986e33554e7b155f9eddcc3904fdba43a5a3638499d3b7b5452692b740","024b5bf755b2f65cab1f7e5505febc1db8b91781e5aac352902e79bc96ad7d9ad0","0309816cb047402b84133f4f3c5e56c215e860204513278beef54a87254e44c14a","03f53d34337c12ddb94950b1fee9e4a9cf06ad591db66194871d31a17ec7b59ac7","0325ede4b08073d7f288741c2c577878919fd5d832a9e6e04c9eac5563ae13aa83","02eca43081b04f68d6c8b81781acd59e5b8d2ba44dba195369afc40790fd9edef7","029a8ca96c64d3a98345be1594208908f2be5e6af6bcc6ff3681f271e75fcf232e","02fbe0804980750163a216cc91cfe86e907addf0e80797a8ea5067977eb4897c1b","0344f32fc1ee8b2eb08f419325529f495d77a3b5ea683bbce7a44178705ab59302","021dd62bdf18256bd5316ce3cbcca58785378058a41ba2d1c58f4cc76449b3c424","035e61cdbdb4306e58a816a19ad92c7ca3a392b67ac6d7257646868ffe512068c5","0326a4db82f21787d0246f8144abe6cda124383b7d93a6536f36c05af530ea262a","02b352a27a8f9c57b8e5c89b357ba9d0b5cb18bf623509b34cd881fcf8b89a819a","02a59188edef1ed29c158a0adb970588d2031cfe53e72e83d35b7e8dd0c0c77525","02e8b9e42a54d072c8887542c405f6c99cfabf41bdde639944b44ba7408837afd1"],"xpub":"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG"}},"accounts_expanded":{},"addr_history":{"12LXoVHUnAXn6BVBpshjwd7sSTwp5nsd7W":[],"12iXPYBErR6ZMESB9Nv74S4pVxdGMNLiW2":[],"13jmb5Vc2qh29tPhg637BwCJN7hStGWYXE":[],"14dHBBbwFVC7niSCqrb5HCHRK5K8rrgaW6":[],"14xsHuYGs4gKpRK3deuYwhMBTAwUeu2dpB":[],"15MpWMUasNVPTpzC5hK2AuVFwQ3AHd8fkv":[],"17nmvao3F84ebPrcv1LUxPUSS94U9EvCUt":[],"17yotEc8oUgJVQUnkjZSQjcqqZEbFFnXx8":[],"1A3c1rCCS2MYYobffyUHwPqkqE5ZpvG8Um":[],"1AtCzmcth79q6HgeyDnM3NLfr29hBHcfcg":[],"1AufJhUsMbqwbLK9JzUGQ9tTwphCQiVCwD":[],"1B77DkhJ8qHcwPQC2c1HyuNcYu5TzxxaJ7":[],"1D4bgjc4MDtEPWNTVfqG5bAodVu3D1Gjft":[],"1DefMPXdeCSQC5ieu8kR7hNGAXykNzWXpm":[],"1E673RESY1SvTWwUr5hQ1E7dGiRiSgkYFP":[],"1Ex6hnmpgp3FQrpR5aYvp9zpXemFiH7vky":[],"1FH2iAc5YgJKj1KcpJ1djuW3wJ2GbQezAv":[],"1GpjShJMGrLQGP6nZFDEswU7qUUgJbNRKi":[],"1H4BtV4Grfq2azQgHSNziN7MViQMDR9wxd":[],"1HnWq29dPuDRA7gx9HQLySGdwGWiNx4UP1":[],"1LMuebyhm8vnuw5qX3tqU2BhbacegeaFuE":[],"1LTJK8ffwJzRaNR5dDEKqJt6T8b4oVbaZx":[],"1LtXYvRr4j1WpLLA398nbmKhzhqq4abKi8":[],"1NfsUmibBxnuA3ir8GJvPUtY5czuiCfuYK":[],"1Q3cZjzADnnx5pcc1NN2ekJjLijNjXMXfr":[],"1okpBWorqo5WsBf5KmocsfhBCEDhNstW2":[]},"master_private_keys":{"x/":"xprv9s21ZrQH143K4D3WqM7zpQrWeqJHJRJhRhpkk5tr2fKBdoTTPDYUL88T12Ad9RHwViugcMbngkMDY626vD5syaFDoUB2cpLeraBaHvZHWFn"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGh7ywNf1BYoFCs8mht2YnvkMYUJTazrAWbnbvkrisvSvrKGjRTDtw324xzprbDgphsmPv2pB6K5Sux3YNHC8pnJANCBY6vG"},"pruned_txo":{},"seed":"tent alien genius panic stage below spoon swap merge hammer gorilla squeeze ability","seed_version":11,"stored_height":489715,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard","winpos-qt":[100,100,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_5_4_importedkeys(self): + async def test_upgrade_from_client_2_5_4_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported","winpos-qt":[595,261,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_5_4_watchaddresses(self): + async def test_upgrade_from_client_2_5_4_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported","winpos-qt":[406,393,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_5_4_trezor_singleacc(self): + async def test_upgrade_from_client_2_5_4_trezor_singleacc(self): wallet_str = '''{"accounts":{"0":{"change":["033608f89d381bcb9964df9da428d706d3eb30c14433af8de21bee2601e7392a80","0295c3905730d987ae9a9c09ad85c9c22c28aa414448f9d3450d8afb3da0d78890","038cf10bcf2bd3384f05974295fc83fc4e9cb48c0105995ad86d3ea237edb7e1d1","029b76e98f87c537165f016cf6840fe40c172ca0dba10278fb10e49a2b718cd156","034f08127c3651e5c5a65803e22dcbb1be10a90a79b699173ed0de82e0ceae862e","036013206a41aa6f782955b5a3b0e67f9a508ecd451796a2aa4ee7a02edef9fb7e"],"receiving":["020be78fa1a35e44fb1ee3141b40bd8d68330f12f98fdef5ba249b4d8c52a6a1ae","03f23e9a3b5337f322f720f533653349f6e97228d1c4a6feca36d4d1554aa19f74","03d3e7cfde0117561856e6e43d87852480c512910bfd1988c2ff1e6f6d795f7046","02ec56fc0bfe6a1466a783737919edbe83c8907af29a5ae672919ffcb1bb96303f","031b1d151f6584f9926614a7c335ee61606ff7a9769ca6e175ad99f9c7b5e9fb4d","03d782be0ace089e02529029b08ca9107b0e58302306de30bd9f9a3a1ed40c3717","0325784a4290eeeea1f99a928cf6c75c33417659dbd50a3a2850136dc3138ba631","035b7c1176926a54cdeb0342df5ecc7bb3fe1820fce99491fb50c091e3093f200f","02e0a2d615bff26a57754afa0e8ac8b692a79b399f6d04647398f377dcac4116be","026c7cee5bce1ae9e2fa930001ece81c35442a461fc9ef1266ac3d41b9f13e3bd5","0217b1d5066708e0cdaee99087c407db684131e34578adc7800dc66f329576c457","03ec0ed891b0ead00f1eaca7a4736d6816e348731d995bd4e77acbc8c582f68429","028cb4c682dde9692de47f71f3b16755cc440d722b84eed68db2b3d80bce83d50a","03d5d770a58d32b5d59b12861bbda37560fe7b789181b3349abf56223ea61b39c4","0250b6aee8338ac0497f2106b0ed014f5a2419c7bf429eb2b17a70bec77e6ff482","02565da9be6fc66a1e354638dcd8a4244e8733f38599c91c4f1ab0fb8d5d94fd2f","02e6c88509ff676b686afc2326370684bbc6edc0b31e09f312df4f7a17fe379e31","02224fef0921e61adcb2cd14ef45dbe4b859f1fcdc62eba26c6a7ce386c0a8f4b1","034c63da9c2a20132d9fd1088028de18f7ccd72458f9eb07a72452bd9994d28b1f","032bfe2fc88a55e19ba2338155b79e67b7d061d5fd1844bc8edc1808d998f8ba2c"],"xpub":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9"}},"accounts_expanded":{},"addr_history":{},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6D77dkWgEcSNBq7xDA1RUysGvD64QNy2TykC9UuRK6fEzqy3512HR2p2spstKCybkhDqkNStPWZKcnhwdD6kDYWJxsTQJhg9RCwifzcfJN9","x/1'":"xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG"},"next_account2":["1","xpub6D77dkWgEcSNFtXV2CQgsbfG33VyGMaUtUdpbdfMMHsS4WDzLtRapchQWcVBMFFjdRYjhkvQwGnJeKWPP3C2e1DevATAEUzL258Lhfkd7KG","03571f041921078b153a496638d703dfd1cee75e73c42653bbe0650ab6168d6a5b","18i2zqeCh6Gjto81KvVaeSM8YBUAkmgjRG"],"pruned_txo":{},"stored_height":490046,"transactions":{},"txi":{},"txo":{},"wallet_type":"trezor","winpos-qt":[522,328,840,400]}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_5_4_trezor_multiacc(self): + async def test_upgrade_from_client_2_5_4_trezor_multiacc(self): wallet_str = '''{"accounts":{"0":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"],"xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"1":{"change":["03b0df486b4e1baa03ad565622820d692089b059c8f9fefa3567c3fa26d0cbaa34","0294c76c062c865873dccab84d51682f880e0197b64789c61bff85e1be2506925e","036f900d0c6bafbbcac0fbc95bed44954007faa182655cf69dc84d50c22e6edce8","03d1be74f1360ecede61ad1a294b2e53d64d44def67848e407ec835f6639d825ff","03a6a526cfadd510a47da95b074be250f5bb659b857b8432a6d317e978994c30b7","022216da9e351ae57174f93a972b0b09d788f5b240b5d29985174fbd2119a981a9"],"receiving":["02106878f6aefd9a81e1ca4a5f30ea0e1851aa36404fb62d19bd2325e180112b58","039e95f369e8d65aa7a7bf6a5d7d3259b827c1549c77c9b502b75a18f7708a9aa9","0273197861097be131542f8b7e03bc912934da51bc957d425be5bc7c1b69fb44ec","02b4c829b6a20815c5e1eef7ffd5d55c99505a7afeac5135ec2c97cfaae3483604","0312b1285272f1005c5834de2eec830ce9f9163c842d728c3921ae790716d8503f","0354059948c709c777a49a37e150271a3377f7aaee17798253d5240e4119f2a1c6","03800d87cc3878912d22a42a79db7ddbff3efec727d29ae1c0165730e5314483cd","03cafa35ad9adb41cff39e3bc2e0592d88c8b91981e73f068397e6c863c42c7b00","028668f734a4927e03621e319ab385919e891d248c86aea07ab922492d3d414ad3","02e42d46823893978ae7be9e032be21ce3e613cecb5ffe687b534795f90dc8ef85","03b86914af797e7b68940bc4ee2dec134036781a8e23ffaf4189ca7637e0afe898","021221ae9be51a9747aa7ebc2213a42a2364ce790ee86255277dc5f9beeb0bf6b4","03c8d58183f5d8102f8eb5f6db0f60add0a51ec6737097c46fc8a6b7c840d7571f","0304de0806b299cef4be3a162bac78f811d4adacc6a229ffdaeb7333bce72d88ff","03e08262e18616a3a9b9aecbfb8a860ccee147820a3c60050695ef72ff2cedc4a7","02caf4d61bb5deec29a39e5a1cc6d5987ec71d61d57c57bb5c2a47dd9266130bec","0252d429002d9c06f0befbef6c389bdd021969b416dd83d220394e414bd5d83c0a","024e23ce58533163df3e1d5766295144beb8f9729b1ac41e80ba485f39c483dfe6","026de9e7e6b11fbecd88b7b49915b5df64d672ef900aa043a8cac3bc79eb414089","02aaac08fc100014ec692efa0f3b408bf741e1dc68ebe28ce41837662810f40986","03e0d2b426705dcc5cb62c6113b10153f10624c926a3fe86142fd9020e7d6a2129"],"xpub":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH"}},"accounts_expanded":{},"addr_history":{"12bBPWWDwvtXrR9ntSgaQ7AnGyVJr16m5q":[],"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[["a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837",490002]],"13853om3ye5c8x6K1LfT3uCWEnG14Z82ML":[],"13BGVmizH8fk3qNm1biNZxAaQY3vPwurjZ":[],"13Tvp2DLQFpUxvc7JxAD3TXfAUWvjhwUiL":[],"15EQcTGzduGXSaRihKy1FY99EQQco8k2UW":[],"15paDwtQ33jJmJhjoBJhpWYGJDFCZppEF9":[],"17X8K766zBYLTjSNvHB9hA6SWRPMTcT556":[],"17zSo4aveNaE5DiTmwNZtxrJmS5ymzvwqj":[],"19BRVkUFfrAcxW9poaBSEUA2yv7SwN3SXh":[],"19gPT2mb9FQCiiPdAmMAaberShzNRiAtTB":[],"1A3vopoUcrWn7JbiAzGZactQz8HbnC1MoD":[],"1D1bn2Jzcx4D2GXbxzrJ1GwP4eNq98Q948":[],"1DvytpRGLJujPtSLYTRABzpy2r6hKJBYQd":[],"1EGg2acXNhJfv1bU3ixrbrmgxFtAUWpdY":[],"1Ev3S9YWxS7KWT8kyLmEuKV5sexNKcMUKV":[],"1FfpRnukxbfBnoudWvw9sdmc86YbVs7eGb":[],"1GBxNE82WLgd38CzoFTEkz6QS9EwLj1ym7":[],"1JFDe97zENNUiKeizcFUHss13vS2AcrVdE":[],"1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ":[],"1JQqX3yg6VYxL6unuRArDQaBZYo3ktSCCP":[],"1JUbrr4grE71ZgWNqm9z9ZHHJDcCzFYM4V":[],"1JuHUVbYfBLDUhTHx5tkDDyDbCnMsF8C9w":[],"1KZu7p244ETkdB5turRP4vhG2QJskARYWS":[],"1LE7jioE7y24m3MMZayRKpvdCy2Dz2LQae":[],"1LVr2pTU7LPQu8o8DqsxcGrvwu5rZADxfi":[],"1LmugnVryiuMbgdUAv3LucnRMLvqg8AstU":[],"1MPN5vptDZCXc11fZjpW1pvAgUZ5Ksh3ky":[]},"labels":{"0":"Main account"},"master_public_keys":{"x/0'":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y","x/1'":"xpub6BycoSLDNcWjFs4B6T82q4zCbJBJdzQLwburAtBAwTLPyDPtkotGUWbef1t8D6XuCs6Yz5FUgFaL2hNzCTGe8F1bf9vNyXFMgLyKV65C9BH","x/2'":"xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa"},"next_account2":["2","xpub6BycoSLDNcWjHWrJyJJYmq9dDwBxSkFbWeaFFcrB6zBH9JTvyRVbAoWcmbPRmxicUkiutGQWqfsom9CbKSVG8Zh5HqHyR25xHE1xxmHeNYa","031b68cff8114df7677c4fe80619b701ea966428ecbeba55c9224cd8149cc5f05e","1JGek3B8b3Nt3p39x27QK5UnFtNnZ2ZdGJ"],"pruned_txo":{},"stored_height":490009,"transactions":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":"01000000018394dfaba83ca6f510f622ecf95b445e856eab3193cb0dad53e1262841149d5f00000000da0047304402207761cdbf009c0bd3864c6a457288cadfa565601f782cc09f0046926d54a1b68b022060b73a7babb5dfd5188c4697cfcab6c15c4dd3de8507d39722e3a6b728f697dc01483045022100a540921229b02c4cfbf2d57222a455cbb4a5bd09bff063749fb71292f720850a02204dd18369213ec4cb033cbf222e8439eb8a9dd0a1b864bfeefa44cfe0c0066ee401475221025966a0193194a071e71501f9f8987111f7364bd8105a006f908b1f743da8d353210397c83f4963bdf333f129ab8000d89536bfea0971fc7578fdff5c2104b296c4d252aefdffffff0288130000000000001976a9141516b5e9653ab1fb09180186077fc2d7dfa07e5788aca0ba09000000000017a9148132c19d6b9abba9ec978ca5269d577ae104541e8700000000"},"txi":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{}},"txo":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":{"12vWPzJtGLKRZjnYVtWSufjRuoE8pHLpmi":[[0,5000,false]]}},"verified_tx3":{"a242aeff746aa481c5d8a496111039262f2a3fbde6038124301522539fa06837":[490002,1508090436,607]},"wallet_type":"trezor","winpos-qt":[757,469,840,400]}''' - self._upgrade_storage(wallet_str, accounts=2) + await self._upgrade_storage(wallet_str, accounts=2) - def test_upgrade_from_client_2_5_4_multisig(self): + async def test_upgrade_from_client_2_5_4_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["02a63209b49df0bb98d8a262e9891fe266ffdce4be09d5e1ffaf269a10d7e7a17c","02a074035006ed8ee8f200859c004c073b687140f7d40bd333cdbbe43bad1e50bc"],["0280e2367142669e08e27fb9fd476076a7f34f596e130af761aef54ec54954a64d","02719a66c59f76c36921cf7b330fca7aaa4d863ee367828e7d89cd2f1aad98c3ac"],["0332083e80df509d3bd8a06538ca20030086c9ed3313300f7313ed98421482020f","032f336744f53843d8a007990fa909e35e42e1e32460fae2e0fc1aef7c2cff2180"],["03fe014e5816497f9e27d26ce3ae8d374edadec410227b2351e9e65eb4c5d32ab7","0226edd8c3af9e339631145fd8a9f6d321fdc52fe0dc8e30503541c348399dd52a"],["03e6717b18d7cbe264c6f5d0ad80f915163f6f6c08c121ac144a7664b95aedfdf3","03d69a074eba3bc2c1c7b1f6f85822be39aee20341923e406c2b445c255545394a"],["023112f87a5b9b2eadc73b8d5657c137b50609cd83f128d130172a0ed9e3fea9bc","029a81fd5ba57a2c2c6cfbcb34f369d87af8759b66364d5411eddd28e8a65f67fa"]],"m":2,"receiving":[["03c35c3da2c864ee3192a847ffd3f67fa59c095d8c2c0f182ed9556308ec37231e","03cfcb6d1774bfd916bd261232645f6c765da3401bf794ab74e84a6931d8318786"],["03973c83f84a4cf5d7b21d1e8b29d6cbd4cb40d7460166835cd1e1fd2418cfcf2e","03596801e66976959ac1bdb4025d65a412d95d320ed9d1280ac3e89b041e663cf4"],["02b78ac89bfdf90559f24313d7393af272092827efc33ba3a0d716ee8b75fd08ff","038e21fae8a033459e15a700551c1980131eb555bbb8b23774f8851aa10dcac6b8"],["0288e9695bb24f336421d5dcf16efb799e7d1f8284413fe08e9569588bc116567e","027123ba3314f77a8eb8bb57ba1015dd6d61b709420f6a3320ba4571b728ef2d91"],["0312e1483f7f558aef1a14728cc125bb4ee5cff0e7fa916ba8edd25e3ebceb05e9","02dad92a9893ad95d3be5ebc40828cef080e4317e3a47af732127c3fee41451356"],["03a694e428a74d37194edc9e231e68399767fdb38a20eca7b72caf81b7414916a8","03129a0cef4ed428031972050f00682974b3d9f30a571dc3917377595923ac41d8"],["026ed41491a6d0fb3507f3ca7de7fb2fbfdfb28463ae2b91f2ab782830d8d5b32c","03211b3c30c41d54734b3f13b8c9354dac238d82d012839ee0199b2493d7e7b6fc"],["03480e87ffa55a96596be0af1d97bca86987741eb5809675952a854d59f5e8adc2","0215f04df467d411e2a9ed8883a21860071ab721314503019a10ed30e225e522e7"],["0389fce63841e9231d5890b1a0c19479f8f40f4f463ef8e54ef306641abe545ac8","02396961d498c2dcb3c7081b50c5a4df15fda31300285a4c779a59c9abc98ea20d"],["03d4a3053e9e08dc21a334106b5f7d9ac93e42c9251ceb136b83f1a614925eb1fb","025533963c22b4f5fbfe75e6ee5ad7ee1c7bff113155a7695a408049e0b16f1c52"],["038a07c8d2024b9118651474bd881527e8b9eb85fc90fdcb04c1e38688d498de4b","03164b188eb06a3ea96039047d0db1c8f9be34bfd454e35471b1c2f429acd40afb"],["0214070cd393f39c062ce1e982a8225e5548dbbbd654aeba6d36bfcc7a685c7b12","029c6a9fb61705cc39bef34b09c684a362d4862b16a3b0b39ca4f94d75cd72290c"],["027b3497f72f581fea0a678bc20482b6fc7b4b507f7263d588001d73fdf5fe314e","021b80b159d19b6978a41c2a6bf7d3448bc73001885f933f7854f450b5873091f3"],["0303e9d76e4fe7336397c760f6fdfd5fb7500f83e491efb604fa2442db6e1da417","03a8d1b22a73d4c181aecd8cfe8bb2ee30c5dd386249d2a5a3b071b7a25b9da73a"],["0298e472b74832af856fb68eed02ff00a235fd0424d833bc305613e9f44087d0ee","03bb9bc2e4aaa9b022b35c8d122dfccb6c28ae8f0996a8fb4a021af8ec96a7beaf"],["02e933a4afb354500da03373514247e1be12e67cc4683e0cb82f508878cc3cc048","02c07a57b071bc449a95dd80308e53b26e4ebf4d523f620eecb17f96ae3aa814e9"],["03f73476951078b3ccc549bc7e6362797aaaacb1ea0edc81404b4d16cb321255a3","03b3a825fb9fc497e568fba69f70e2c3dcdc793637e242fce578546fcbd33cb312"],["03bbdf99fddeea64a96bbb9d1e6d7ced571c9c7757045dcbd8c40137125b017dc5","03aedf4452afefb1c3da25e698f621cb3a3a0130aa299488e018b93a45b5e6c21d"],["03b85891edb147d43c0a5935a20d6bbf8d32c542bfecccf3ae0158b65bd639b34e","03b34713c636a1c103b82d6cec917d442c59522ddc5a60bf7412266dd9790e7760"],["028ddf53b85f6c01122a96bd6c181ee17ca222ee9eca85bdeeb25c4b5315005e3b","02f4821995bfd5d0adb7a78d6e3a967ac72ace9d9a4f9392aff2711533893e017b"]],"xpubs":["xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b","xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp"]}},"accounts_expanded":{},"addr_history":{"32JvbwfEGJwZHGm3nwYiXyfsnGCb3L8hMX":[],"32pWy5sKkQsjyDz45tog47cA8vQyzC3UUZ":[],"334yqX1WtS6mY2vize7znTaL64HspwVkGF":[],"33GY9w6a4XmLAWxNgNFFRXTTRxbu3Nz8ip":[],"33geBcyW8Bw53EgAv3qwMVkVnvxZWj5J1X":[],"35BneogkCNxSiSN1YLmhKLP8giDbGkZiTX":[],"37U4J5b9B7rQnQXYstMoQnb6i9aWpptnLi":[],"37gqbHdbrCcGyrNF21AiDkofVCie5LpFmQ":[],"37t1Q5R92co4by2aagtLcqdWTDEzFuAuwZ":[],"37z3ruAHCxnzeJeLz96ZpkbwS3CLbtXtPc":[],"39qePsKaeviFEMC6CWX37DqaQda4jA2E6A":[],"3A5eratrDWu4SqsoHpuqswNsQmp9k8TXR2":[],"3B1N3PG5dNPYsTAuHFbVfkwXeZqqNS1CuP":[],"3BABbvd3eAuwiqJwppm54dJauKnRUieQU8":[],"3CAsH7BJnNT4kmwrbG8XZMMwW6ue8w4auJ":[],"3CX2GLCTfpFHSgAmbGRmuDKGHMbWY8tCp7":[],"3CrLUTVHuG1Y3swny9YDmkfJ89iHHU93NB":[],"3CxRa6yAQ2N2rpDHyUTaViGG4XVASAqwAN":[],"3DLTrsdYabso7QpxoLSW5ZFjLxBwrLEqqW":[],"3GG3APgrdDCTmC9tTwWu3sNV9aAnpFcddA":[],"3JDWpTxnsKoKut9WdG4k933qmPE5iJ8hRR":[],"3LdHoahj7rHRrQVe38D4iN43ySBpW5HQRZ":[],"3Lt56BqiJwZ1um1FtXJXzbY5uk32GVBa8K":[],"3MM9417myjN7ubMDkaK1wQ9RbjEc1zHCRH":[],"3NTivFVXva4DCjPmsf5p5Gt1dmuV39qD2v":[],"3QCwtjMywMtT3Vg6BwS146LcQjJnZPAPHZ":[]},"master_private_keys":{"x1/":"xprv9s21ZrQH143K29YeVxd7jCexomdRiuw8UPSnHbbrAecbrQ6FgTKPyVcZqp2256L5DSTdb8UepPVaDwJecswTrEhdyZiaNGERJpfzWV5FcN5"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcEdd7bzA86LbhMoTv8NeyqcNP5z1Tiz9ajCRQDzdeXHw3h5ucDNGWr6mPFCZBcBE31VNKyR3vWM7WEeisu5m4VsCyuA6H8fp","x2/":"xpub661MyMwAqRbcGHtCYBSGGVgMSihroMkuyE25GPyzfQvS2vSFG7SgJYf7rtXJjMh7srBJj8WddLtjapHnUQLwJ7kxsy5HiNZnGvF9pm2du7b"},"pruned_txo":{},"seed":"park dash merit trend life field acid wrap dinosaur kit bar hotel abuse","seed_version":11,"stored_height":490034,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2","winpos-qt":[564,329,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_6_4_seeded(self): + async def test_upgrade_from_client_2_6_4_seeded(self): wallet_str = '{"accounts":{"0":{"change":["03236a8ce6fd3d343358f92d3686b33fd6e7301bf9f635e94c21825780ab79c93d","0393e39f6b4a3651013fca3352b89f1ae31751d4268603f1423c71ff79cbb453a1","033d9722ecf50846527037295736708b20857b4dd7032fc02317f9780d6715e8ff","03f1d56d2ade1daae5706ea945cab2af719060a955c8ad78153693d8d08ed6b456","029260d935322dd3188c3c6b03a7b82e174f11ca7b4d332521740c842c34649137","0266e8431b49f129b892273ab4c8834a19c6432d5ed0a72f6e88be8c629c731ede"],"receiving":["0350f41cfac3fa92310bb4f36e4c9d45ec39f227a0c6e7555748dff17e7a127f67","02f997d3ed0e460961cdfa91dec4fa09f6a7217b2b14c91ed71d208375914782ba","029a498e2457744c02f4786ac5f0887619505c1dae99de24cf500407089d523414","03b15b06044de7935a0c1486566f0459f5e66c627b57d2cda14b418e8b9017aca1","026e9c73bdf2160630720baa3da2611b6e34044ad52519614d264fbf4adc5c229a","0205184703b5a8df9ae622ea0e8326134cbeb92e1f252698bc617c9598aff395a1","02af55f9af0e46631cb7fde6d1df6715dc6018df51c2370932507e3d6d41c19eec","0374e0c89aa4ecf1816f374f6de8750b9c6648d67fe0316a887a132c608af5e7c0","0321bb62f5b5c393aa82750c5512703e39f4824f4c487d1dc130f690360c0e5847","0338ea6ebb2ed80445f64b2094b290c81d0e085e6000367eb64b1dc5049f11c2e9","020c3371a9fd283977699c44a205621dea8abfc8ebc52692a590c60e22202fa49b","0395555e4646f94b10af7d9bc57e1816895ad2deddef9d93242d6d342cea3d753b","02ffa4495d020d17b54da83eaf8fbe489d81995577021ade3a340a39f5a0e2d45c","030f0e16b2d55c3b40b64835f87ab923d58bcdbb1195fadc2f05b6714d9331e837","02f70041fc4b1155785784a7c23f35d5d6490e300a7dd5b7053f88135fc1f14dfd","03b39508c6f9c7b8c3fb8a1b91e61a0850c3ac76ccd1a53fbc5b853a94979cffa8","03b02aa869aa14b0ec03c4935cc12f221c3f204f44d64146d468e07370c040bfe7","02b7d246a721e150aaf0e0e60a30ad562a32ef76a450101f3f772fef4d92b212d9","037cd5271b31466a75321d7c9e16f995fd0a2b320989c14bee82e161c83c714321","03d4ad77e15be312b29987630734d27ca6e9ee418faa6a8d6a50581eca40662829"],"xpub":"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE"}},"accounts_expanded":{},"addr_history":{"12qKnKuhCZ1Q9XBi1N6SnxYEUtb5XZXuY5":[],"1321ddunxShHmF4cjh3v5yqR7uatvSNndK":[],"13Ji3kGWn9qxLcWGhd46xjV6hg8SRw8x2P":[],"145q5ZDXuFi6v9dA2t8HyD8ysorfb81NRt":[],"14gB2wLy2DMkBVtuU6HHP3kQYNFYPzAguU":[],"16VGRwtZwp4yapQN5fS8CprK6mmnEicCEj":[],"16ahKVzCviRi24rwkoKgiSVSkvRNiQudE1":[],"16wjKZ1CWAMEzSR4UxQTWqXRm9jcJ9Dbuf":[],"18ReWGJBq1XkJaPAirVdT6RqDskcFeD5Ho":[],"1A1ECMMJU4NicWNwfMBn3XJriB4WHAcPUC":[],"1Bvxbfc2wXB8z8kyz2uyKw2Ps8JeGQM9FP":[],"1EDWUz4kPq8ZbCdQq8rLhFc3qSZ6Fpt1TD":[],"1EsvTarawMm5BfF44hpRtE4GfZFfZZ1JG3":[],"1JgaekD2ETMJm6oRNnwTWRK9ZxXeUcbi18":[],"1KHdLodsSWj1LrrD9d1RbApfqzpxRs5sxu":[],"1KgGwpKhruHWpMNtrpRExDWLLk5qHCHBdg":[],"1LFf8d3XD9atZvMVMAiq9ygaeZbphbKzSo":[],"1N3XncDQsWE2qff1EVyQEmR6JLLzD3mEL7":[],"1NUtLcVQNmY5TJCieM1cUmBmv18AafY1vq":[],"1NYFsm7PpneT65byRtm8niyvtzKsbEeuXA":[],"1NvEcSvfCe8LPvPkK4ZxhjzaUncTPqe9jX":[],"1PV8xdkYKxeMpnzeeA4eYEpL24j1G9ApV2":[],"1PdiGtznaW1mok6ETffeRvPP5f4ekBRAfq":[],"1QApNe4DtK7HAbJrn5kYkYxZMt86U5ChSb":[],"1QnH7F6RBXFe7LtszQ6KTRUPkQKRtXTnm":[],"1ekukhMNSWCfnRsmpkuTRuLMbz6cstkrq":[]},"master_private_keys":{"x/":"xprv9s21ZrQH143K4TCkhu7bE82GbtTB6ZUzXkjRfBu8ccAGe51Q7jyJ4QTsGbWxpHxnatKeYV7Ad83m7KC81THBm2xmyxA1q8BuuRXSGnmhhR8"},"master_public_keys":{"x/":"xpub661MyMwAqRbcGwHDovebbFy19vHfW2Cqtyf2TaJkAwhFWsLYfHHYcCnM7smpvntxJP1YMVT5triFbWiCGXPRPhqdCxFumA77MuQB1CeWHpE"},"pruned_txo":{},"seed":"heart cabbage scout rely square census satoshi home purpose legal replace move able","seed_version":11,"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"standard","winpos-qt":[582,394,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_6_4_importedkeys(self): + async def test_upgrade_from_client_2_6_4_importedkeys(self): wallet_str = '{"accounts":{"/x":{"imported":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM"],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":["04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2","5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":["0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U"]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":489716,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"imported","winpos-qt":[510,338,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_6_4_watchaddresses(self): + async def test_upgrade_from_client_2_6_4_watchaddresses(self): wallet_str = '{"accounts":{"/x":{"imported":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[null,null],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[null,null],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[null,null]}}},"accounts_expanded":{},"addr_history":{},"pruned_txo":{},"stored_height":490038,"transactions":{},"txi":{},"txo":{},"wallet_type":"imported","winpos-qt":[582,425,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_6_4_multisig(self): + async def test_upgrade_from_client_2_6_4_multisig(self): wallet_str = '{"accounts":{"0":{"change":[["03d0bcdc86a64cc2024c84853e88985f6f30d3dc3f219b432680c338a3996a89ed","024f326d48aa0a62310590b10522b69d250a2439544aa4dc496f7ba6351e6ebbfe"],["03c0416928528a9aaaee558590447ee63fd33fa497deebefcf363b1af90d867762","03db7de16cd6f3dcd0329a088382652bc3e6b21ee1a732dd9655e192c887ed88a7"],["0291790656844c9d9c24daa344c0b426089eadd3952935c58ce6efe00ef1369828","02c2a5493893643102f77f91cba709f11aaab3e247863311d6fc3d3fc82624c3cc"],["023dc976bd1410a7e9f34c230051db58a3f487763f00df1f529b10f55ee85b931c","036c318a7530eedf3584fd8b24c4024656508e35057a0e7654f21e89e121d0bd30"],["02c8820711b39272e9730a1c5c5c78fe39a642b8097f8724b2592cc987017680ce","0380e3ebe0ea075e33acb3f796ad6548fde86d37c62fe8e4f6ab5d2073c1bb1d43"],["0369a32ddd213677a0509c85af514537d5ee04c68114da3bc720faeb3adb45e6f8","0370e85ac01af5e3fd5a5c3969c8bca3e4fc24efb9f82d34d5790e718a507cecb6"]],"m":2,"receiving":[["0207739a9ff4a643e1d4adb03736ec43d13ec897bdff76b40a25d3a16e19e464aa","02372ea4a291aeb1fadb26f36976348fc169fc70514797e53b789a87c9b27cc568"],["0248ae7671882ec87dd6bacf7eb2ff078558456cf5753952cddb5dde08f471f3d6","035bac54828b383545d7b70824a8be2f2d9584f656bfdc680298a38e9383ed9e51"],["02cb99ba41dfbd510cd25491c12bd0875fe8155b5a6694ab781b42bd949252ff26","03b520feba42149947f8b2bbc7e8c03f9376521f20ac7b7f122dd44ab27309d7c6"],["0395902d5ebb4905edd7c4aedecf17be0675a2ffeb27d85af25451659c05cc5198","02b4a01d4bd25cadcbf49900005e8d5060ed9cdc35eb33f2cd65cc45cc7ebc00c5"],["02f9d06c136f05acc94e4572399f17238bb56fa15271e3cb816ae7bb9be24b00b6","035516437612574b2b563929c49308911651205e7cebb621940742e570518f1c50"],["0376a7de3abaee6631bd4441658987c27e0c7eee2190a86d44841ae718a014ee43","03cb702364ffd59cb92b2e2128c18d8a5a255be2b95eb950641c5f17a5a900eecb"],["03240c5e868ecb02c4879ae5f5bad809439fdbd2825769d75be188e34f6e533a67","026b0d05784e4b4c8193443ce60bea162eee4d99f9dfa94a53ae3bc046a8574eeb"],["02d087cccb7dc457074aa9decc04de5a080757493c6aa12fa5d7d3d389cfdb5b8e","0293ab7d0d8bbb2d433e7521a1100a08d75a32a02be941f731d5809b22d86edb33"],["03d1b83ab13c5b35701129bed42c1f1fbe86dd503181ad66af3f4fb729f46a277e","0382ec5e920bc5c60afa6775952760668af42b67d36d369cd0e9acc17e6d0a930d"],["03f1737db45f3a42aebd813776f179d5724fce9985e715feb54d836020b8517bfe","0287a9dfb8ee2adab81ef98d52acd27c25f558d2a888539f7d583ef8c00c34d6dc"],["038eb8804e433023324c1d439cd5fbbd641ca85eadcfc5a8b038cb833a755dac21","0361a7c80f0d9483c416bc63d62506c3c8d34f6233b6d100bb43b6fe8ec39388b9"],["0336437ada4cd35bec65469afce298fe49e846085949d93ef59bf77e1a1d804e4a","0321898ed89df11fcfb1be44bb326e4bb3272464f000a9e51fb21d25548619d377"],["0260f0e59d6a80c49314d5b5b857d1df64d474aba48a37c95322292786397f3dc6","03acd6c9aeac54c9510304c2c97b7e206bbf5320c1e268a2757d400356a30c627b"],["0373dc423d6ee57fac3b9de5e2b87cf36c21f2469f17f32f5496e9e7454598ba8e","031ddc1f40c8b8bf68117e790e2d18675b57166e9521dff1da44ba368be76555b3"],["031878b39bc6e35b33ceac396b429babd02d15632e4a926be0220ccbd710c7d7b9","025a71cc5009ae07e3e991f78212e99dd5be7adf941766d011197f331ce8c1bed0"],["032d3b42ed4913a134145f004cf105b66ae97a9914c35fb73d37170d37271acfcd","0322adeb83151937ddcd32d5bf2d3ed07c245811d0f7152716f82120f21fb25426"],["0312759ff0441c59cb477b5ec1b22e76a794cd821c13b8900d72e34e9848f088c2","02d868626604046887d128388e86c595483085f86a395d68920e244013b544ef3b"],["038c4d5f49ab08be619d4fed7161c339ea37317f92d36d4b3487f7934794b79df4","03f4afb40ae7f4a886f9b469a81168ad549ad341390ff91ebf043c4e4bfa05ecc1"],["02378b36e9f84ba387f0605a738288c159a5c277bbea2ea70191ade359bc597dbb","029fd6f0ee075a08308c0ccda7ace4ad9107573d2def988c2e207ac1d69df13355"],["02cfecde7f415b0931fc1ec06055ff127e9c3bec82af5e3affb15191bf995ffc1a","02abb7481504173a7aa1b9860915ef62d09a323425f680d71746be6516f0bb4acf"]],"xpubs":["xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV","xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW"]}},"accounts_expanded":{},"addr_history":{"329Ju5tiAr4vHZExAT4KydYEkfKiHraY2N":[],"32HJ13iTVh3sCWyXzipcGb1e78ZxcHrQ7v":[],"32cAdiAapUzNVRYXmDud5J5vEDcGsPHjD8":[],"33fKLmoCo8oFfeV987P6KrNTghSHjJM251":[],"34cE6ZcgXvHEyKbEP2Jpz5C3aEWhvPoPG2":[],"36xsnTKKBojYRHEApVR6bCFbDLp9oqNAxU":[],"372PG6D3chr8tWF3J811dKSpPS84MPU6SE":[],"378nVF8daT4r3jfX1ebKRheUVZX5zaa9wd":[],"392ZtXKp2THrk5VtbandXxFLB8yr2g14aA":[],"39cCrU3Zz3SsHiQUDiyPS1Qd5ZL3Rh1GhQ":[],"3A2cRoBdem5tdRjq514Pp7ZvaxydgZiaNG":[],"3Ceoi3MKdh2xiziHDAzmriwjDx4dvxxLzm":[],"3FcXdG8mh1YeQCYVib8Aw7zwnKpComimLH":[],"3J4b31yAbQkKhejSW7Qz54qNJDEy3t9uSe":[],"3JpJrSxE1GP1X5h82zvLA2TbMZ8nUsGW6z":[],"3K1dzpbcop1MotuqyFQyEuXbvQehaKnGVM":[],"3L8Us8SN22Hj6GnZPRCLaowA1ZtbptXxxL":[],"3LANyoJyShQ8w55tvopoGiZ2BTVjLfChiP":[],"3LoJGQdXTzVaDYudUguP4jNJYy4gNDaRpN":[],"3MD8jVH7Crp5ucFomDnWqB6kQrEQ9VF5xv":[],"3ME8DemkFJSn2tHS23yuk2WfaMP86rd3s7":[],"3MFNr17oSZpFtH16hGPgXz2em2hJkd3SZn":[],"3QHRTYnW2HWCWoeisVcy3xsAFC5xb6UYAK":[],"3QKwygVezHFBthudRUh8V7wwtWjZk3whpB":[],"3QNPY3dznFwRv6VMcKgmn8FGJdsuSRRjco":[],"3QNwwD8dp6kvS8Fys4ZxVJYZAwCXdXQBKo":[]},"master_private_keys":{"x1/":"xprv9s21ZrQH143K3oPcB2UmMA6Cy9W49HLyW6CDNhQuRcn7tGu1tQ2bn6TLw8HFWbu5oP38Z2fFCo5Q4n3fog4DTqywYqfSDWhYbDgVD1TGZoP"},"master_public_keys":{"x1/":"xpub661MyMwAqRbcGHU5H41miJ2wXBLYYk4psK7pB5pWyxK6m5EARwLrKtmpnMzP52qGsKZEtjJCyohVEaZTFXbohjVdfpDFifgMBT82EvkFpsW","x2/":"xpub661MyMwAqRbcF4mZnFnBRYGBaiD9aQRp9w2jaPUrDg3Eery5gywV7eFMzQKmNyY1W4m4fUwsinMw1tFhMNEZ9KjNtkUSBHPXdcXBwCg5ctV"},"pruned_txo":{},"seed":"turkey weapon legend tower style multiply tomorrow wet like frame leave cash achieve","seed_version":11,"stored_height":490035,"transactions":{},"txi":{},"txo":{},"use_encryption":false,"wallet_type":"2of2","winpos-qt":[610,418,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_7_18_seeded(self): + async def test_upgrade_from_client_2_7_18_seeded(self): wallet_str = '{"addr_history":{"12nzqpb4vxiFmcvypswSWK1f4cvGwhYAE8":[],"13sapXcP5Wq25PiXh5Zr9mLhyjdfrppWyi":[],"14EzC5y5eFCXg4T7cH4hXoivzysEpGXBTM":[],"15PUQBi2eEzprCZrS8dkfXuoNv8TuqwoBm":[],"16NvXzjxHbiNAULoRRTBjSmecMgF87FAtb":[],"16oyPjLM4R96aZCnSHqBBkDMgbE2ehDWFe":[],"1BfhL8ZPcaZkXTZKASQYcFJsPfXNwCwVMV":[],"1Bn3vun14mDWBDkx4PvK2SyWK1nqB9MSmM":[],"1BrCEnhf763JhVNcZsjGcNmmisBfRkrdcn":[],"1BvXCwXAdaSTES4ENALv3Tw6TJcZbMzu5o":[],"1C2vzgDyPqtvzFRYUgavoLvk3KGujkUUjg":[],"1CN22zUHuX5SxGTmGvPTa2X6qiCJZjDUAW":[],"1CUT9Su42c4MFxrfbrouoniuhVuvRjsKYS":[],"1DLaXDPng4wWXW7AdDG3cLkuKXgEUpjFHq":[],"1DTLcXN6xPUVXP1ZQmt2heXe2KHDSdvRNv":[],"1F1zYJag8yXVnDgGGy7waQT3Sdyp7wLZm3":[],"1Fim67c46NHTcSUu329uF8brTmkoiz6Ej8":[],"1Go6JcgkfZuA7fyQFKuLddee9hzpo31uvL":[],"1J6mhetXo9Eokq7NGjwbKnHryxUCpgbCDn":[],"1K9sFmS7qM2P5JpVGQhHMqQgAnNiujS5jZ":[],"1KBdFn9tGPYEqXnHyJAHxBfCQFF9v3mq95":[],"1LRWRLWHE2pdMviVeTeJBa8nFbUTWSCvrg":[],"1LpXAktoSKbRx7QFkyb2KkSNJXSGLtTg9T":[],"1LtxCQLTqD1q5Q5BReP932t5D7pKx5wiap":[],"1MX5AS3pA5jBhmg4DDuDQEuNhPGS4cGU4F":[],"1Pz9bYFMeqZkXahx9yPjXtJwL69zB3xCp2":[]},"keystore":{"seed":"giraffe tuition frog desk airport rural since dizzy regular victory mind coconut","type":"bip32","xprv":"xprv9s21ZrQH143K28Jvnpm7hU3xPt18neaDpcpoMKTyi9ewNRg6puJ2RAE5gZNPQ73bbmU9WsagxLQ3a6i2t1M9W289HY9Q5sEzFsLaYq3ZQf3","xpub":"xpub661MyMwAqRbcEcPPtrJ84bzgwuqdC7J5BqkQ9hsbGVBvFE1FNScGxxYZXpC9ncowEe7EZVbAerSypw3wCjrmLmsHeG3RzySw5iEJhAfZaZT"},"pruned_txo":{},"pubkeys":{"change":["033e860b0823ed2bf143594b07031d9d95d35f6e4ad6093ddc3071b8d2760f133f","03f51e8798a1a46266dee899bada3e1517a7a57a8402deeef30300a8918c81889a","0308168b05810f62e3d08c61e3c545ccbdce9af603adbdf23dcc366c47f1c5634c","03d7eddff48be72310347efa93f6022ac261cc33ee0704cdad7b6e376e9f90f574","0287e34a1d3fd51efdc83f946f2060f13065e39e587c347b65a579b95ef2307d45","02df34e258a320a11590eca5f0cb0246110399de28186011e8398ce99dd806854a"],"receiving":["031082ff400cbe517cc2ae37492a6811d129b8fb0a8c6bd083313f234e221527ae","03fac4d7402c0d8b290423a05e09a323b51afebd4b5917964ba115f48ab280ef07","03c0a8c4ab604634256d3cfa350c4b6ca294a4374193055195a46626a6adea920f","03b0bc3112231a9bea6f5382f4324f23b4e2deb5f01a90b0fe006b816367e43958","03a59c08c8e2d66523c888416e89fa1aaec679f7043aa5a9145925c7a80568e752","0346fefc07ab2f38b16c8d979a8ffe05bc9f31dd33291b4130797fa7d78f6e4a35","025eb34724546b3c6db2ee8b59fbc4731bafadac5df51bd9bbb20b456d550ef56e","02b79c26e2eac48401d8a278c63eec84dc5bef7a71fa7ce01a6e333902495272e2","03a3a212462a2b12dc33a89a3e85684f3a02a647db3d7eaae18c029a6277c4f8ac","02d13fc5b57c4d057accf42cc918912221c528907a1474b2c6e1b9ca24c9655c1a","023c87c3ca86f25c282d9e6b8583b0856a4888f46666b413622d72baad90a25221","030710e320e9911ebfc89a6b377a5c2e5ae0ab16b9a3df54baa9dbd3eb710bf03c","03406b5199d34be50725db2fcd440e487d13d1f7611e604db81bb06cdd9077ffa5","0378139461735db84ff4d838eb408b9c124e556cfb6bac571ed6b2d0ec671abd0c","030538379532c476f664d8795c0d8e5d29aea924d964c685ea5c2343087f055a82","02d1b93fa37b824b4842c46ef36e5c50aadbac024a6f066b482be382bec6b41e5a","02d64e92d12666cde831eb21e00079ecfc3c4f64728415cc38f899aca32f1a5558","0347480bf4d321f5dce2fcd496598fbdce19825de6ed5b06f602d66de7155ac1c0","03242e3dfd8c4b6947b0fbb0b314620c0c3758600bb842f0848f991e9a2520a81c","021acadf6300cb7f2cca11c6e1c7e59e3cf923a786f6371c3b85dd6f8b65c68470"]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[709,314,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_7_18_importedkeys(self): + async def test_upgrade_from_client_2_7_18_importedkeys(self): wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"pubkeys":{"change":[],"receiving":["0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2"]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[420,312,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_7_18_watchaddresses(self): + async def test_upgrade_from_client_2_7_18_watchaddresses(self): wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[553,402,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_7_18_trezor_singleacc(self): + async def test_upgrade_from_client_2_7_18_trezor_singleacc(self): wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"pubkeys":{"change":["03143bc04f007c454e03caf9d59b61e27f527b5e6723e167b50197ce45e2071902","03157710459a8213a79060e2f2003fe0eb7a7ed173ac3f846309de52269dd44740","028ec4bbbf4ac9edfabb704bd82acb0840f2166312929ce01af2b2e99059b16dee","021a9f1201968bd835029daf09ae98745a75bcb8c6143b80610cfc2eb2eee94dd8","031fe8323703fee4a1f6c59f27ceed4e227f5643b1cb387b39619b6b5499a971b4","033199fc62b72ce98e3780684e993f31d520f1da0bf2880ed26153b2efcc86ac1d"],"receiving":["03d27c0f5594d8df0616d64a735c909098eb867d01c6f1588f04ca2cf353837ec0","024d299f21e9ee9cc3eb425d04f45110eff46e45abcab24a3e594645860518fb97","03f6bc650e5f118ab4a63359a9cde4ab8382fe16e7d1b36b0a459145a79bef674b","028bed00a2fbd03f1ff43e0150ec1018458f7b39f3e4e602e089b1f47f8f607136","02108b15014d53f2e4e1b5b2d8f5eaf82006bbc4f273dbfbaef91eff08f9d10ea5","02a9a59a529818f3ba7a37ebe34454eac2bcbe4da0e8566b13f369e03bb020c4c4","023fde4ecf7fbdffb679d92f58381066cf2d840d34cb2d8bef63f7c5182d278d53","02ad8bf6dc0ff3c39bd20297d77fbd62073d7bf2fa44bf716cdd026db0819bb2b4","029c8352118800beaef1f3fa9c12afe30d329e7544bea9b136b717b88c24d95d92","02c42c251392674e2c2768ccd6224e04298bd5479436f02e9867ecc288dd2eb066","0316f3c82d9fce97e267b82147d56a4b170d39e6cf01bfaff6c2ae6bcc79a14447","0398554ee8e333061391b3e866505bbc5e130304ae09b198444bcd31c4ba7846ea","02e69d21aadb502e9bd93c5536866eff8ca6b19664545ccc4e77f3508e0cbe2027","0270fb334407a53a23ad449389e2cb055fae5017ca4d79ec8e082038db2d749c50","03d91a8f47453f9da51e0194e3aacff88bf79a625df82ceee73c71f3a7099a5459","0306b2d3fd06c4673cc90374b7db0c152ba7960be382440cecc4cdad7642e0537c","028020dd6062f080e1e2b49ca629faa1407978adab13b74875a9de93b16accf804","03907061c5f6fde367aafe27e1d53b39ff9c2babffe8ab7cf8c3023acba5c39736","029749462dba9af034455f5e0f170aac67fe9365ce7126092b4d24ced979b5381f","02f001d35308833881b3440670d25072256474c6c4061daf729055bf9563134105"]},"seed_version":13,"stored_height":490013,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[631,410,840,405]}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_7_18_multisig(self): + async def test_upgrade_from_client_2_7_18_multisig(self): wallet_str = '{"addr_history":{"32WKXQ6BWtGJDVTpdcUMhtRZWzgk5eKnhD":[],"33rvo2pxaccCV7jLwvth36sdLkdEqhM8B8":[],"347kG9dzt2M1ZPTa2zzcmVrAE75LuZs9A2":[],"34BBeAVEe5AM6xkRebddFG8JH6Vx1M5hHH":[],"34MAGbxxCHPX8ASfKsyNkzpqPEUTZ5i1Kx":[],"36uNpoPSgUhN5Cc1wRQyL77aD1RL3a9X6f":[],"384xygkfYsSuXN478zhN4jmNcky1bPo7Cq":[],"39GBGaGpp1ePBsjjaw8NmbZNZkMzhfmZ3W":[],"3BRhw13g9ShGcuHbHExxtFfvhjrxiSiA7J":[],"3BboKZc2VgjKVxoC5gndLGpwEkPJuQrZah":[],"3C3gKJ2UQNNHY2SG4h43zRS1faSLhnqQEr":[],"3CEY1V5WvCTxjHEPG5BY4eXpcYhakTvULJ":[],"3DJyQ94H9g18PR6hfzZNxwwdU6773JaYHd":[],"3Djb7sWog5ANggPWHm4xT5JiTrTSCmVQ8N":[],"3EfgjpUeJBhp3DcgP9wz3EhHNdkCbiJe2L":[],"3FWgjvaL8xN6ne19WCEeD5xxryyKAQ5tn1":[],"3H4ZtDFovXxwWXCpRo8mrCczjTrtbT6eYL":[],"3HvnjPzpaE3VGWwGTALZBguT8p9fyAcfHS":[],"3JGuY9EpzuZkDLR7vVGhqK7zmX9jhYEfmD":[],"3JvrP4gpCUeQzqgPyDt2XePXn3kpqFTo9i":[],"3K3TVvsfo52gdwz7gk84hfP77gRmpc3hkf":[],"3K5uh5viV4Dac267Q3eNurQQBnpEbYck5G":[],"3KaoWE1m3QrtvxTQLFfvNs8gwQH8kQDpFM":[],"3Koo71MC4wBfiDKTsck7qCrRjtGx2SwZqT":[],"3L8XBt8KxwqNX1vJprp6C9YfNW4hkYrC6d":[],"3QmZjxPwcsHZgVUR2gQ6wdbGJBbFro8KLJ":[]},"pruned_txo":{},"pubkeys":{"change":[["031bfbbfb36b5e526bf4d94bfc59f170177b2c821f7d4d4c0e1ee945467fe031a0","03c4664d68e3948e2017c5c55f7c1aec72c1c15686b07875b0f20d5f856ebeb703"],["03c515314e4b695a809d3ba08c20bef00397a0e2df729eaf17b8e082825395e06b","032391d8ab8cad902e503492f1051129cee42dc389231d3cdba60541d70e163244"],["035934f55c09ecec3e8f2aa72407ee7ba3c2f077be08b92a27bc4e81b5e27643fe","0332b121ed13753a1f573feaf4d0a94bf5dd1839b94018844a30490dd501f5f5fb"],["02b1367f7f07cbe1ef2c75ac83845c173770e42518da20efde3239bf988dbff5ac","03f3a8b9033b3545fbe47cab10a6f42c51393ed6e525371e864109f0865a0af43c"],["02e7c25f25ecc17969a664d5225c37ec76184a8843f7a94655f5ed34b97c52445d","030ae4304923e6d8d6cd67324fa4c8bc44827918da24a05f9240df7c91c8e8db8f"],["02deb653a1d54372dbc8656fe0a461d91bcaec18add290ccaa742bdaefdb9ec69b","023c1384f90273e3fc8bc551e71ace8f34831d4a364e56a6e778cd802b7f7965a6"]],"receiving":[["02d978f23dc1493db4daf066201f25092d91d60c4b749ca438186764e6d80e6aa1","02912a8c05d16800589579f08263734957797d8e4bc32ad7411472d3625fd51f10"],["024a4b4f2553d7f4cc2229922387aad70e5944a5266b2feb15f453cedbb5859b13","03f8c6751ee93a0f4afb7b2263982b849b3d4d13c2e30b3f8318908ad148274b4b"],["03cd88a88aabc4b833b4631f4ffb4b9dc4a0845bb7bc3309fab0764d6aa08c4f25","03568901b1f3fb8db05dd5c2092afc90671c3eb8a34b03f08bcfb6b20adf98f1cd"],["030530ffe2e4a41312a41f708febab4408ca8e431ce382c1eedb837901839b550d","024d53412197fc609a6ca6997c6634771862f2808c155723fac03ea89a5379fdcc"],["02de503d2081b523087ca195dbae55bafb27031a918a1cfedbd2c4c0da7d519902","03f4a27a98e41bddb7543bf81a9c53313bf9cfb2c2ebdb6bf96551221d8aecb01a"],["03504bc595ac0d947299759871bfdcf46bcdd8a0590c44a78b8b69f1b152019418","0291f188301773dbc7c1d12e88e3aa86e6d4a88185a896f02852141e10e7e986ab"],["0389c3ab262b7994d2202e163632a264f49dd5f78517e01c9210b6d0a29f524cd4","034bdfa9cc0c6896cb9488329d14903cfe60a2879771c5568adfc452f8dba1b2cb"],["02c55a517c162aae2cb5b36eef78b51aa15040e7293033a5b55ba299e375da297d","027273faf29e922d95987a09c2554229becb857a68112bd139409eb111e7cdb45e"],["02401e62d645dc64d43f77ba1f360b529a4c644ed3fc15b35932edafbaf741e844","02c44cbffc13cb53134354acd18c54c59fa78ec61307e147fa0f6f536fb030a675"],["02194a538f37b388b2b138f73a37d7fbb9a3e62f6b5a00bad2420650adc4fb44d9","03e5cc15d47fcdcf815baa0e15227bc5e6bd8af6cae6add71f724e95bc29714ce5"],["037ebf7b2029c8ea0c1861f98e0952c544a38b9e7caebbf514ff58683063cd0e78","022850577856c810dead8d3d44f28a3b71aaf21cdc682db1beb8056408b1d57d52"],["02aea7537611754fdafd98f341c5a6827f8301eaf98f5710c02f17a07a8938a30e","032fa37659a8365fdae3b293a855c5a692faca687b0875e9720219f9adf4bdb6c2"],["0224b0b8d200238495c58e1bc83afd2b57f9dbb79f9a1fdb40747bebb51542c8d3","03b88cd2502e62b69185b989abb786a57de27431ece4eabb26c934848d8426cbd6"],["032802b0be2a00a1e28e1e29cfd2ad79d36ef936a0ef1c834b0bbe55c1b2673bff","032669b2d80f9110e49d49480acf696b74ecca28c21e7d9c1dd2743104c54a0b13"],["03fcfa90eac92950dd66058bbef0feb153e05a114af94b6843d15200ef7cf9ea4a","023246268fbe8b9a023d9a3fa413f666853bbf92c4c0af47731fdded51751e0c3a"],["020cf5fffe70b174e242f6193930d352c54109578024677c1a13ffce5e1f9e6a29","03cb996663b9c895c3e04689f0cf1473974023fa0d59416be2a0b01ccdaa3cc484"],["03467e4fff9b33c73b0140393bde3b35a3f804bce79eccf9c53a1f76c59b7452bd","03251c2a041e953c8007d9ee838569d6be9eacfbf65857e875d87c32a8123036d8"],["02192e19803bfa6f55748aada33f778f0ebb22a1c573e5e49cba14b6a431ef1c37","02224ce74f1ee47ba6eaaf75618ce2d4768a041a553ee5eb60b38895f3f6de11dc"],["032679be8a73fa5f72d438d6963857bd9e49aef6134041ca950c70b017c0c7d44f","025a8463f1c68e85753bd2d37a640ab586d8259f21024f6173aeed15a23ad4287b"],["03ab0355c95480f0157ae48126f893a6d434aa1341ad04c71517b104f3eda08d3d","02ba4aadba99ae8dc60515b15a087e8763496fcf4026f5a637d684d0d0f8a5f76c"]]},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[523,230,840,405],"x1/":{"seed":"pudding sell evoke crystal try order supply chase fine drive nurse double","type":"bip32","xprv":"xprv9s21ZrQH143K2MK5erSSgeaPA1H7gENYS6grakohkaK2M4tzqo6XAjLoRPcBRW9NbGNpaZN3pdoSKLeiQJwmqdSi3GJWZLnK1Txpbn3zinV","xpub":"xpub661MyMwAqRbcEqPYksyT3nX7i37c5h6PoKcTP9DKJur1DsE9PLQmiXfHGe8RmN538Pj8t3qUQcZXCMrkS5z1uWJ6jf9EptAFbC4Z2nKaEQE"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcGYXvLgWjW91feK49GajmPdEarB3Ny8JDduUhzTcEThc8Xs1GyqMR4S7xPHvSq4sbDEFzQh3hjJJFEksUzvnjYnap5RX9o4j"}}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) # seed_version 13 is ambiguous # client 2.7.18 created wallets with an earlier "v13" structure @@ -217,81 +217,81 @@ def test_upgrade_from_client_2_7_18_multisig(self): # the wallet here was created in 2.7.18 with a couple privkeys imported # then opened in 2.8.3, after which a few other new privkeys were imported # it's in some sense in an "inconsistent" state - def test_upgrade_from_client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18(self): + async def test_upgrade_from_client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18(self): wallet_str = '{"addr_history":{"15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE":[],"179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn":[],"18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7":[],"1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar":[],"1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w":[],"1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8":[],"1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj":[],"1PYtQBkjXHQX6YtMzEgehN638o784pK3ce":[],"1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM":[]},"addresses":{"change":[],"receiving":["1PYtQBkjXHQX6YtMzEgehN638o784pK3ce","1yT2T4ha3i1GZoK2iP8EpcgSNG34R2ufM","1CjW4KM38acCRw3spiFKiZsj7xmmQqqwd8","1A9F6ZEqmfKeuLeEq5eWFxajgiJfGCc7ar","18o6WCBWdAaM5kjKnyEL4HysoT324rvJu7","1EaDNLPwHRraX1N3ecPWJ2mm7NRgdtvpCj","179vRrzjT9k7k5oCNCx6eodYCaLKPy9UQn","1BTjGNUmeMSPBTuXTdwD3DLyCugAZaFb7w","15VBrfYwoXvDWyXHq1myxDv4h36qUmCHcE"]},"keystore":{"keypairs":{"0206b77fd06f212ad7d85f4a054c231ba4e7894b1773dcbb449671ee54618ff5e9":"L52LWS2hB5ev9JYiisFewJH9Q16U7yYcSNt3M8UKLmL5p1q3v2H2","028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42":"KzRhkN9Psm9BobcPx3X3VykVA8yhCBrVvE4tTyq6NE283sL6uvYG","02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f":"KySXfvidmMBf8iw6m3R9WtdfKcQPWXenwMZtpno5XpfLMNHH8PMn","031bb44462038b97010624a8f8cb15a10fd0d277f12aba3ccf5ce0d36fc6df3112":"KxmcmCvNrZFgy2jyz9W353XbMwCYWHzYTQVzbaDfZM4FLxemgmKh","0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042":"L53Ks569m3H1dRzua3nGzBE3AaEV8dMvBoHDeSJGnWEDeL775mJ5","0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643":"KwHDUpfvnSC58bs3nGy7YpducXkbmo6UUHrydBHy6sT1mRJcVvBo","04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba":"5JECca5E7r1eNgME7NsPdE29XiVCVwXSzEihnhAQXuMdsJ4VL8S","04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed":"5Jt9rGLWgxoJUo4eoYEECskLmRA4BkZqHPHg7DdghKBaWarKuxW","04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b":"5KRjCNThRDP8aQTJ3Hq9HUSVNRNUB2e69xwLfMUsrXYLXT7U8b9"},"type":"imported"},"pruned_txo":{},"pubkeys":{"change":[],"receiving":["04e9ad0bf70c51c06c2459961175c47cfec59d58ebef4ffcd9836904ef11230afce03ab5eaac5958b538382195b5aea9bf057c0486079869bb72ef9c958f33f1ed","0339081c4a0ce22c01aa78a5d025e7a109100d1a35ef0f8f06a0d4c5f9ffefc042","0339ea71aba2805238e636c2f1b3e5a8308b1dbdbb335787c51f2f6bf3f6218643","02ba4117a24d7e38ae14c429fce0d521aa1fb6bb97558a13f1ef2bc0a476a1741f","028cda4a0f03cbcbc695d9cac0858081fd5458acfd29564127d329553245afca42","04e7dc460c87267cf0958d6904d9cd99a4af0d64d61858636aec7a02e5f9a578d27c1329d5ddc45a937130ed4a59e4147cb4907724321baa6a976f9972a17f79ba","04f8cbd67830ab37138c92898a64a4edf836a60aa5b36956547788bd205c635d6a3056fa6a079961384ae336e737d4c45835821c8915dbc5e18a7def88df83946b"]},"seed_version":13,"stored_height":492756,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_8_3_seeded(self): + async def test_upgrade_from_client_2_8_3_seeded(self): wallet_str = '{"addr_history":{"13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt":[],"14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ":[],"14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw":[],"16FECc7nP2wor1ijXKihGofUoCkoJnq6XR":[],"16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk":[],"17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz":[],"17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV":[],"19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL":[],"19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW":[],"1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q":[],"1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms":[],"1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh":[],"1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv":[],"1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC":[],"1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq":[],"1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3":[],"1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6":[],"1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT":[],"1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL":[],"1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z":[],"1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN":[],"1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst":[],"1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn":[],"1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL":[],"1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn":[],"1QEuVTdenchPn9naMhakYx8QwGUXE6JYp":[]},"addresses":{"change":["1K5CWjgZEYcKTsJWeQrH6NcMPzFUAikD8z","19snysSPZEbgjmeMtuT7qDMTLH2fa7zrWW","1DGhQ1up6dMieEwFdsQQFHRriyyR59rYVq","17CbQhK3gutqgWt2iLX69ZeSCvw8yFxPLz","1Q3eTNJWTnfxPkUJXQkeCqPh1cBQjjEXFn","17jEaAyekE8BHPvPmkJqFUh1v1GSi6ywoV"],"receiving":["1KMDUXdqpthH1XZU4q5kdSoMZmCW9yDMcN","1HkbtbyocwHWjKBmzKmq8szv3cFgSGy7dL","1HiB2QCfNem8b4cJaZ2Rt9T4BbUCPXvTpT","14fH7oRM4bqJtJkgJEynTShcUXQwdxH6mw","1NuPofeK8yNEjtVAu9Rc2pKS9kw8YWUatL","16FECc7nP2wor1ijXKihGofUoCkoJnq6XR","19F5SjaWYVCKMPWR8q1Freo4RGChSmFztL","1NQwmHYdxU1pFTTWyptn8vPW1hsSWJBRTn","1HBAAqFVndXBcWdWQNYVYSDK9kdUu8ZRU3","1B4FU2WEd2NQzd2MkWBLHw87uJhBxoVghh","1HMrRJkTayNRBZdXZKVb7oLZKj24Pq65T6","1KmHNiNmeS7tWRLYTFDMrTbKR6TERYicst","1BdB7ahc8TSR9RJDmWgGSgsWji2BgzcVvC","14C6nXs2GRaK3o5U5e8dJSpVRCoqTsyAkJ","1AFgvLGNHP3nZDNrZ4R2BZKnbwDVAEUP4q","13sNgoAhqDUTB3YSzWYcKKvP2EczG5JGmt","1AwWgUbjQfRhKVLKm1o7qfpXnqeN3cu7Ms","1QEuVTdenchPn9naMhakYx8QwGUXE6JYp","1BEBouVJFihDmEQMTAv4bNV2Q7dZh5iJzv","16cMJC5ZAtPnvLQBzfHm9YR9GoDxUseMEk"]},"keystore":{"seed":"novel clay width echo swing blanket absorb salute asset under ginger final","type":"bip32","xprv":"xprv9s21ZrQH143K2jfFF6ektPj6zCCsDGGjQxhD2FQ21j6yrA1piWWEjch2kf1smzB2rzm8rPkdJuHf3vsKqMX9ogtE2A7JF49qVUHrgtjRymM","xpub":"xpub661MyMwAqRbcFDjiM8BmFXfqYE3McizanBcopdoda4dxixLyG3pVHR1WbwgjLo9RL882KRfpfpxh7a7zXPogDdR4xj9TpJWJGsbwaodLSKe"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_8_3_importedkeys(self): + async def test_upgrade_from_client_2_8_3_importedkeys(self): wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_8_3_watchaddresses(self): + async def test_upgrade_from_client_2_8_3_watchaddresses(self): wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[535,380,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_8_3_trezor_singleacc(self): + async def test_upgrade_from_client_2_8_3_trezor_singleacc(self): wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"addresses":{"change":["1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ","14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM","1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG","15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6","1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL","1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs"],"receiving":["1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu","18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw","17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH","12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC","15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ","1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid","1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz","1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj","146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz","1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC","1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo","1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb","1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe","1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv","1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp","15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S","1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX","1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp","1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk","1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD"]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[744,390,840,405]}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_8_3_multisig(self): + async def test_upgrade_from_client_2_8_3_multisig(self): wallet_str = '{"addr_history":{"32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS":[],"339axnadPaQg3ngChNBKap2dndUWrSwjk6":[],"34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ":[],"35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM":[],"35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ":[],"36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1":[],"37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc":[],"39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW":[],"3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav":[],"3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX":[],"3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq":[],"3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts":[],"3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh":[],"3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF":[],"3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK":[],"3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc":[],"3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb":[],"3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav":[],"3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c":[],"3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz":[],"3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso":[],"3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM":[],"3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6":[],"3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg":[],"3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a":[],"3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE":[]},"addresses":{"change":["34FG8qzA6UYLxrnkpVkM9mrGYix3ZyePJZ","3LzjAqqfiN8w4TSiW8Up7bKLD5CicBUC3a","3GEhQHTrWykC6Jfu923qtpxJECsEGVdhUc","3Nro51wauHugv72NMtY9pmLnwX3FXWU1eE","3JBbS3GvhvoLgtLcuMvHCtqjE7dnbpTMkz","3CGCLSHU8ZjeXv6oukJ3eAQN4fqEQ7wuyX"],"receiving":["35zrocLBQbHfEqysgv2v5z3RH7BRGQzSMJ","3FWZ7pJPxZhGr8p6HNr9LLsHA8sABcP7cF","3DPheE9uany9ET2qBnWF1wh3zDtptGP6Ts","3HbdMVgKRqadNiHRNGizUCyTQYpJ1aXFav","3KQ5tTEbkQSkKiccKFDPrhLnBjSMey6CQM","35CR3h2dFF3EkRX5yK47NGuF2FcLtJvpUM","3HJ95uxwW6rMoEhYgUfcgpd3ExU3fjkfNb","3FZbzEF9HdRqzif2cKUFnwW9AFTJcibjVK","39r4XCmfU4J3N98YQ8Fwvm8VN1Fukfj7QW","3LNZbX9wenL3bLxJTQnPidSvVt3EtDrnUg","32Qk6Q7XYD2v3et9g5fA97ky8XRAJNDZCS","339axnadPaQg3ngChNBKap2dndUWrSwjk6","3EeNJHgSYVJPxYR2NaYv2M2ZnXkPRWSHQh","3BDqFoYMxyy7nWCpRChYV6YCGh9qnWDmav","3DCNnfh7oWLsnS3p5QdWfW3hvcFF8qAPFq","3KNWZasWDBuVzzp5Y5cbEgjeYn3NKHZKso","37nSiBvGXm1PNYseymaJn5ERcU4mSMueYc","3KrFHcAzNJYjukGDDZm2HeV5Mok4NGQaD6","36uBJPkgiQwav23ybewbgkQ2zEzJDY2EX1","3J6xRF9d16QNsvoXkYkeTwTU8L5N3Y8f7c"]},"pruned_txo":{},"seed_version":13,"stored_height":0,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[671,238,840,405],"x1/":{"seed":"property play install hill hunt follow trash comic pulse consider canyon limit","type":"bip32","xprv":"xprv9s21ZrQH143K46tCjDh5i4H9eSJpnMrYyLUbVZheTbNjiamdxPiffMEYLgxuYsMFokFrNEZ6S6z5wSXXszXaCVQWf6jzZvn14uYZhsnM9Sb","xpub":"xpub661MyMwAqRbcGaxfqFE65CDtCU9KBpaQLZQCHx7G1vuibP6nVw2vD9Z2Bz2DsH43bDZGXjmcvx2TD9wq3CmmFcoT96RCiDd1wMSUB2UH7Gu"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcEncvVc1zrPFZSKe7iAP1LTRhzxuXpmztu1kTtnfj8XNFzzmGH1X1gcGxczBZ3MmYKkxXgZKJCsNXXdasNaQJKJE4KcUjn1L"}}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_9_3_seeded(self): + async def test_upgrade_from_client_2_9_3_seeded(self): wallet_str = '{"addr_history":{"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes":[],"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1":[],"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB":[],"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c":[],"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz":[],"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA":[],"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV":[],"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z":[],"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv":[],"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B":[],"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz":[],"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G":[],"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq":[],"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d":[],"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs":[],"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado":[],"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z":[],"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52":[],"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP":[],"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv":[],"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb":[],"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ":[],"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G":[],"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN":[],"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J":[],"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt":[]},"addresses":{"change":["1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP","1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z","15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV","1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq","19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G","1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb"],"receiving":["14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA","13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB","19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz","1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv","1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt","13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c","1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ","12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes","12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1","14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz","1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN","17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z","1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado","18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv","1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G","18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B","1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d","1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs","1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52","1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J"]},"keystore":{"seed":"cereal wise two govern top pet frog nut rule sketch bundle logic","type":"bip32","xprv":"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v","xpub":"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[619,310,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) @as_testnet - def test_upgrade_from_client_2_9_3_old_seeded_with_realistic_history(self): + async def test_upgrade_from_client_2_9_3_old_seeded_with_realistic_history(self): wallet_str = '{"addr_history":{"mfxZoA5dHaVn6QiHMbRwQTQUB7VFpZQFW4":[["8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471",1446399]],"mgSZfzhK7VDj7MQRxjRLSC7FSESskDKsrN":[],"mgaArVGf5heGUko1i24wYrvkfFcN442U4v":[["76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4",1251084]],"mhBod4XkgRs3orfynhDGR7gLkDuD23tHKP":[],"mhgErkPdC6BP5h6JAQ7nw2EfNerepQB8QL":[],"mhwQ9cxhAxaED747XuzgUo3F39MDc6txHr":[["8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471",1446399]],"mjUYaF2jPsTWYEUWidQiKuqnHDRh49Q95W":[],"mjWMtS2NZvAcZLdHGanWrsVwnyYEtBr9si":[],"mk8xgq5ubvpXQKFNNeMzYpy25G5zSQ6Vtc":[],"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a":[["e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25",1297404],["e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306",1297610]],"mnFrzrrgj65VAynUzzhnuSkrrWwivtR7a2":[],"mnQ53GF9oa4njpWswsnmUQ9A4Hif8ct86q":[],"mo2ougmAzBmvQW5iJCojfi7n2Rt7RaRtGc":[],"mp2CafXnWnN8rR6BnFToVQ8bXNY4jnAecr":[["e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306",1297610]],"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh":[["0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67",1230168],["72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112",1230191]],"mpEGnkPKtMyfHo8EaUFks7xFZJdSgLjnC7":[["3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308",1231594]],"mpHhM9knUvMLHjhy7kDjXuEKV72hM6FzuK":[],"mpg6Y3SCPa8CSDYdzA7GEx855Zxg3ZhasV":[],"mpmDNEsBXxRC55RpqKpqSEy37edNzSx9Cw":[],"mq9piav8nf5yw9pJt4bsob7mpngw6EK8Bx":[],"mqA4SNeHJtqUmxXWsmddweawM9DRRGVMuN":[],"mqEq8T5ktH1E9RbVramCwAXnrUa8EdzZrk":[],"mqSpdctdWV7gdiXWEJVgkt7yMoL8FSVtLB":[],"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX":[["336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa",1242088],["c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462",1242088]],"mqvJJCT4WHQHQvj7bvCNQDdHdr9WjCPMaH":[],"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm":[["475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d",1281357],["e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25",1297404]],"mrdUepR2frXufJH5zeHEQZRVbYiAjVNufo":[],"msgSKRJY9y8GPFsDpsnso23RQbEWFY2DJL":[],"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt":[["e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968",1231594],["3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308",1231594]],"mtPuWkAj3cYHj4xoDYYL4yJ5tTckeXb6go":[],"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd":[["a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d",1230163],["0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67",1230168],["6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254",1230626],["31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f",1230627],["9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb",1255526]],"muwtzEGtaACt6KrLxwdu8itbfsKo8WerW7":[],"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK":[["c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a",1251074],["475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d",1281357]],"mvXwR94pXVgP7kdrW3vTiDQtVrkP3NY3zn":[["c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462",1242088]],"mvbi7ywmFLL8FLrcn7XEYATbn1kBeeNbzx":[],"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf":[["e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306",1297610],["8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471",1446399]],"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp":[["c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462",1242088],["2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd",1251055]],"mwzAXyVfyG5mcGKAQANs67M3HhENmu2Uh2":[],"mx1KFACmoAA2EedMAcoQ4ed5dRtsh3ow4L":[],"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb":[["7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a",1230624],["6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254",1230626]],"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm":[["a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d",1230163],["0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67",1230168]],"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6":[["c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9",1323966],["8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471",1446399]],"myunReLavjcSN8mWUn3jqhirHWYiok51jU":[],"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY":[["56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216",1231595],["c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462",1242088]],"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd":[["0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67",1230168],["72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112",1230191]],"mzeE9KrQrsfqYAuTN5EJXcs91rUJU8Y8Bb":[],"mzrXKAWzbctb6Ee1LkbXLmdsNhPtLucUkw":[],"n1Hjparfsp2c4yCZ72KbotNcY84XLS73jj":[],"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH":[["e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968",1231594],["c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462",1242088]],"n1yXnvBc7giip11h2D2NX3azXqhasAeFeM":[],"n23NSQfgAmVaW1qE1kgnxkW8JvWfveAktH":[["3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308",1231594]],"n3JYuxqhKja3QcJG3sm4yKTmZUBpTbVQyP":[],"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX":[["2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d",1231593],["e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968",1231594]],"n4g4z5GRAWXXcx5f3m7Cyyek9LRRPHcuJy":[]},"addresses":{"change":["mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd","mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt","n23NSQfgAmVaW1qE1kgnxkW8JvWfveAktH","mvXwR94pXVgP7kdrW3vTiDQtVrkP3NY3zn","mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm","mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a","mp2CafXnWnN8rR6BnFToVQ8bXNY4jnAecr","mhwQ9cxhAxaED747XuzgUo3F39MDc6txHr","n1yXnvBc7giip11h2D2NX3azXqhasAeFeM","mpmDNEsBXxRC55RpqKpqSEy37edNzSx9Cw","msgSKRJY9y8GPFsDpsnso23RQbEWFY2DJL","mhBod4XkgRs3orfynhDGR7gLkDuD23tHKP","mvbi7ywmFLL8FLrcn7XEYATbn1kBeeNbzx","mzeE9KrQrsfqYAuTN5EJXcs91rUJU8Y8Bb"],"receiving":["mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd","myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm","mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh","n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX","n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH","mpEGnkPKtMyfHo8EaUFks7xFZJdSgLjnC7","myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY","mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX","mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp","mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK","mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb","mgaArVGf5heGUko1i24wYrvkfFcN442U4v","mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6","mgSZfzhK7VDj7MQRxjRLSC7FSESskDKsrN","mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf","mqSpdctdWV7gdiXWEJVgkt7yMoL8FSVtLB","n4g4z5GRAWXXcx5f3m7Cyyek9LRRPHcuJy","mqvJJCT4WHQHQvj7bvCNQDdHdr9WjCPMaH","n1Hjparfsp2c4yCZ72KbotNcY84XLS73jj","mfxZoA5dHaVn6QiHMbRwQTQUB7VFpZQFW4","mjWMtS2NZvAcZLdHGanWrsVwnyYEtBr9si","mhgErkPdC6BP5h6JAQ7nw2EfNerepQB8QL","mx1KFACmoAA2EedMAcoQ4ed5dRtsh3ow4L","myunReLavjcSN8mWUn3jqhirHWYiok51jU","mrdUepR2frXufJH5zeHEQZRVbYiAjVNufo","mk8xgq5ubvpXQKFNNeMzYpy25G5zSQ6Vtc","mqEq8T5ktH1E9RbVramCwAXnrUa8EdzZrk","mq9piav8nf5yw9pJt4bsob7mpngw6EK8Bx","mpHhM9knUvMLHjhy7kDjXuEKV72hM6FzuK","mwzAXyVfyG5mcGKAQANs67M3HhENmu2Uh2","mnQ53GF9oa4njpWswsnmUQ9A4Hif8ct86q","mpg6Y3SCPa8CSDYdzA7GEx855Zxg3ZhasV","mo2ougmAzBmvQW5iJCojfi7n2Rt7RaRtGc","mzrXKAWzbctb6Ee1LkbXLmdsNhPtLucUkw","mnFrzrrgj65VAynUzzhnuSkrrWwivtR7a2","muwtzEGtaACt6KrLxwdu8itbfsKo8WerW7","n3JYuxqhKja3QcJG3sm4yKTmZUBpTbVQyP","mqA4SNeHJtqUmxXWsmddweawM9DRRGVMuN","mjUYaF2jPsTWYEUWidQiKuqnHDRh49Q95W","mtPuWkAj3cYHj4xoDYYL4yJ5tTckeXb6go"]},"keystore":{"mpk":"e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3","seed":"acb740e454c3134901d7c8f16497cc1c","type":"old"},"pruned_txo":{},"seed_type":"old","seed_version":13,"stored_height":1482542,"transactions":{"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67":"01000000029d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2000000008b483045022100a146a2078a318c1266e42265a369a8eef8993750cb3faa8dd80754d8d541d5d202207a6ab8864986919fd1a7fd5854f1e18a8a0431df924d7a878ec3dc283e3d75340141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff9d1bdbe67f0bd0d7bd700463f5c29302057c7b52d47de9e2ca5069761e139da2010000008a47304402201c7fa37b74a915668b0244c01f14a9756bbbec1031fb69390bcba236148ab37e02206151581f9aa0e6758b503064c1e661a726d75c6be3364a5a121a8c12cf618f64014104dc28da82e141416aaf771eb78128d00a55fdcbd13622afcbb7a3b911e58baa6a99841bfb7b99bcb7e1d47904fda5d13fdf9675cdbbe73e44efcc08165f49bac6feffffff02b0183101000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac005a6202000000001976a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac54c51200","2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d":"010000000132201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a050000006a47304402201d20bb5629a35b84ff9dd54788b98e265623022894f12152ac0e6158042550fe02204e98969e1f7043261912dd0660d3da64e15acf5435577fc02a00eccfe76b323f012103a336ad86546ab66b6184238fe63bb2955314be118b32fa45dd6bd9c4c5875167fdffffff0254959800000000001976a9148d2db0eb25b691829a47503006370070bc67400588ac80969800000000001976a914f96669095e6df76cfdf5c7e49a1909f002e123d088ace8ca1200","2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd":"01000000036cdf8d2226c57d7cc8485636d8e823c14790d5f24e6cf38ba9323babc7f6db2901000000171600143fc0dbdc2f939c322aed5a9c3544468ec17f5c3efdffffff507dce91b2a8731636e058ccf252f02b5599489b624e003435a29b9862ccc38c0200000017160014c50ff91aa2a790b99aa98af039ae1b156e053375fdffffff6254162cf8ace3ddfb3ec242b8eade155fa91412c5bde7f55decfac5793743c1010000008b483045022100de9599dcd7764ca8d4fcbe39230602e130db296c310d4abb7f7ae4d139c4d46402200fbfd8e6dc94d90afa05b0c0eab3b84feb465754db3f984fbf059447282771c30141045eecefd39fabba7b0098c3d9e85794e652bdbf094f3f85a3de97a249b98b9948857ea1e8209ee4f196a6bbcfbad103a38698ee58766321ba1cdee0cbfb60e7b2fdffffff01e85af70100000000160014e8d29f07cd5f813317bec4defbef337942d85d74ed161300","31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f":"010000000454022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a000000008b483045022100ea8fe74db2aba23ad36ac66aaa481bad2b4d1b3c331869c1d60a28ce8cfad43c02206fa817281b33fbf74a6dd7352bdc5aa1d6d7966118a4ad5b7e153f37205f1ae80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a01000000171600146dfe07e12af3db7c715bf1c455f8517e19c361e7fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a020000006a47304402200b1fb89e9a772a8519294acd61a53a29473ce76077165447f49a686f1718db5902207466e2e8290f84114dc9d6c56419cb79a138f03d7af8756de02c810f19e4e03301210222bfebe09c2638cfa5aa8223fb422fe636ba9675c5e2f53c27a5d10514f49051fdffffff54022b1b4d3b45e7fcac468de2d6df890a9f41050c05d80e68d4b083f728e76a0300000000fdffffff018793140d000000001600144b3e27ddf4fc5f367421ee193da5332ef351b70022c71200","336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa":"010000000232201ff125888a326635a2fc6e971cd774c4d0c1a757d742d0f6b5b020f7203a020000006a4730440220198c0ba2b2aefa78d8cca01401d408ecdebea5ac05affce36f079f6e5c8405ca02200eabb1b9a01ff62180cf061dfacedba6b2e07355841b9308de2d37d83489c7b80121031c663e5534fe2a6de816aded6bb9afca09b9e540695c23301f772acb29c64a05fdfffffffb28ff16811d3027a2405be68154be8fdaff77284dbce7a2314c4107c2c941600000000000fdffffff015e104f01000000001976a9146dfd56a0b5d0c9450d590ad21598ecfeaa438bd788ac79f31200","3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308":"010000000168091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0000000008a47304402202f683a63af571f405825066bd971945a35e7142a75c9a5255d364b25b7115d5602206c59a7214ae729a519757e45fdc87061d357813217848cf94df74125221267ac014104aecb9d427e10f0c370c32210fe75b6e72ccc4f415076cf1a6318fbed5537388862c914b29269751ab3a04962df06d96f5f4f54e393a0afcbfa44b590385ae61afdffffff0240420f00000000001976a9145f917fd451ca6448978ebb2734d2798274daf00b88aca8063d00000000001976a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ace9ca1200","475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d":"01000000013a7e6f19a963adc7437d2f3eb0936f1fc9ef4ba7e083e19802eb1111525a59c2000000008b483045022100958d3931051306489d48fe69b32561e0a16e82a2447c07be9d1069317084b5e502202f70c2d9be8248276d334d07f08f934ffeea83977ad241f9c2de954a2d577f94014104d950039cec15ad10ad4fb658873bc746148bc861323959e0c84bf10f8633104aa90b64ce9f80916ab0a4238e025dcddf885b9a2dd6e901fe043a433731db8ab4fdffffff02a086010000000000160014bbfab2cc3267cea2df1b68c392cb3f0294978ca922940d00000000001976a914760f657c67273a06cad5b1d757a95f4ed79f5a4b88ac4c8d1300","56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216":"0100000001614b142aeeb827d35d2b77a5b11f16655b6776110ddd9f34424ff49d85706cf90200000000fdffffff02784a4c00000000001600148464f47f35cbcda2e4e5968c5a3a862c43df65a1404b4c00000000001976a914c9efecf0ecba8b42dce0ae2b28e3ea0573d351c988ace9ca1200","6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254":"010000000496941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e000000001716001441aec99157d762708339d7faf7a63a8c479ed84cfdffffff96941b9f18710b39bacde890e39a7fa401e6bf49985857cb7adfb8a45147ef1e0100000000fdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f000000006a4730440220652145460092ef42452437b942cb3f563bf15ad90d572d0b31d9f28449b7a8dd022052aae24f58b8f76bd2c9cf165cc98623f22870ccdbef1661b6dbe01c0ef9010f01210375b63dd8e93634bbf162d88b25d6110b5f5a9638f6fe080c85f8b21c2199a1fdfdffffff1a5d1e4ca513983635b0df49fd4f515c66dd26d7bff045cfbd4773aa5d93197f010000008a47304402207517c52b241e6638a84b05385e0b3df806478c2e444f671ca34921f6232ee2e70220624af63d357b83e3abe7cdf03d680705df0049ec02f02918ee371170e3b4a73d014104de408e142c00615294813233cdfe9e7774615ae25d18ba4a1e3b70420bb6666d711464518457f8b947034076038c6f0cfc8940d85d3de0386e0ad88614885c7cfdffffff0480969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac809698000000000017a914f2a76207d7b54bd34282281205923841341d9e1f87002d3101000000001976a914b8d4651937cd7db5bcf5fc98e6d2d8cfa131e85088ac743db20a00000000160014c7d0df09e03173170aed0247243874c6872748ed20c71200","72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112":"0100000002677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f000000008b4830450221009c50c0f56f34781dfa7b3d540ac724436c67ffdc2e5b2d5a395c9ebf72116ef802205a94a490ea14e4824f36f1658a384aeaecadd54839600141eb20375a49d476d1014104c291245c2ee3babb2a35c39389df56540867f93794215f743b9aa97f5ba114c4cdee8d49d877966728b76bc649bb349efd73adef1d77452a9aac26f8c51ae1ddfdffffff677b2113f26697718c8991823ec0e637f08cb61426da8da508b97449c872490f010000008b483045022100ae0b286493491732e7d3f91ab4ac4cebf8fe8a3397e979cb689e62d350fdcf2802206cf7adf8b29159dd797905351da23a5f6dab9b9dbf5028611e86ccef9ff9012e014104c62c4c4201d5c6597e5999f297427139003fdb82e97c2112e84452d1cfdef31f92dd95e00e4d31a6f5f9af0dadede7f6f4284b84144e912ff15531f36358bda7fdffffff019f7093030000000022002027ce908c4ee5f5b76b4722775f23e20c5474f459619b94040258290395b88afb6ec51200","76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4":"0100000001f4ba9948cdc4face8315c7f0819c76643e813093ffe9fbcf83d798523c7965db000000006a473044022061df431a168483d144d4cffe1c5e860c0a431c19fc56f313a899feb5296a677c02200208474cc1d11ad89b9bebec5ec00b1e0af0adaba0e8b7f28eed4aaf8d409afb0121039742bf6ab70f12f6353e9455da6ed88f028257950450139209b6030e89927997fdffffff01d4f84b00000000001976a9140b93db89b6bf67b5c2db3370b73d806f458b3d0488ac0a171300","7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a":"0100000002681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a00000000232200209adfa712053a06cc944237148bcefbc48b16eb1dbdc43d1377809bcef1bea9affdffffff681b6a8dd3a406ee10e4e4aece3c2e69f6680c02f53157be6374c5c98322823a0100000023220020f40ed2e3fbffd150e5b74f162c3ce5dae0dfeba008a7f0f8271cf1cf58bfb442fdffffff02801d2c04000000001976a9140cc01e19090785d629cdcc98316f328df554de4f88ac6d455d05000000001976a914b9e828990a8731af4527bcb6d0cddf8d5ffe90ce88ac1fc71200","8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471":"0100000002b9480e55d5afe8f63565c9bd9ff9eb304dd881ba9c5634a87d114c5c5983fac1000000008b483045022100acc3f465902feed13f7358626003c517b2b659007b8876e401ee6933c034b7f80220309eae30631d444a3fca218edbae04e6e2f3492958f31bb8b8f762c02974e671014104d2416bae1a485b6e1ef78d30b41ce1acff13da94f53897c3847f004bc5f87237f53e2fc8d56073cd7a3d3932b2a10eff9cc5e4a4da52f1ad445806c1f8b986e0fdffffff06a38bc8fab255df2662059889fc0ba6ebf873eab591165607b5936634e753e4000000008b48304502210099e11a4e861963e50b2536bfd3d0d70e5faf17e758717a1d36e78973638ba8f802204435f2b0664e0bc95c54d21520696391911d6aaa11dfcbc613c51f594e884680014104861473a447374a30387cca4548dd6462d9526b44beb1029b5f074a5fa8f09e01d21dbae1599aae3f9221e5ed830df7bae69caf4565e50471e34d51801d9a588afdffffff02f0230000000000001976a9141a8fd125d7d6728c0d84bd7b9f6f16442eef776988aca0860100000000001976a91404d808d10acfd7f6dcf81f88912ccf7285ed447688acfe111600","9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb":"01000000013409c10fd732d9e4b3a9a1c4beb511fa5eb32bc51fd169102a21aa8519618f800000000000fdffffff0640420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac40420f00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac80841e00000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac64064a000000000016001469825d422ca80f2a5438add92d741c7df45211f280969800000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac65281300","a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d":"010000000400899af3606e93106a5d0f470e4e2e480dfc2fd56a7257a1f0f4d16fd5961a0f000000006a47304402205b32a834956da303f6d124e1626c7c48a30b8624e33f87a2ae04503c87946691022068aa7f936591fb4b3272046634cf526e4f8a018771c38aff2432a021eea243b70121034bb61618c932b948b9593d1b506092286d9eb70ea7814becef06c3dfcc277d67fdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753000000006b483045022100de775a580c6cb47061d5a00c6739033f468420c5719f9851f32c6992610abd3902204e6b296e812bb84a60c18c966f6166718922780e6344f243917d7840398eb3db0121025d7317c6910ad2ad3d29a748c7796ddf01e4a8bc5e3bf2a98032f0a20223e4aafdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753010000006a4730440220615a26f38bf6eb7043794c08fb81f273896b25783346332bec4de8dfaf7ed4d202201c2bc4515fc9b07ded5479d5be452c61ce785099f5e33715e9abd4dbec410e11012103caa46fcb1a6f2505bf66c17901320cc2378057c99e35f0630c41693e97ebb7cffdffffff4bc2dcc375abfc7f97d8e8c482f4c7b8bc275384f5271678a32c35d955170753030000006b483045022100c8fba762dc50041ee3d5c7259c01763ed913063019eefec66678fb8603624faa02200727783ccbdbda8537a6201c63e30c0b2eb9afd0e26cb568d885e6151ef2a8540121027254a862a288cfd98853161f575c49ec0b38f79c3ef0bf1fb89986a3c36a8906fdffffff0240787d01000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac3bfc1502000000001976a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac52c51200","c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462":"0100000003aabec9cb99096073ae47cfb84bfd5b0063ae7f157956fd37c5d1a79d74ee6e33000000008b4830450221008136fc880d5e24fdd9d2a43f5085f374fef013b814f625d44a8075104981d92a0220744526ec8fc7887c586968f22403f0180d54c9b7ff8db9b553a3c4497982e8250141047b8b4c91c5a93a1f2f171c619ca41770427aa07d6de5130c3ba23204b05510b3bd58b7a1b35b9c4409104cfe05e1677fc8b51c03eac98b206e5d6851b31d2368fdffffff16d23bdc750c7023c085a6fc76e3e468944919783535ea2c13826f181058a656010000008a47304402204148410f2d796b1bb976b83904167d28b65dcd7c21b3876022b4fa70abc86280022039ea474245c3dc8cd7e5a572a155df7a6a54496e50c73d9fed28e76a1cf998c00141044702781daed201e35aa07e74d7bda7069e487757a71e3334dc238144ad78819de4120d262e8488068e16c13eea6092e3ab2f729c13ef9a8c42136d6365820f7dfdffffff68091e76227e99b098ef8d6d5f7c1bb2a154dd49103b93d7b8d7408d49f07be0010000008b4830450221008228af51b61a4ee09f58b4a97f204a639c9c9d9787f79b2fc64ea54402c8547902201ed81fca828391d83df5fbd01a3fa5dd87168c455ed7451ba8ccb5bf06942c3b0141046fcdfab26ac08c827e68328dbbf417bbe7577a2baaa5acc29d3e33b3cc0c6366df34455a9f1754cb0952c48461f71ca296b379a574e33bcdbb5ed26bad31220bfdffffff0210791c00000000001976a914a4b991e7c72996c424fe0215f70be6aa7fcae22c88ac80c3c901000000001976a914b0f6e64ea993466f84050becc101062bb502b4e488ac7af31200","c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9":"01000000014f86fabb180a955cd9f304aca1917d5dc08f1fbf0be501d1fc2c76ab60d5f56e0000000000fdffffff01a6250000000000001976a914c695421e5fe3cf96c75410ed160418dbda96dbc588acbd331400","c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a":"01000000018557003cb450f53922f63740f0f77db892ef27e15b2614b56309bfcee96a0ad3010000006a473044022041923c905ae4b5ed9a21aa94c60b7dbcb8176d58d1eb1506d9fb1e293b65ce01022015d6e9d2e696925c6ad46ce97cc23dec455defa6309b839abf979effc83b8b160121029332bf6bed07dcca4be8a5a9d60648526e205d60c75a21291bffcdefccafdac3fdffffff01c01c0f00000000001976a914a2185918aa1006f96ed47897b8fb620f28a1b09988ac01171300","e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968":"01000000016d445091b7b4fa19cbbee30141071b2202d0c27d195b9d6d2bcc7085c9cd9127010000008b483045022100daf671b52393af79487667eddc92ebcc657e8ae743c387b25d1c1a2e19c7a4e7022015ef2a52ea7e94695de8898821f9da539815775516f18329896e5fc52a3563b30141041704a3daafaace77c8e6e54cf35ed27d0bf9bb8bcd54d1b955735ff63ec54fe82a80862d455c12e739108b345d585014bf6aa0cbd403817c89efa18b3c06d6b5fdffffff02144a4c00000000001976a9148942ac692ace81019176c4fb0ac408b18b49237f88ac404b4c00000000001976a914dd36d773acb68ac1041bc31b8a40ee504b164b2e88ace9ca1200","e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306":"010000000125af87b0c2ebb9539d644e97e6159ccb8e1aa80fe986d01f60d2f3f37f207ae8010000008b483045022100baed0747099f7b28a5624005d50adf1069120356ac68c471a56c511a5bf6972b022046fbf8ec6950a307c3c18ca32ad2955c559b0d9bbd9ec25b64f4806f78cadf770141041ea9afa5231dc4d65a2667789ebf6806829b6cf88bfe443228f95263730b7b70fb8b00b2b33777e168bcc7ad8e0afa5c7828842794ce3814c901e24193700f6cfdffffff02a0860100000000001976a914ade907333744c953140355ff60d341cedf7609fd88ac68830a00000000001976a9145d48feae4c97677e4ca7dcd73b0d9fd1399c962b88acc9cc1300","e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25":"01000000010db780fff7dfcef6dba9268ecf4f6df45a1a86b86cad6f59738a0ce29b145c47010000008a47304402202887ec6ec200e4e2b4178112633011cbdbc999e66d398b1ff3998e23f7c5541802204964bd07c0f18c48b7b9c00fbe34c7bc035efc479e21a4fa196027743f06095f0141044f1714ed25332bb2f74be169784577d0838aa66f2374f5d8cbbf216063626822d536411d13cbfcef1ff3cc1d58499578bc4a3c4a0be2e5184b2dd7963ef67713fdffffff02a0860100000000001600145bbdf3ba178f517d4812d286a40c436a9088076e6a0b0c00000000001976a9143fc16bef782f6856ff6638b1b99e4d3f863581d388acfbcb1300"},"tx_fees":{},"txi":{"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67":{"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd":[["a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d:0",25000000]],"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm":[["a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d:1",34995259]]},"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d":{},"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd":{"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp":[["c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462:1",30000000]]},"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f":{"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd":[["6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254:0",10000000]]},"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa":{},"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308":{"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt":[["e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968:0",4999700]]},"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d":{"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK":[["c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a:0",990400]]},"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216":{},"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254":{"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb":[["7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a:1",89998701]]},"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112":{"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh":[["0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67:1",40000000]],"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd":[["0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67:0",19994800]]},"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4":{},"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a":{},"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471":{"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf":[["e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306:0",100000]],"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6":[["c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9:0",9638]]},"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb":{},"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d":{},"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462":{"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX":[["336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa:0",21958750]],"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY":[["56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216:1",5000000]],"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH":[["e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968:1",5000000]]},"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9":{},"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a":{},"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968":{"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX":[["2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d:1",10000000]]},"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306":{"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a":[["e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25:1",789354]]},"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25":{"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm":[["475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d:1",889890]]}},"txo":{"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67":{"mp9iZVBSUokAUX1p57Kjc4mHrtqqEhxjrh":[[1,40000000,false]],"mywTRsN56GipUEbmCnoUV9YFdrUU5CmUxd":[[0,19994800,false]]},"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d":{"n4FfEQrf1PS3no7FCPhhDugxqgR4fUSvdX":[[1,10000000,false]]},"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd":{},"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f":{},"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa":{"mqYXSDuvMpdibxUy6ftKW1564L9UE5eeFX":[[0,21958750,false]]},"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308":{"mpEGnkPKtMyfHo8EaUFks7xFZJdSgLjnC7":[[0,1000000,false]],"n23NSQfgAmVaW1qE1kgnxkW8JvWfveAktH":[[1,3999400,false]]},"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d":{"mrHCUTD63vsP9K3oon2AWZ9bKjqDhd5PMm":[[1,889890,false]]},"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216":{"myvhXvymTD4Ncgfg8r2WXiTZeDY3TtZGUY":[[1,5000000,false]]},"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254":{"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd":[[0,10000000,false]]},"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112":{},"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4":{"mgaArVGf5heGUko1i24wYrvkfFcN442U4v":[[0,4978900,false]]},"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a":{"mxTwRCnJgafjVaUVLsniWZPpXhz5dFzRyb":[[1,89998701,false]]},"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471":{"mfxZoA5dHaVn6QiHMbRwQTQUB7VFpZQFW4":[[1,100000,false]],"mhwQ9cxhAxaED747XuzgUo3F39MDc6txHr":[[0,9200,false]]},"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb":{"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd":[[0,1000000,false],[1,1000000,false],[2,1000000,false],[3,2000000,false],[5,10000000,false]]},"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d":{"mupBXEDhWQnrmyW4TukDs2qcqQrhRJGrQd":[[0,25000000,false]],"myJLELfyhG1Fu7iPyfpHfviVCQuLwsNCBm":[[1,34995259,false]]},"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462":{"mvXwR94pXVgP7kdrW3vTiDQtVrkP3NY3zn":[[0,1866000,false]],"mweezhmgY1kEvmSNpfrqdLcd6NHekupXKp":[[1,30000000,false]]},"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9":{"mycxtgambuph71Hg6MdwRoF7c7GkFvHiZ6":[[0,9638,false]]},"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a":{"mvJ2vrvNySAXWxVYuCn2tX6amyfchFGHvK":[[0,990400,false]]},"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968":{"mt2ijtY7BUkQRpErd99N9131YgsEL7oBSt":[[0,4999700,false]],"n1gdKukb5TUu37x5GahHsp4Gp2fdowdZPH":[[1,5000000,false]]},"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306":{"mp2CafXnWnN8rR6BnFToVQ8bXNY4jnAecr":[[1,689000,false]],"mwNWMKSGou8ZJzXgfDaAUy1C8Jip3TEmdf":[[0,100000,false]]},"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25":{"mmL4aJtiAPVUQca9AaZZdPUht9FvS2eb4a":[[1,789354,false]]}},"use_encryption":false,"verified_tx3":{"0f4972c84974b908a58dda2614b68cf037e6c03e8291898c719766f213217b67":[1230168,1510528889,25],"2791cdc98570cc2b6d9d5b197dc2d002221b074101e3becb19fab4b79150446d":[1231593,1511484570,30],"2d216451b20b6501e927d85244bcc1c7c70598332717df91bb571359c358affd":[1251055,1512046701,245],"31494e7e9f42f4bd736769b07cc602e2a1019617b2c72a03ec945b667aada78f":[1230627,1510871704,71],"336eee749da7d1c537fd5679157fae63005bfd4bb8cf47ae73600999cbc9beaa":[1242088,1511680407,146],"3a6ed17d34c49dfdf413398e113cf5f71710d59e9f4050bbc601d513a77eb308":[1231594,1511485793,27],"475c149be20c8a73596fad6cb8861a5af46d4fcf8e26a9dbf6cedff7ff80b70d":[1281357,1518388420,13],"56a65810186f82132cea35357819499468e4e376fca685c023700c75dc3bd216":[1231595,1511487012,215],"6ae728f783b0d4680ed8050c05419f0a89dfd6e28d46acfce7453b4d1b2b0254":[1230626,1510870499,1],"72419d187c61cfc67a011095566b374dc2c01f5397e36eafe68e40fc44474112":[1230191,1510536040,201],"76bcf540b27e75488d95913d0950624511900ae291a37247c22d996bb7cde0b4":[1251084,1512048610,1],"7f19935daa7347bdcf45f0bfd726dd665c514ffd49dfb035369813a54c1e5d1a":[1230624,1510868089,289],"8361b8f56b1ba8d9b59d5ef4547fe7203859e0c3107bc131b83fc1db12a11471":[1446399,1543859403,92],"9de08bcafc602a3d2270c46cbad1be0ef2e96930bec3944739089f960652e7cb":[1255526,1513816274,12],"a29d131e766950cae2e97dd4527b7c050293c2f5630470bdd7d00b7fe6db1b9d":[1230163,1510527129,10],"c1433779c5faec5df5e7bdc51214a95f15deeab842c23efbdde3acf82c165462":[1242088,1511680407,147],"c1fa83595c4c117da834569cba81d84d30ebf99fbdc96535f6e8afd5550e48b9":[1323966,1528290006,54],"c2595a521111eb0298e183e0a74befc91f6f93b03e2f7d43c7ad63a9196f7e3a":[1251074,1512047838,4],"e07bf0498d40d7b8d7933b1049dd54a1b21b7c5f6d8def98b0997e22761e0968":[1231594,1511485793,26],"e453e7346693b507561691b5ea73f8eba60bfc8998056226df55b2fac88ba306":[1297610,1526308364,61],"e87a207ff3f3d2601fd086e90fa81a8ecb9c15e6974e649d53b9ebc2b087af25":[1297404,1526137238,56]},"wallet_type":"standard","winpos-qt":[671,324,840,400]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_9_3_importedkeys(self): + async def test_upgrade_from_client_2_9_3_importedkeys(self): wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_9_3_watchaddresses(self): + async def test_upgrade_from_client_2_9_3_watchaddresses(self): wallet_str = '{"addr_history":{"1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf":[],"1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs":[],"1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa":[]},"addresses":["1H3mPXHFzA8UbvhQVabcDjYw3CPb3djvxs","1HocPduHmQUJerpdaLG8DnmxvnDCVQwWsa","1DgrwN2JCDZ6uPMSvSz8dPeUtaxLxWM2kf"],"pruned_txo":{},"seed_version":13,"stored_height":490039,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"verified_tx3":{},"wallet_type":"imported","winpos-qt":[499,386,840,405]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_9_3_trezor_singleacc(self): + async def test_upgrade_from_client_2_9_3_trezor_singleacc(self): wallet_str = '''{"addr_history":{"12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC":[],"146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz":[],"14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM":[],"15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ":[],"15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6":[],"15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S":[],"17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH":[],"18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw":[],"1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb":[],"1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX":[],"1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ":[],"1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp":[],"1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk":[],"1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD":[],"1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp":[],"1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz":[],"1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs":[],"1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid":[],"1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu":[],"1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo":[],"1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj":[],"1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv":[],"1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC":[],"1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe":[],"1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL":[],"1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG":[]},"addresses":{"change":["1ES8hmtgXFLRex71CZHu85cLFRYDczeTZ","14Co2CRVu67XLCGrD4RVVpadtoXcodUUWM","1rDkHFozR7kC7MxRiakx3mBeU1Fu6BRbG","15sFkiVrGad5QiKgtYjfgi8SSeEfRzxed6","1NZs4y3cJhukVdKSYDhaiMHhP4ZU2qVpAL","1KotB3FVFcYuHAVdRNAe2ZN1MREpVWnBgs"],"receiving":["1LpV3F25jiNWV8N2RPP1cnKGgpjZh2r8xu","18TKpsznSha4VHLzpVatnrEBdtWkoQSyGw","17YQXYHoDqcpd7GvWN9BYK8FnDryhYbKyH","12sQvVXgdoy2QDorLgr2t6J8JVzygBGueC","15KDqFhdXP6Zn4XtJVVVgahJ7chw9jGhvQ","1Le4rXQD4kMGsoet4EH8VGzt5VZjdHBpid","1KnQX5D5Tv2u5CyWpuXaeM8CvuuVAmfwRz","1MrA1WS4iWcTjLrnSqNNpXzSq5W92Bttbj","146j6RMbWpKYEaGTdWVza3if3bnCD9Maiz","1NMkEhuUYsxTCkfq9zxxCTozKNNqjHeKeC","1Mdq8bVFSBfaeH5vjaXGjiPiy6qPVtdfUo","1BngGArwhpzWjCREXYRS1uhUGszCTe7vqb","1NTRF8Y7Mu57dQ9TFwUA98EdmzbAamtLYe","1NFhYYBh1zDGdnqD1Avo9gaVV8LvnAH6iv","1J2NdSfFiQLhkHs2DVyBmB47Mk65rfrGPp","15zoPN5rVKDCsKnZUkTYJWFv4gLdYTat8S","1E9wSjSWkFJp3HUaUzUF9eWpCkUZnsNCuX","1FdV7zK6RdRAKqg3ccGHGK51nJLUwpuBFp","1GjFaGxzqK12N2F7Ao49k7ZvMApCmK7Enk","1HkHDREiY3m9UCxaSAZEn1troa3eHWaiQD"]},"keystore":{"derivation":"m/44'/0'/0'","hw_type":"trezor","label":"trezor1","type":"hardware","xpub":"xpub6BycoSLDNcWjBQMuYgSaEoinupMjma8Cu2uj4XiRCZkecLHXXmzcxbyR1gdfrZpiZDVSs92MEGGNhF78BEbbYi2b5U2oPnaUPRhjriWz85y"},"pruned_txo":{},"seed_version":13,"stored_height":490014,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[753,486,840,405]}''' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_2_9_3_multisig(self): + async def test_upgrade_from_client_2_9_3_multisig(self): wallet_str = '{"addr_history":{"31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3":[],"32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe":[],"33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw":[],"33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm":[],"33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA":[],"35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs":[],"36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE":[],"37rZuTsieKVpRXshwrY8qvFBn6me42mYr5":[],"38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V":[],"38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm":[],"3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM":[],"3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5":[],"3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7":[],"3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN":[],"3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn":[],"3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS":[],"3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC":[],"3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6":[],"3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p":[],"3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c":[],"3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va":[],"3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i":[],"3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M":[],"3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ":[],"3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi":[],"3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J":[]},"addresses":{"change":["31uiqKhw4PQSmZWnCkqpeh6moB8B1jXEt3","3JhkakvHAyFvukJ3cyaVgiyaqjYNo2gmsS","3GK4CBbgwumoeR9wxJjr1QnfnYhGUEzHhN","3LAPqjqW4C2Se9HNziUhNaJQS46X1r9p3M","33MQEs6TCgxmAJhZvUEXYr6gCkEoEYzUfm","3AEtxrCwiYv5Y5CRmHn1c5nZnV3Hpfh5BM"],"receiving":["3P3JJPoyNFussuyxkDbnYevYim5XnPGmwZ","33FQMD675LMRLZDLYLK7QV6TMYA1uYW1sw","3DAD19hHXNxAfZjCtUbWjZVxw1fxQqCbY7","3AaHWprY1MytygvQVDLp6i63e9o5CwMSN5","3H18xmkyX3XAb5MwucqKpEhTnh3qz8V4Mn","36zhHEtGA33NjHJdxCMjY6DLeU2qxhiLUE","37rZuTsieKVpRXshwrY8qvFBn6me42mYr5","38A2KDXYRmRKZRRCGgazrj19i22kDr8d4V","38GZH5GhxLKi5so9Aka6orY2EDZkvaXdxm","33vuhs2Wor9Xkax66ucDkscPcU6nQHw8LA","3L5aZKtDKSd65wPLMRooNtWHkKd5Mz6E3i","3KXe1z2Lfk22zL6ggQJLpHZfc9dKxYV95p","3KQosfGFGsUniyqsidE2Y4Bz1y4iZUkGW6","3KZiENj4VHdUycv9UDts4ojVRsaMk8LC5c","32PBjkXmwRoEQt8HBZcAEUbNwaHw5dR5fe","3KeTKHJbkZN1QVkvKnHRqYDYP7UXsUu6va","3JtA4x1AKW4BR5YAEeLR5D157Nd92NHArC","3PgNdMYSaPRymskby885DgKoTeA1uZr6Gi","3Pm7DaUzaDMxy2mW5WzHp1sE9hVWEpdf7J","35tbMt1qBGmy5RNcsdGZJgs7XVbf5gEgPs"]},"pruned_txo":{},"seed_version":13,"stored_height":485855,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"2of2","winpos-qt":[617,227,840,405],"x1/":{"seed":"speed cruise market wasp ability alarm hold essay grass coconut tissue recipe","type":"bip32","xprv":"xprv9s21ZrQH143K48ig2wcAuZoEKaYdNRaShKFR3hLrgwsNW13QYRhXH6gAG1khxim6dw2RtAzF8RWbQxr1vvWUJFfEu2SJZhYbv6pfreMpuLB","xpub":"xpub661MyMwAqRbcGco98y9BGhjxscP7mtJJ4YB1r5kUFHQMNoNZ5y1mptze7J37JypkbrmBdnqTvSNzxL7cE1FrHg16qoj9S12MUpiYxVbTKQV"},"x2/":{"type":"bip32","xprv":null,"xpub":"xpub661MyMwAqRbcGrCDZaVs9VC7Z6579tsGvpqyDYZEHKg2MXoDkxhrWoukqvwDPXKdxVkYA6Hv9XHLETptfZfNpcJZmsUThdXXkTNGoBjQv1o"}}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) - def test_upgrade_from_client_3_2_3_ledger_standard_keystore_changes(self): + async def test_upgrade_from_client_3_2_3_ledger_standard_keystore_changes(self): # see #6066 wallet_str = '''{"addr_history":{"bc1q0k4hemnmw5czyq7yyka5mpc3hvz37lk0urhd34":[],"bc1q2tgeuhkr85pjkrys44zn2a7lfap0g8u7ny68p3":[],"bc1q2xm8slpsqlt47u0j7segcsfmaq6s4s2pvx2526":[],"bc1q4dhwcvnnm8a0umt4gvn2tatq66qf9d37rx5t8u":[],"bc1q4pqe6tcfyl8m35myj9trz7fn4w0kdpljnt3sxd":[],"bc1q5l345sqf8fhlurn4hgu8dxu0w76j5tf3kc2f7h":[],"bc1q5nd447vdf9gx0l8xmj500wr859pny29xurgcpn":[],"bc1qaa2xnanrpmttw35gc4xqvz9ldz5sggqvc2ed72":[],"bc1qav4zrnx5g4s5h2z5hzr9hncg8qwt96ezltepmp":[],"bc1qcxryu22d3k66k4l55dzupvx45e88lmvp3rcww3":[],"bc1qd4us67486cn5qy44z6v6ervv5cuzrykq0vlcw2":[],"bc1qdd773rd9p8t3eylv2gvs2tmn2n79pwcfz65uyp":[],"bc1qdwafv8hy0cx9utkkj4hs6ntafm3x95m9zgpgvn":[],"bc1qehqt2um35x0c49snyugf94hvh7jz3vcjt0ya6m":[],"bc1qex23ueucc9hxyxgk3jg8ahw7cgw954legfnrxg":[],"bc1qf4tx5eesmrcy478gk384s2jv4lfh9dwt9jws0e":[],"bc1qh9l2au0f6m2fl3l3qa6perw5xnpvjul8lyylkt":[],"bc1qkmprcg50zcsdd0p3w70w2rxs5hwmwwn2xd0ls9":[],"bc1qkztpz05djsatmxxafgjqqldp0yfs8knr6um3e4":[],"bc1qrgj0zygryl6edylgm6gzx5j9rghdufrn5fp6hw":[],"bc1qscxh3na5uqapjm006xmg4s0geurq7nw427ywca":[],"bc1qunqye3f6cw88wqsjkks7amskder0rvufu49l6e":[],"bc1qv077qy5udlr3q8ammxq9ecq57vh9lxjnwh0vy9":[],"bc1qw9nqstryl3e0e49jg6670u6mu8507takz66qgv":[],"bc1qx4neqay68lmvgrav3yslzuempv9xn7aqdks5r6":[],"bc1qzhwpu84e5ajet4mxxr9ylc0fwass3q5k32uj5u":[]},"addresses":{"change":["bc1qdd773rd9p8t3eylv2gvs2tmn2n79pwcfz65uyp","bc1qv077qy5udlr3q8ammxq9ecq57vh9lxjnwh0vy9","bc1qx4neqay68lmvgrav3yslzuempv9xn7aqdks5r6","bc1qh9l2au0f6m2fl3l3qa6perw5xnpvjul8lyylkt","bc1qw9nqstryl3e0e49jg6670u6mu8507takz66qgv","bc1qaa2xnanrpmttw35gc4xqvz9ldz5sggqvc2ed72"],"receiving":["bc1qav4zrnx5g4s5h2z5hzr9hncg8qwt96ezltepmp","bc1qzhwpu84e5ajet4mxxr9ylc0fwass3q5k32uj5u","bc1qehqt2um35x0c49snyugf94hvh7jz3vcjt0ya6m","bc1q0k4hemnmw5czyq7yyka5mpc3hvz37lk0urhd34","bc1qf4tx5eesmrcy478gk384s2jv4lfh9dwt9jws0e","bc1q2xm8slpsqlt47u0j7segcsfmaq6s4s2pvx2526","bc1q5nd447vdf9gx0l8xmj500wr859pny29xurgcpn","bc1qex23ueucc9hxyxgk3jg8ahw7cgw954legfnrxg","bc1qscxh3na5uqapjm006xmg4s0geurq7nw427ywca","bc1qdwafv8hy0cx9utkkj4hs6ntafm3x95m9zgpgvn","bc1qkmprcg50zcsdd0p3w70w2rxs5hwmwwn2xd0ls9","bc1qunqye3f6cw88wqsjkks7amskder0rvufu49l6e","bc1q5l345sqf8fhlurn4hgu8dxu0w76j5tf3kc2f7h","bc1q4pqe6tcfyl8m35myj9trz7fn4w0kdpljnt3sxd","bc1qkztpz05djsatmxxafgjqqldp0yfs8knr6um3e4","bc1q4dhwcvnnm8a0umt4gvn2tatq66qf9d37rx5t8u","bc1q2tgeuhkr85pjkrys44zn2a7lfap0g8u7ny68p3","bc1qrgj0zygryl6edylgm6gzx5j9rghdufrn5fp6hw","bc1qd4us67486cn5qy44z6v6ervv5cuzrykq0vlcw2","bc1qcxryu22d3k66k4l55dzupvx45e88lmvp3rcww3"]},"keystore":{"cfg":{"mode":0,"pair":""},"derivation":"m/84'/0'/0'","hw_type":"ledger","label":"","type":"hardware","xpub":"zpub6qmVsnBYWipPzoeuZwtVeVnC42achPEZpGopT7jsop5WgDuFqKT3aS3EuAAQ6G76wbwtvDMdzffwxyEtwa6iafXSgjW2RjraiXfsgxQHnz8"},"seed_version":18,"spent_outpoints":{},"stored_height":646576,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[168,276,840,400]}''' - db = self._upgrade_storage(wallet_str) + db = await self._upgrade_storage(wallet_str) wallet = Wallet(db, None, config=self.config) ks = wallet.keystore # to simulate ks.opportunistically_fill_in_missing_info_from_device(): ks._root_fingerprint = "deadbeef" ks.is_requesting_to_be_rewritten_to_wallet_file = True - asyncio.run_coroutine_threadsafe(wallet.stop(), self.asyncio_loop).result() + await wallet.stop() - def test_upgrade_from_client_2_9_3_importedkeys_keystore_changes(self): + async def test_upgrade_from_client_2_9_3_importedkeys_keystore_changes(self): # see #6401 wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' - db = self._upgrade_storage(wallet_str) + db = await self._upgrade_storage(wallet_str) wallet = Wallet(db, None, config=self.config) wallet.import_private_keys( ["p2wpkh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY"], password=None ) - asyncio.run_coroutine_threadsafe(wallet.stop(), self.asyncio_loop).result() + await wallet.stop() @as_testnet - def test_upgrade_from_client_3_3_8_xpub_with_realistic_history(self): + async def test_upgrade_from_client_3_3_8_xpub_with_realistic_history(self): wallet_str = '{"addr_history":{"tb1q04m5vxgzsctgn8kgyfxcen3pqxdr2yx53vzwzl":[["866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff",1772350]],"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3":[["5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b",1746825],["6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b",1772251]],"tb1q0k62kjqt053p37a9v8lnqstc7jhuhjtjphw3h7":[],"tb1q0l3cxy8xs8ujxm6cv9h2xgra430rwaw2xux6qe":[],"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj":[["4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e",1665679],["0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0",1665687]],"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq":[["cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52",1612648],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv":[["b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5",1612648],["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802",1612704]],"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd":[["fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa",1772346],["901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366",1772347]],"tb1q27juqmgmq2749wylmyqk00lvx9mgaz4k5nfnud":[["b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94",1744777]],"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0":[["133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62",1607022],["d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5",1607022]],"tb1q2qsa3ygu6l2z2kyvgwpnnmurym99m9duelc2hf":[],"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r":[["442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3",1747567],["c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295",1747569]],"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl":[["ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2",1667168],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t":[["01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61",1413374],["ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6",1612788]],"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0":[["600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54",1747720],["42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab",1772251]],"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4":[["442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3",1747567],["781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05",1774902]],"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h":[["56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b",1612072],["dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137",1666106]],"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t":[["2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d",1665679],["62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38",1665815]],"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj":[["1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04",1638861],["4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e",1665679]],"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s":[["38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3",1666768],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev":[["d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156",1413150],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw":[["a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867",1612009],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c":[["b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7",1612648],["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802",1612704]],"tb1q6z3t5mrmhwqu0gw3kpwrpcfzpezcha3j8xpdma":[],"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r":[["48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a",1666551],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4":[["b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5",1612648],["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802",1612704]],"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f":[["75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3",1774146],["d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3",1774752]],"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3":[["feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877",1772375],["75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3",1774146]],"tb1q8564fhyum66n239wt0gp8m0khlqgwgac8ft2r0":[["48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6",1747720]],"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w":[["94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d",1665679],["ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2",1667168]],"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2":[["81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a",1772346],["955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514",1772348]],"tb1q8zwvzwh8tnthf3d2qsxpyemu5mwwddysy5pxc2":[],"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn":[["42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab",1772251],["feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877",1772375]],"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658":[["9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd",1612004],["0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687",1612005]],"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7":[["b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7",1612648],["2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d",1665679]],"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts":[["43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15",1746271],["7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d",1746274]],"tb1q98enaj75ssnrjjkx3svkm5jg8af65u44rx5pnp":[],"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4":[["d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee",1746834],["7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506",1747567]],"tb1q9jtcype5swm4reyz4sktvq609shw88fwzjz9jg":[["26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a",1774145]],"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg":[["8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c",1584541],["b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5",1612648]],"tb1q9mqvf4w35aljtgl4p6kca9xnhesc4l7vpamdp0":[],"tb1qa4gcpuzu0vwunqrnycpv2rx6gpfsuq9d4sg25y":[],"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc":[["43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35",1607959],["cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52",1612648]],"tb1qa6dgfxczcjshyhv6d4ck0qvs3mgdjd2gpdqqzj":[["7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506",1747567]],"tb1qadwk4rmwxcayxg27f6cpv46dusyksk273pq09u":[],"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw":[["b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94",1744777],["a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9",1744791]],"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6":[],"tb1qalw5ax88hnr6gse240prn370020uxq505tw3n8":[],"tb1qaqkakr58cs8jq7zyhx4dwt8maemadrfnwevsc9":[],"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9":[["a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9",1744791],["43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15",1746271]],"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43":[["85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834",1747541],["d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057",1747567]],"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6":[["1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04",1638861],["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4",1663206]],"tb1qc5ztxm2kvtrtxun50v0rn6asm9tv0t3mfzh68v":[],"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l":[],"tb1qcmmu23wur97duygz524t07s40gdxzgc4kfpkp5":[["94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9",1747721]],"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl":[["50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc",1721735],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qczlwxujnm7hg4559hhsywfc993my4807p50qdm":[],"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28":[["8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c",1584541],["b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7",1612648]],"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0":[["dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c",1746274],["5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b",1746825]],"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7":[["366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f",1746833],["6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a",1746834],["442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3",1747567]],"tb1qdlv5pjqdk27x04m6xte3kcsz9h2euuylhv4tgl":[],"tb1qdn5xyyvkr5y20p6sfynturxhpd9gx24maw4n98":[],"tb1qe7wv04mlsg7hkarsdx07jgr7mgs80pe6nl87sq":[],"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx":[["c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0",1584540],["8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c",1584541]],"tb1qevkywexfy5gnydx0mrsrrthzncymydc0zz4rqx":[["c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295",1747569]],"tb1qez8zr7fjzdln2fmgyepmmd2jkcnkrvg3czqqn9":[],"tb1qflmaysgnsh9acv6ewxj08eur45lgsrrdgmvxz2":[],"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf":[["62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38",1665815],["22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f",1692449]],"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd":[["3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549",1606858],["cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52",1612648]],"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya":[["0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0",1665687],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qg8nggy5f8avhucghpc8h5garasdqfwv37nh7tc":[],"tb1qgas5dck220ygdafe3ehhpgqpclve8j5a0c2cs2":[],"tb1qgg2avhyk30s8a0n72t8sm3cggdmqgdutdvwfa8":[["781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05",1774902]],"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p":[["40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1",1665686],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7":[["ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2",1667168],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qh6qyzl3pds56azgjqkhkk7kfkavzxghjwsv0rl":[],"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu":[["6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b",1772251],["42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab",1772251]],"tb1qhksthm48t4eqrzup3gzzlqnf433z8aq5uj03jr":[["4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e",1747720]],"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw":[["bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9",1607028],["09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be",1609187]],"tb1qhnpmvc5uvgw28tvtyv9nr5ckx3fz48lrd94mym":[],"tb1qhvpcyyj29tt2rtpe693whfse5hpzh4r7ums7zv":[],"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s":[["b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2",1665693],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0":[["d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82",1772251],["81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a",1772346]],"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw":[["d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67",1774477],["6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc",1774910]],"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9":[["0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0",1772375],["d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67",1774477]],"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc":[["0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d",1666105],["4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e",1666106]],"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5":[["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4",1663206],["94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d",1665679]],"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe":[["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802",1612704],["f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d",1636331]],"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn":[["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4",1663206],["50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc",1721735]],"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9":[["18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf",1772648],["d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67",1774477]],"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq":[["62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38",1665815],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qmy2luym2mtyydcwlmf7gxpe30k0cp8gt7gj63k":[],"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu":[["7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55",1772374],["feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877",1772375]],"tb1qn7e3uh2lfrmxheg0t5gvm2cqrn3cqgurz4as76":[],"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt":[["c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295",1747569],["94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9",1747721]],"tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh":[["6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc",1774910]],"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c":[["cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52",1612648],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qpntvn7xwp2nn6alu7lc4d360tjlvdyrtzf02xh":[],"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn":[["09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be",1609187],["9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e",1609312]],"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp":[["4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755",1607028],["09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be",1609187]],"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6":[["ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271",1609187],["9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd",1612004]],"tb1qqmvztjh4yg6x5fgw3wq685zkna5jv0e06v4ee4":[],"tb1qr6g9qrkssn822tklp83accz4j9s4sat9g068g3":[],"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr":[["69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1",1413150],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qrln929gz055hse2ylytl3cxnse6wxshek97t7j":[],"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw":[["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4",1663206],["94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d",1665679]],"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp":[["0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761",1772346],["e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336",1772347]],"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x":[["0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687",1612005],["a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867",1612009]],"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp":[["ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21",1772346],["9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2",1772347]],"tb1qsyhawdg9zj2cepa0zg096rna2nxg4zj0c0fnvq":[["19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801",1772373]],"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr":[["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802",1612704],["2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d",1665679]],"tb1qtgsfkgptcxdn6dz6wh8c4dguk3cezwne5j5c47":[],"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y":[["e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135",1746825],["600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54",1747720]],"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh":[["7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d",1746274],["dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c",1746274]],"tb1quneg2cv7v8ne9z64whgcvg6hzwhxuselhpak3e":[],"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg":[["934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5",1607022],["9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e",1609312]],"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta":[["f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d",1636331],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv":[["955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514",1772348],["866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff",1772350]],"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp":[["0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca",1609182],["ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271",1609187]],"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4":[["9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e",1609312],["1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04",1638861]],"tb1qwu3708q32l7wdcvfhf9vfhgazp8yzggf5x4y72":[["7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506",1747567]],"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z":[["d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5",1607022],["934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5",1607022]],"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0":[["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777],["b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94",1744777]],"tb1qxx7t6g3dpts4ytlzetdqv8e04qdal36xg9d7zc":[["d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057",1747567]],"tb1qy5xx4uyqv6yhq9eptha8n5shqj94vqw7euftmk":[["d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3",1774752]],"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw":[["a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9",1744791],["e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135",1746825]],"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c":[],"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33":[["ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6",1612788],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p":[["65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc",1746833],["d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057",1747567]],"tb1qyprqlu8dn88d4kgk5yldruvg96tjamups3ww69":[],"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7":[["72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390",1692476],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]],"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g":[["674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4",1638866],["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067",1744777]]},"addresses":{"change":["tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28","tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z","tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg","tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn","tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6","tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658","tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4","tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x","tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw","tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7","tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c","tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4","tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe","tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33","tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta","tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj","tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5","tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w","tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t","tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj","tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya","tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq","tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw","tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl","tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9","tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts","tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh","tb1qczlwxujnm7hg4559hhsywfc993my4807p50qdm","tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0","tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp","tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y","tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3","tb1qxx7t6g3dpts4ytlzetdqv8e04qdal36xg9d7zc","tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r","tb1qwu3708q32l7wdcvfhf9vfhgazp8yzggf5x4y72","tb1qevkywexfy5gnydx0mrsrrthzncymydc0zz4rqx","tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0","tb1qcmmu23wur97duygz524t07s40gdxzgc4kfpkp5","tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu","tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp","tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn","tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd","tb1qn7e3uh2lfrmxheg0t5gvm2cqrn3cqgurz4as76","tb1q98enaj75ssnrjjkx3svkm5jg8af65u44rx5pnp","tb1qgas5dck220ygdafe3ehhpgqpclve8j5a0c2cs2","tb1qyprqlu8dn88d4kgk5yldruvg96tjamups3ww69","tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv","tb1qflmaysgnsh9acv6ewxj08eur45lgsrrdgmvxz2","tb1q04m5vxgzsctgn8kgyfxcen3pqxdr2yx53vzwzl","tb1qez8zr7fjzdln2fmgyepmmd2jkcnkrvg3czqqn9","tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3","tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f","tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw","tb1qhnpmvc5uvgw28tvtyv9nr5ckx3fz48lrd94mym","tb1qaqkakr58cs8jq7zyhx4dwt8maemadrfnwevsc9","tb1qy5xx4uyqv6yhq9eptha8n5shqj94vqw7euftmk","tb1qgg2avhyk30s8a0n72t8sm3cggdmqgdutdvwfa8","tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh","tb1q0l3cxy8xs8ujxm6cv9h2xgra430rwaw2xux6qe","tb1q6z3t5mrmhwqu0gw3kpwrpcfzpezcha3j8xpdma","tb1qdn5xyyvkr5y20p6sfynturxhpd9gx24maw4n98","tb1q9mqvf4w35aljtgl4p6kca9xnhesc4l7vpamdp0","tb1qhvpcyyj29tt2rtpe693whfse5hpzh4r7ums7zv","tb1qg8nggy5f8avhucghpc8h5garasdqfwv37nh7tc"],"receiving":["tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx","tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t","tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg","tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev","tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr","tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0","tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp","tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd","tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw","tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp","tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq","tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc","tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr","tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv","tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c","tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g","tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p","tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h","tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6","tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw","tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn","tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s","tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf","tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl","tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc","tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r","tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s","tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7","tb1qa6dgfxczcjshyhv6d4ck0qvs3mgdjd2gpdqqzj","tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7","tb1q27juqmgmq2749wylmyqk00lvx9mgaz4k5nfnud","tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0","tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4","tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7","tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p","tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43","tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4","tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw","tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt","tb1qhksthm48t4eqrzup3gzzlqnf433z8aq5uj03jr","tb1q8564fhyum66n239wt0gp8m0khlqgwgac8ft2r0","tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0","tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2","tb1qsyhawdg9zj2cepa0zg096rna2nxg4zj0c0fnvq","tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu","tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9","tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9","tb1q9jtcype5swm4reyz4sktvq609shw88fwzjz9jg","tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l","tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6","tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c","tb1qh6qyzl3pds56azgjqkhkk7kfkavzxghjwsv0rl","tb1qmy2luym2mtyydcwlmf7gxpe30k0cp8gt7gj63k","tb1qtgsfkgptcxdn6dz6wh8c4dguk3cezwne5j5c47","tb1qe7wv04mlsg7hkarsdx07jgr7mgs80pe6nl87sq","tb1qrln929gz055hse2ylytl3cxnse6wxshek97t7j","tb1qadwk4rmwxcayxg27f6cpv46dusyksk273pq09u","tb1qdlv5pjqdk27x04m6xte3kcsz9h2euuylhv4tgl","tb1quneg2cv7v8ne9z64whgcvg6hzwhxuselhpak3e","tb1qa4gcpuzu0vwunqrnycpv2rx6gpfsuq9d4sg25y","tb1qqmvztjh4yg6x5fgw3wq685zkna5jv0e06v4ee4","tb1qpntvn7xwp2nn6alu7lc4d360tjlvdyrtzf02xh","tb1qr6g9qrkssn822tklp83accz4j9s4sat9g068g3","tb1q0k62kjqt053p37a9v8lnqstc7jhuhjtjphw3h7","tb1qalw5ax88hnr6gse240prn370020uxq505tw3n8","tb1q8zwvzwh8tnthf3d2qsxpyemu5mwwddysy5pxc2","tb1q2qsa3ygu6l2z2kyvgwpnnmurym99m9duelc2hf","tb1qc5ztxm2kvtrtxun50v0rn6asm9tv0t3mfzh68v"]},"invoices":{"c58dbd42b883d60433d9fb626b772406":{"hex":"0801120b783530392b7368613235361a9f160ac50c3082064130820529a003020102020900e1222c5cacc7e4c2300d06092a864886f70d01010b05003081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d204732301e170d3138303830323138343432305a170d3139313030313231313333385a303d3121301f060355040b1318446f6d61696e20436f6e74726f6c2056616c696461746564311830160603550403130f746573742e6269747061792e636f6d30820122300d06092a864886f70d01010105000382010f003082010a0282010100d14377b6f2b50aa73427c248915563d2c8804dd824b1d8f015dd1ae2a390c8022fd963355849b217309208c3450a016ff4001ee336a3189edadce12bc41e4999e45f6fcf0b00daa7e9a4b12e72dce19c51ae8e55b8bfec02b80a58ccb2c1b3e5bde81f679b9993bf52c871a1fadef6bb5f7d8d9208889400ba2be1d2baf82ec303470852570c3bbb6b89334d8974e61b8867bf299fb802c57e9b00d9b9f70572ac6d81fecd304c83aaf21f4f3b529e9898ea9b868f8f07b4189668e71854ae776bacd0d9706a8be03f528c68ad023e3b45bfa55e9b42e535aafc7eb8672645dcdeaf7204a468d2b84f27ed12072a411627647108e421abe7308e3bac305896f10203010001a38202ca308202c6300c0603551d130101ff04023000301d0603551d250416301406082b0601050507030106082b06010505070302300e0603551d0f0101ff0404030205a030370603551d1f0430302e302ca02aa0288626687474703a2f2f63726c2e676f64616464792e636f6d2f676469673273312d3835342e63726c305d0603551d20045630543048060b6086480186fd6d010717013039303706082b06010505070201162b687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f3008060667810c010201307606082b06010505070101046a3068302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f304006082b060105050730028634687474703a2f2f6365727469666963617465732e676f64616464792e636f6d2f7265706f7369746f72792f67646967322e637274301f0603551d2304183016801440c2bd278ecc348330a233d7fb6cb3f0b42c80ce302f0603551d1104283026820f746573742e6269747061792e636f6d82137777772e746573742e6269747061792e636f6d301d0603551d0e041604142cd8def7d64c620cce78bdec365fd961cfd035e130820104060a2b06010401d6790204020481f50481f200f0007700a4b90990b418581487bb13a2cc67700a3c359804f91bdfb8e377cd0ec80ddc1000000164fbf55c500000040300483046022100d6a82312b4a16c7ed7911a2e559168ea77d01e3dcf9cfe63a6662e2661546405022100a43fe7cbe2fd9ae72a349ba1b19de71e7804cae2c3755774c4b8de4d432cbcd5007500747eda8331ad331091219cce254f4270c2bffd5e422008c6373579e6107bcc5600000164fbf55ed7000004030046304402201727366c115d0fb8f940cce989730727cc59d15c94120146f1c239989deeecaa0220602b946de08661ed7ff3ed64988a3d872151c89f20a9c0dbbc6edcc04870380a300d06092a864886f70d01010b05000382010100060cf534f7adea711fabcd9338fa89f4bc244da0a6af232f7d337f2d8ea79394343b5f5dfb07c3f2e71195755e0a8f3f51b0444e7e17b4b926cb0f9dc49253dc66f1ed8fd52297be8b515a4240ac59fc3f7f1fa810ab24b196c4ae57827abf25e76838edc0a86bfdd0c386f9c4b6a7fa94672b79177646323875cfff2da42106be46ff0e1b41e3b5706f1be19b582005fbedacf88d69370a2ccda66bffe6e7ef5f33a408707ebd3ddc20b1ff24d6bd94cc6db5e069277d78d56218cfddbfcd83f2be5e477734f548a6da0ae58bed212bf817914f3fe6f9afa4ad59fe5d97b0514e277dc6ba48672e074b23fe89a5f72c58ec139b3f1c09d557b6e7d2df5f52880ad409308204d0308203b8a003020102020107300d06092a864886f70d01010b0500308183310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e3131302f06035504031328476f20446164647920526f6f7420436572746966696361746520417574686f72697479202d204732301e170d3131303530333037303030305a170d3331303530333037303030305a3081b4310b30090603550406130255533110300e060355040813074172697a6f6e61311330110603550407130a53636f74747364616c65311a3018060355040a1311476f44616464792e636f6d2c20496e632e312d302b060355040b1324687474703a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f313330310603550403132a476f2044616464792053656375726520436572746966696361746520417574686f72697479202d20473230820122300d06092a864886f70d01010105000382010f003082010a0282010100b9e0cb10d4af76bdd49362eb3064b881086cc304d962178e2fff3e65cf8fce62e63c521cda16454b55ab786b63836290ce0f696c99c81a148b4ccc4533ea88dc9ea3af2bfe80619d7957c4cf2ef43f303c5d47fc9a16bcc3379641518e114b54f828bed08cbef030381ef3b026f86647636dde7126478f384753d1461db4e3dc00ea45acbdbc71d9aa6f00dbdbcd303a794f5f4c47f81def5bc2c49d603bb1b24391d8a4334eeab3d6274fad258aa5c6f4d5d0a6ae7405645788b54455d42d2a3a3ef8b8bde9320a029464c4163a50f14aaee77933af0c20077fe8df0439c269026c6352fa77c11bc87487c8b993185054354b694ebc3bd3492e1fdcc1d252fb0203010001a382011a30820116300f0603551d130101ff040530030101ff300e0603551d0f0101ff040403020106301d0603551d0e0416041440c2bd278ecc348330a233d7fb6cb3f0b42c80ce301f0603551d230418301680143a9a8507106728b6eff6bd05416e20c194da0fde303406082b0601050507010104283026302406082b060105050730018618687474703a2f2f6f6373702e676f64616464792e636f6d2f30350603551d1f042e302c302aa028a0268624687474703a2f2f63726c2e676f64616464792e636f6d2f6764726f6f742d67322e63726c30460603551d20043f303d303b0604551d20003033303106082b06010505070201162568747470733a2f2f63657274732e676f64616464792e636f6d2f7265706f7369746f72792f300d06092a864886f70d01010b05000382010100087e6c9310c838b896a9904bffa15f4f04ef6c3e9c8806c9508fa673f757311bbebce42fdbf8bad35be0b4e7e679620e0ca2d76a637331b5f5a848a43b082da25d90d7b47c254f115630c4b6449d7b2c9de55ee6ef0c61aabfe42a1bee849eb8837dc143ce44a713700d911ff4c813ad8360d9d872a873241eb5ac220eca17896258441bab892501000fcdc41b62db51b4d30f512a9bf4bc73fc76ce36a4cdd9d82ceaae9bf52ab290d14d75188a3f8a4190237d5b4bfea403589b46b2c3606083f87d5041cec2a190c3bbef022fd21554ee4415d90aaea78a33edb12d763626dc04eb9ff7611f15dc876fee469628ada1267d0a09a72e04a38dbcf8bc0430012294020a0474657374121f08848e06121976a914314f713cd59781894277dcfdd31b5f179860b61e88ac18dbe793f80520dfee93f8052a5a5061796d656e74207265717565737420666f722042697450617920696e766f69636520526452684a4e3334426f45417a443151614c7067524b20666f72206d65726368616e7420536f6d6265724e696768745f74657374696e67323068747470733a2f2f746573742e6269747061792e636f6d2f692f526452684a4e3334426f45417a443151614c7067524b3a4c7b22696e766f6963654964223a22526452684a4e3334426f45417a443151614c7067524b222c226d65726368616e744964223a225372384b5774647158666b6a58563751704171773336227d450000803f2a80022896c21e381d423cd31672981c90f65919db4e30dd5a2f62b9be1e73a9a875ba176086e66507aeb67ad4b1c5cfd6bab57b49a6b8895b12bf14dbe868dfacc3ce04e5617c96bd1a4eb8c5fcb14122e569dfb9d860d67ed8d494f0a348a3e6ea00d27aab41c18ea625b872411543a52db374f97f7f01ed3df3be5f33551661ecb83dbab311d17d8942925934a0eab7965bb3e8a908bedd82001c6f1afe2ebca2616da288716452da8f4f32ca625a34f3f52b04b7ff32816b4875cfe3789e132ae1959d2a68d34aaed937c60b268cd7737889981071431e406318fd3619615d84647a0b900060f997ed8aa7ce50f65212d9d654409ae77001d3952b784a687a4082","requestor":"test.bitpay.com","txid":null},"tb1qh6qyzl3pds56azgjqkhkk7kfkavzxghjwsv0rl":{"hex":"2229121c08f8c70d12160014be80417e216c29ae891205af6b7ac9b7582322f2180020002a0574657374742a00","requestor":null,"txid":null}},"keystore":{"pw_hash_version":1,"type":"bip32","xprv":null,"xpub":"vpub5VmsevU91fpRaJkfa8b6c9MK53gKY8rSzZjrZdp6dkHZjnFhM1HN74ezHY96JCgFnbQJhRbeUyr5S1vzdcTB6qUKrrG7GBuwPYDTzBjLQmv"},"labels":{"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6":"123","tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l":"asdasd","tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c":"sfsafas"},"payment_requests":{"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6":{"address":"tb1qaj8j0ldjswlmtamfxl3kgrq7gcc7nqnun4dwn6","amount":20000,"exp":null,"id":"dbc868ee2e","memo":"123","time":1594159917},"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l":{"address":"tb1qckp4ztmstwtyxzml3dmfvegeq5mfxwu2h3q94l","amount":410000,"exp":null,"id":"aa668f9d93","memo":"asdasd","time":1594159909},"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c":{"address":"tb1qy9986nnmd8dhznxfs9sh9c5nknnj0s4vwgky6c","amount":0,"exp":null,"id":"4329cf1c01","memo":"sfsafas","time":1594159923}},"seed_type":"segwit","seed_version":18,"spent_outpoints":{"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61":{"1":"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6"},"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761":{"0":"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336"},"0448b48ce3cf3265619f4f915b2bbb8cab661666decdf5df805fbf884679c51e":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","1":"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82"},"07412f8a52ec8d3a58f9911daeccfb4164a368d5d8e36f354a72edf722119415":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"09a4c03e3bdf83bbe3955f907ee52da4fc12f4813d459bc75228b64ad08617c7":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be":{"0":"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e"},"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0":{"0":"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67"},"0b39f916a3889c69981d9285bade5f078cbb07e74502311d9c2417a7a638de52":{"0":"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801"},"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d":{"0":"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e"},"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0":{"0":"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2","1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687":{"1":"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867"},"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca":{"1":"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271"},"11769927f369180fac9e3d728d084c46aa0b8bddef99d4ea85e580d3dc1c30e9":{"1":"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e"},"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","2":"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5"},"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf":{"0":"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67"},"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04":{"0":"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4","1":"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e"},"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f":{"0":"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390"},"270d167a047a9414fa301029bca0faa909af033fae54400df83dcdbd260ebd52":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"2860ae00b39a0411768c897bdb806cdacf2aeefd62d6d95fdd20f648bb82b211":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d":{"0":"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38"},"2fb9610fd2307d342b735e907eebb804571807e78549c9df322f428d1b863ed7":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"3116900ba48ad3dcd112dd876764c44dca9e68b27ac36d5417600b1e00e6ce6e":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"315223ccb031589de2774fc51a0b277952c04c9d576fb8fc651830286325b350":{"1":"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf"},"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549":{"0":"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52","1":"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35","3":"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62"},"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f":{"0":"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc","1":"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3"},"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"3b9e0581602f4656cb04633dac13662bc62d9f5191caa15cc901dcc76e430856":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"3db1964fe70b0d8511bef6be222fcec67159b6412050e56fb2dd36a4bcdce039":{"1":"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a"},"3ee0eb4cfbc1fb73d5facbebf310c9c97f7e14b94090b409f274ea1d2d4c6ad1":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab":{"0":"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877","1":"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa"},"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15":{"1":"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d"},"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35":{"1":"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52"},"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3":{"0":"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05","1":"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295"},"456469af3de265b2f19c11f27ea88ccd17beb0831fb7c4864498125117ded136":{"0":"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156"},"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e":{"0":"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1","1":"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0"},"4a33546eeaed0e25f9e6a58968be92a804a7e70a5332360dabc79f93cd059752":{"0":"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755","1":"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9","2":"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9","3":"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755"},"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e":{"0":"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6"},"4cb75138f2da3440b29c5f52a58d0bcfc5244344a9dbf300005ad52e2e099782":{"1":"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a"},"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755":{"0":"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be"},"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"530d1c4c48a351e76440f3aabb51c16d2028ad83e66f48338b6354842d44127d":{"0":"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0"},"54ede80a5e9fa5c457050f8781cf7c5de0d729f24841585a78ade023edd4e83f":{"17":"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4"},"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b":{"0":"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137"},"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b":{"0":"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a","1":"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b"},"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54":{"1":"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab"},"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38":{"0":"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f","1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc":{"0":"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057","1":"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee"},"666e42b90a3f74de168f98eeb1603cceec83bffa4585722f18fb62e5859a5c28":{"0":"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d"},"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4":{"1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"6ae39e1fd138cb8a52b349ea6d1b13e41eaeb9586704fc2fa5c6381bef899094":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b":{"0":"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab","1":"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761"},"6de9a4b2a954ae980b96c949f21b9ac6eed38df72736de1936ff18b3c3a5f378":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","10":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","102":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","106":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","107":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","109":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","110":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","112":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","113":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","114":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","117":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","118":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","12":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","120":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","124":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","125":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","127":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","134":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","136":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","138":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","141":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","146":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","147":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","148":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","149":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","15":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","151":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","153":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","156":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","16":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","160":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","162":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","165":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","170":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","173":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","174":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","175":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","176":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","179":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","189":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","192":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","195":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","198":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","199":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","200":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","25":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","26":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","30":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","36":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","43":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","51":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","52":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","55":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","57":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","59":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","72":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","80":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","88":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","89":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","93":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","97":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","98":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a":{"1":"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3"},"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55":{"0":"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877"},"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4":{"0":"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc","1":"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d","2":"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d"},"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3":{"1":"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3"},"77030e79f58370693c857efd71c77d0b1b584059361df378fd6362a23db1056d":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d":{"1":"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c"},"7a0de50c0a753910a7eb1d0c6f1c4738b45bdceafd994c0504e5283210cba5df":{"0":"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61"},"7a6ec63d6cd61edd3058f3f1c8da65a7b208d9b5119d68929ce36054fac44fa5":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067":{"0":"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94"},"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a":{"0":"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514"},"839135d3a4e74e7d64b62b0f3c3176528ff7039c26ebaa19973a26478752cb46":{"0":"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82"},"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834":{"0":"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057"},"8ab025435b1353d78b1d20992c23c180534c9202846360662e5f1f5007b67f21":{"0":"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55"},"8b344d1b83f0c8ea3b3152a10bfa51c5253e31531d5b456195ec43e07169f289":{"0":"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1"},"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","1":"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7","2":"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5"},"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5":{"0":"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e"},"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d":{"0":"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2"},"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514":{"1":"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff"},"979b000c96f9842165ec1449c3bb4629217e4e95b3fcb75ea3541e4b67b64af6":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd":{"1":"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687"},"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e":{"0":"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04"},"9fb64bbd59cb2bbd1d16b43f74f795d39375354789420b2ebfc8124fae3958f3":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9":{"0":"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135","1":"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15"},"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867":{"1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"a912ebcd60302c657301c49b0e20709d8ae29aec2b7b1459fa7357425d6769a1":{"0":"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834"},"a9d9481cd3d1501b7a8d4360cf8af9c81f907ea80def3ffcc656c0373b22ba5e":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6":{"1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271":{"1":"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd"},"af2c12ebaec902f8d3605c18473a274d6fb90adb9a2caac6196ea020292bee99":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"af907dc5a4c177c35126456f4c9069bb90a133d1b2b15111cf8d72f4a56f11f7":{"0":"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c"},"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5":{"0":"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802","1":"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802"},"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"b6ddbf30659904350d05b4829423f4acea0820d52d1bed4ffcc33ccfb82d56e6":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","101":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","102":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","107":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","109":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","12":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","15":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","17":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","18":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","2":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","20":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","27":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","3":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","32":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","39":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","41":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","42":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","50":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","52":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","53":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","54":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","55":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","58":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","62":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","66":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","70":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","74":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","75":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","77":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","78":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","8":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","80":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","81":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","82":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","87":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","93":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","94":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","97":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7":{"0":"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802","1":"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d"},"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94":{"1":"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9"},"be7ba3c3de4f62592e23e546fed8e1ba6b02592ceb8b297c04d536d57d6a9218":{"2":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9":{"0":"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be"},"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca","1":"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c"},"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295":{"1":"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9"},"c26329086092268a5f6a18bc20d81271dfac5b65ebbd6ceb6d103be5a5adf4e2":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"c27901da01e6e05ec21395e7ce0a9b69c6b7a0d30d9f043c60a25843d3243686":{"1":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"c758ba0bafdb3b25d8cf866809db64ece5d31cb5bfe418369cf945db02def241":{"299":"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0"},"c83b20ef4c53362cd1dd517b89005f638c681f2bf7f66384114c6e4105f73066":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"ca18da7ac5f817579f971324034577a564600384e6cdbc45acd9f2fc63835b7a":{"1":"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b"},"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067","1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"cd8a9b7bb5780b911f1d627bea5aa07e92fe0da8cd799e7640f8dbb0432ac9f0":{"1":"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4"},"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee":{"0":"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506"},"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82":{"1":"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a"},"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67":{"0":"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc"},"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5":{"1":"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5"},"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c":{"0":"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21","1":"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b"},"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135":{"0":"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f","1":"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54"},"e64d9d8313736de85e55d1bcf43d24ccd3f841dd6654b53d525bfc69d7f70eea":{"1":"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82"},"e8498c07730f78fcd30a2f5ad4f5df7191db947a7e5f70bd3e0f07f205a473d5":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802":{"0":"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d","1":"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d"},"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21":{"0":"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2"},"ee817525ed76318f476385e96173a676efb627d265eb86e4a5c483f3e4b9adaa":{"3":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067","1":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"ef91cd3d463aae564cbe08af7aa89f38cddb536c6701cc615938242a6ddede20":{"1":"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549"},"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d":{"0":"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067"},"fc579c56611dcb0681e5b81ceea2b004af1018c4903ac4397e229860ebd3d9cf":{"0":"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca"},"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa":{"0":"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366"},"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877":{"0":"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3"},"ff6ea64ce1a2a38ccbdd51998290250d8b9c7937caaa5c89bb15ac276cb9acaf":{"1":"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3"}},"stored_height":1775877,"transactions":{"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61":"01000000000101dfa5cb103228e504054c99fdeadc5bb438471c6f0c1deba71039750a0ce50d7a0000000000fdffffff02d3030000000000001600142a9d0b586e93440c6f6dde7baef3cd5c04d5bb1ce8030000000000001600148b589790abda555b54946c7dd23d9dc13a0a019d024730440220378df1dc830292515f18fc1d1d31dd93ca6187705dee6983fe0e6c104081e91402207e457ebc320cff113eeb78ea875c5be07ff4f48626ab6e21ff957f5d1c91ec5a0121036fb36e520403ef925e1ed41fcb6c858fd425c7cf4e4bdeda7d85cdb469d9d407608f1500","0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761":"020000000001011bfe68f11fce4d584911c02191fd17784c9b284b76ca26aad6fa9f497cbcde6b01000000002852208001c99598000000000016001486eca9634910df929ac9f38bf8dfa783d66bf62c0400473044022009f6111e0b18921ccb5bd5b0f603caa6093571e454b8db2ff67755e8e924a4ae0220157d0779b5ba3f79f71b94ac9ec78f1bc0cf299da6b0ee14e0bbae2716c79bf2014730440220605b3de279569bd70e6419251de934e31739fc49092862d48348010dcbd7e6220220295b65fae25fa2bc84fe9f642e56206e2d1c25feac7d4084b5a3547af4d95ee601475221025f3c508d0adbddd6ce87b0bcd173bb0a0e3dc84de8e755468a01f298667f104f21032157fde1aa538570586f9e3978ef7ba1c9ea61fe04678e6f65bc196a7d57919452aee02af620","09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be":"020000000001025597474bbcb6ca948d182d3f97df2934e08a34a4abfa441ebab8ba0bde612a4d0000000000feffffffc924805fe715df8e925743a62a63152b8901463789027ee4ae967767660f5fbf0000000000feffffff02a1e64700000000001600140ac1eddb8b03f7aecdf30a4eb395a662f38005e7ffffff0000000000220020675c976d0e46f9ac11025826d4d968437588bf540043ea62e6527336a30f10c30247304402206ea454483037f344d2f009488a7e044be422e172e641811d84634bfbe8899b6402200219c939dda27e1a99a6364e3f86ca536d7e942c85e77aef6744d2e3af55b4810121033025b5ffd9e8abb848e1757525ba93e1ab131497d5c811f6a1987460f274744d024830450221008ec109e40f11b7def5e5423ed87b166958dec8b8c21290f25d4e175d5fbe01a702207af2be6161b73e3eefca6336f4fdc4830d49b554bc30721f169a31321ec039c7012103db186cb34a832d565fd6400aff799acbd932e6ed8bed195aa0dc855207ffa9a5e28d1800","0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0":"020000000001017d12442d8454638b33486fe683ad28206dc151bbaaf34064e751a3484c1c0d530000000000feffffff019a33410000000000160014b7b8ade38f533c9433f02ad7c702a5eeb34b7734034730440220511e317b4655fecef9cfeb68356a4382ec5b8f6fc7ed4a6eae55ee940dc15d88022043d1006b4540777c5b3e3119df9e60bd0685a83c1dc0f597d68cd8d8e249791701201df376228cfff0bd7fb256b355d0e51a432c3e486622acdd6612e3d2b132b4326a8201208763a914c015c8f3b13d6fc9da8db184c2ebcbf4e4ad5a6c8821037a80615a0c38fc944f003225f7670dcca9ac4b9ccbbee21c9bc95e02d3377efc677503e20b1bb17521028710822dfbbfcde0b2dd793d1dc4195387fe8c0267b04daf2b5d0fd2382d6bb068ac00000000","0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d":"02000000000101285c9a85e562fb182f728545fabf83ecce3c60b1ee988f16de743f0ab9426e660000000000fdffffff01acf0ff0000000000160014b76eecbc36c4e7b2a44e24534805cd2b191c31a2024730440220571cf8bceba931853a49561eb32e5998995b5683308f7729215f2a29db30a101022074f1172e6ccba194c5b242eb176ca4d721758b620f16a5e7d6a5288a8a83c0e701210244a0615ab8a53d42252d371b8dbf4f6802e10c3a3b708075fa681746e3a6b5ce00000000","0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0":"020000000001018ed0132bb5f35d097572081524cd5e847c895e765b93d5af46b8a8bef621244a0100000000fdffffff0220a1070000000000220020302981db44eb5dad0dab3987134a985b360ae2227a7e7a10cfe8cffd23bacdc9b07912000000000016001442b423aab2aa803f957084832b10359beaa2469002473044022065c5e28900b4706487223357e8539e176552e3560e2081ac18de7c26e8e420ba02202755c7fc8177ff502634104c090e3fd4c4252bfa8566d4eb6605bb9e236e7839012103b63bbf85ec9e5e312e4d7a2b45e690f48b916a442e787a47a6092d6c052394c5966a1900","0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687":"02000000000101dd12647382dc8e8620141b0ff1f5bb8bcdc6eecec47d6971a4d1994d09aef49d0100000000feffffff02ffffff0000000000220020c3b2464da6785a0e63d9b70624eefad269775016846e0ea4bc9819269c6abe1f38dff5020000000016001480d1350905fa099677475a24063f8381e2f6627002473044022019459342deef15f596c388ce68749080d2a54a20dd014a2d3fdcf2206eb78834022063693a44dde77f30d52aa1a9aa940a9bfd2c5013e05ccd50d3173472e8bf415c0121028833486dfa1a24d0aaa9d72deb0d69a835dac9fa6dbc5b86d6d5245f126b057be4981800","0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca":"020000000001801ec5794688bf5f80dff5cdde661666ab8cbb2b5b914f9f616532cfe38cb448040000000000fdffffff15941122f7ed724a356fe3d8d568a36441fbccae1d91f9583a8dec528a2f41070100000000fdffffffc71786d04ab62852c79b453d81f412fca42de57e905f95e3bb83df3b3ec0a4090100000000fdffffff62ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130000000000fdffffff62ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130100000000fdffffff52bd0e26bdcd3df80d4054ae3f03af09a9faa0bc291030fa14947a047a160d270000000000fdffffff11b282bb48f620dd5fd9d662fdee2acfda6c80db7b898c7611049ab300ae60280000000000fdffffffd73e861b8d422f32dfc94985e707185704b8eb7e905e732b347d30d20f61b92f0000000000fdffffffd73e861b8d422f32dfc94985e707185704b8eb7e905e732b347d30d20f61b92f0100000000fdffffff6ecee6001e0b6017546dc37ab2689eca4dc4646787dd12d1dcd38aa40b9016310000000000fdffffff5608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b0100000000fdffffffd16a4c2d1dea74f209b49040b9147e7fc9c910f3ebcbfad573fbc1fb4cebe03e0100000000fdffffff949089ef1b38c6a52ffc046758b9ae1ee4131b6dea49b3528acb38d11f9ee36a0100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0c00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d0f00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d1e00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d2400000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d2b00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3400000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3700000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d3b00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d4800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d5d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6b00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d6e00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7100000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7500000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7c00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d7f00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8800000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8a00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d8d00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9400000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9500000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9700000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9900000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96d9c00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96da000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96da200000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96da500000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96daa00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dad00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dae00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96daf00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96db000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96db300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dbd00000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc000000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc300000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc600000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc700000000fdffffff78f3a5c3b318ff3619de3627f78dd3eec69a1bf249c9960b98ae54a9b2a4e96dc800000000fdffffff6d05b13da26263fd78f31d365940581b0b7dc771fd7e853c697083f5790e03770000000000fdffffffa54fc4fa5460e39c92689d11b5d908b2a765dac8f1f35830dd1ed66c3dc66e7a0000000000fdffffff8c6d5a1b0a0b34113c0f73cbdb61ad490743dca7b9e3f335999f209c1eb4278f0000000000fdfffffff64ab6674b1e54a35eb7fcb3954e7e212946bbc34914ec652184f9960c009b970000000000fdfffffff35839ae4f12c8bf2e0b428947357593d395f7743fb4161dbd2bcb59bd4bb69f0100000000fdffffff5eba223b37c056c6fc3fef0da87e901fc8f98acf60438d7a1b50d1d31c48d9a90100000000fdffffff99ee2b2920a06e19c6aa2c9adb0ab96f4d273a47185c60d3f802c9aeeb122caf0100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60000000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60300000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60800000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60c00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb60f00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61400000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb61b00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62000000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62700000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62900000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb62a00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63400000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63500000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63600000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63700000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63a00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb63e00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64600000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64a00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64b00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64d00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb64e00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65000000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65200000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65700000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65d00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb65e00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66100000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66500000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66600000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66b00000000fdffffffe6562db8cf3cc3fc4fed1b2dd52008eaacf4239482b4050d3504996530bfddb66d00000000fdffffff18926a7dd536d5047c298beb2c59026bbae1d8fe46e5232e59624fdec3a37bbe0200000000fdffffffa0ddbc2ef967817407d33c3650e04f81c57623ca8a4b3bf21a58cc1b979447c20000000000fdffffffe2f4ada5e53b106deb6cbdeb655bacdf7112d820bc186a5f8a269260082963c20100000000fdffffff863624d34358a2603c049f0dd3a0b7c6699b0acee79513c25ee0e601da0179c20100000000fdffffff6630f705416e4c118463f6f72b1f688c635f00897b51ddd12c36534cef203bc80000000000fdffffffd573a405f2070f3ebd705f7e7a94db9171dff5d45a2f0ad3fc780f73078c49e80000000000fdffffffaaadb9e4f383c4a5e486eb65d227b6ef76a67361e98563478f3176ed257581ee0300000000fdffffffcfd9d3eb6098227e39c43a90c41810af04b0a2ee1cb8e58106cb1d61569c57fc0000000000fdffffff02c0570100000000001600145e943a84bef855b4eb05ea509a0a543e2f2f36b600e1f50500000000160014720e942cbe6f9db8cac236ec29bb3e9a0c479fe70247304402205ca4578de2fa8a5938f3fdefa3d72983366762111d794af8c7bb414b4a96da360220667e1d360c3354c156c4a9dd37a41fe21c9dc68fc5fc2ba7af656707168a6c20012102f241daab862c0c11a11923f1a07dd27a7f20c76ea4b5291b161b2fb83f0d6e200247304402204d8c6a23be6872dc7c10948a24a35fa06e8bef2b00b943c23c8ba57834fe41ec02200a8e52c8d4d00694844027c8bdb31addec086b87467b797287b267104a83f387012102a95d186deb94de2064e94d0b5fc2f5b5d44bf26ce0d56a395110bc7d3f57a29102483045022100d88f21c74735a6fda572831d3997b783ae0c9d6566054c8df33e8a971fc2356602201bfe3870c53101581e20a9f6ae4daeb22642b9d43f05dac63b126e7db2f05cb7012103200803a9de725739d0e57b31be2c335a145df533e2f17cfb5f12245ad5fd6ce202483045022100a39882bd88c2ea7a6b6c6f0c740d98254f36bb67f1e8430992ec3da661889657022072ae60f95d60710f97b5165f926ad02e13350521fe4d1656913f9a402b931e11012102701d428192a54dffc6f0ae98d22553e994fb75c16af0d69ed6fb6f895c5d5c8a024730440220034e90bed6ec9b473415432f3817d46dffdf02563a00a3c96310d102db23f9eb022060e0803af307f57c902db623b7e91c8bd1300878fb640afcd97ff503400b5d2a01210214c000fb566eac4c72bf453f7559657678fb248996f73dd223abd389cb931a3f02473044022075e9da9f05c7cdc3f6aea1201a9e31ba19120c669e152a783b363eb8a9b1ee9f022013345feadc0dd8b1a684034a7145344701e13f598e42f8454fa49e033b473ac101210305b8c5c2d662b639c97f718dbd224e5ab4c5dc247e76ec093dc9bce2eaf79e7702483045022100b20783343dec7c5216b4fd7495df3a5227e316faea08635c971e6ec32c8e35ea022049f2098d1b1196d920833b9bf856ab8d2ad851ab6ac03dc95b1da786483751c8012103441a9dc8b461423981c7fd279748a9d44c75d3b7828e74b6336c8d1f52ad0e8f02473044022041a939dd09dbc1d4bd62316bf968df1a1e5595f6d8d942da0ccfa8394e79e27d022031d341915760c889c4eb9d5c9775b582fb88636677d72b3ceb8f0dc0acc6a3f1012103a9643d6b9f3ecd6508ef55910eb9221777c324898133d33ef75bbee074cb2c18024730440220685f90c7ff1019dace8750b34f6fb084434dc6b7c05aabd2347c9c0c53a1f0b10220425fc844d68283896394cc1581fa3b0f5d896d5d229778c7624d2d00dbe1a6e7012102936bafa88cb5a3230811280cd49c35c089b9e0fbf580f02f9d66536030008ebc0247304402200df330de41c70930868da17341263605a3e4141a49b1be5b0d53186f15bb84a102201fb48d676de243711dc743b559ff9f0620b562caccb2683d70a1b6a5da61b35a0121028062a5a9f3af3e3738b4b64a89d101be4ae853e2d887b504b9b830531d18c17b02483045022100e5bbfc79c8f73445b5bf50c08ea229f1e0f559355b8f44503462c40626570e140220441ab082d66947e755dfb32585dc00aab9bf4f5eba4af9eae1687bec6ba2f69f0121033d42c7bb48223e6fe4823c8aad640e42fd0a010e074b9e1edfa0fd010ad7468602483045022100d7d9f34075e68ec2399f74e57f46828788eebb4619ba2fce607458b6ed5efd7a0220344c1a88f06992e80e3f4a547d0edc62e879402321fe3c80271a28900199225b01210244d85f07a4f91d2bb69eab264b43504df429f1a130230aa27a289c3ef0feeea102483045022100bdd81c1405cab31b2e1b2c0f79c2689c19a043b8c85b92b97be215c4c6b30f5302206aed161fb8b262141d5598bafbdb262345ecbdad0320b881f03339fa7d66252d012103d3051fda851abb65863a294b9f41cc6f0f7b9e335301ac0fe417cec694344e2b02483045022100d62d62a47e08975f6e3651ca9d5570c25c0fcacc2e523336d06993244072a38202201bf60faa4139a391729df3336125ddd9571b67e7b2801b2bee848f8a17157faa01210361c902c442ec98654793f958606361d682d15cfc00dfc599615cc893414c8c5d02483045022100831cced452304d326364ff46e9d7bc69fa529eab51280bea6a00edfec561e7f20220161bbe02551d6edac0b576285837732dbc9ea64cb7eaadbabd7ab9a8bf69fc79012102fee9e35cb0d7ee40239c05715ff13adf46d7304b5a65986f53eea0dee1d1ea4e02473044022013c34bafcc250d9c474dad07be5d231b1e2295e9f2882b7967d20fd1e9f77a070220608cae845e5f9ff0f7f82f97bb04cb6859b32961213d22e2eb8e54c1eb45a16c012103ac3998602ce0f36d5f39ad56e9c38dae5c1e309ff421da2b1cff0a0129f0bb2c02473044022011f840a0b98d0cbc4749f33374c580cacedc7a6d3868eed4225a77165912c947022056802dbe210f387798d8c4d99a8934f1558301a6abf95312dbf347f64631f7de0121022269d914457a7d544e647850066f74baddd7bc84cfc294ba3118d78e98ef98e002473044022018e807aa65eb0fa25d2d5d209cfe195884fd7db7ed80e0f0519cdce5e2da96d602200649378859459da4dbb340005ae763230d61befee1a89377d66d052f02ad051a012103e795f563cb1329e76311a11623b7baaee423938e07f02b37d25d1130bde3bab902483045022100b654c8012ddb4d55de3c9682e55e65c8bba86ad82a5cf189acc9cd40cb2e6fdc0220608bf0ab5ddc206ce8a31bb156e6ed656eb8f6276f129d64cdc406bfdd7a7a25012103d8f6cbdd107a872da15104db9b16233162da3386204fffbef45b759e31f0e9810247304402207f49cec4cc4b79b6ae5a98f5c33577796ff3259643012c2ba2f340a772b41cec0220085d08c537ac6426b403cc8c64e2d5e01a22f72cffd44085f1057e3a87d210f10121027258650586c18aec62a509b9d5435fa2c2d574c6a05712a3aadd67b119966c4902483045022100caa3e740e74a0227425ea41d63050976612534fb739c3af61f05cbd90090e8a102206e467b07ea5b228e4697c3ec58c596b191895c9b7ad426f7da299f402598b1be01210360cbe3d42881780f9b95023be84ef1b54705c294c67527029f06346c302c76310247304402202fdf8f747827755307ba16cbe8495b204c576d3989972fc87a41c89bb886c53102207a791f5db12764642d083280867f042a3fe03014a73c81915f724f2e207b072a012102df6a0ed5df4d5c4fbe456a073b6b348f77c2f0291c824ede694fa564ecef9c0202483045022100e5066e2c0dc41afa627b71ad22098a47895916dee4a61fe2af1fb3cb09e9959f02205b55fad3e64889b77ca880c461812c0c0f2b0651e9708f559ba6ab12bcc15ffa0121032bc03395fbf0f9ccc484b155fc756ff42e2fcddf300e78eb038296750560212802483045022100d57e90e5b8a2c02902a8684d1a5f18114b3551b059936921cf9ea81c66bc72150220062f83973ee29a2a2c158baaee0cacd29703794faf7604266e1bab534f6bafe90121039810dc13eb43259bae6f0324df29c5de8055414a2424d2b3f0a4df088685db4b02483045022100a30511b70e6580dc9908e507ef09bcfa4b11aaa631fffc4ba3a977565e98a747022038bbad47e77d5ab47ae08d6baf630358daed5fdd994a8606f9d68ef9c8dc7a5501210276503b497f680b5516635131b411fc2196e3e06023b5906cdaa46e4a2ed2d2f0024830450221008dc03e6b9a360a3be9e2b1675684c26aa33ebc1d698d9f8716c408f254aaaf6e022036fe6715680a3472f1b2ebf05e285a72df3284ca9a2bb15fe45714704bcfed0e012102b6af3fd0c64ee3e9f49c67492419dcc41b303fa7417d72de493c56899a8726f902483045022100b321a8c56fa7d973906188e83c7140d1fc7241bd335b56283cc6966768380e2e022036f85d065bfc585905e9d02c72142a44155f0d531423e8ab5bff89bc8afe743901210203580b308ba623468054d7e2b6586aa580ea823878a61b2f3ca7dffad6a66c0b024730440220660970a008a34ee4c0f07fef354f92b5d51d689d938e52f0737bc8bc1cb3d2b402207e51dd30a9b13193b46a8eccb2b01e0da4814d1ecf7a78f82d844feea56c3579012103cb1332962aff65740d3a1f27071e1592e357313e4f5a273d9ae451bd566b7dc70247304402201a4af8bb735fccf12c537f21823102a76776755def68c609d8d2c42cc27d6d1f022003c2a9d217e171fd3f4bc81c7266e41f62087723565f35d11f7bdc727141e1c90121026bbd0c860bff7c78a5cd1072341b9eec2a97ff93f6da65b3a9f89963f075177702483045022100dbf86780582a3572fd26e4068bb4a858cd9c96b96ffea8d158ef7e75faf29a900220667c357c3bc9875bbbce30c384568f04d018e1ca38aea1d88425de7a2c5a43df012103e3fbd0562e3eee6f5e146380e4c207cf86d71d468dbfba04b666ba97c572fea80247304402206ca203f3b0be03b74ed24f29f9e6fa6308e754543feca71bad94a925fc90c7a602204ba06478fba93d1224a29fc368ab6a42f0b814a06b8264d47661e332b4047c7201210371958a463ec5b8a8b1e677c925c844827732dc57663df524fc282bf4a8e63a3202483045022100b3a8e3e37f3604618795e09c4c902d83c2e8be56ff13c1dc4a01317c240b27aa0220665785e980c0efbd6fed0d5030ab8fe821d9c1ac7bb70ca7b385def3fbe3eaae01210265a54945095ab51bf92b21d67e854a48761992eb200923128e590536f3c6bd3b024830450221009d7973b55322a502db88d944070bc629cd2611925244a6821c2ffcc7d526789702207761c6b1c2c49526a118fae1ee16f89203cdcfa285c6e3c09409cc013c51fd1301210317c90ded4e94522e0ba9095323e7c54a48a01854b48eca8e8438c809115365310247304402200c94a592bb0647a3c99bf1e7236a585fa23cd0f6c0511048dd386eba6a731d1a0220588b05f9f4f6fec5d4c1408dcb9789d63553adcabf62be98f4171da51fcbd3e7012103bb7347770870dad01bb64249130dcbd01c6c5d0161128f07c950e1767e52eccd024730440220048e5341582f04b14e8417719bb0c4e5988ddc00a82d78736adbdd75edfc8f0f02206cda1f7ee5c1bb143a3632d650262986cd0b01084b6df62eb2fdd43ca4b936e6012103beded311004b9b0a74f372404ad26931bc602f02ce5e73359a9ffd53932bd5010247304402205a7464c7ae24f5b798c9948e3cbf0bcab51779bf0204c14cc728f62978928d32022018519b54e2d9df4ce216271f4bcc9b464e70e9332c4f0d9d7d4208a420ddef1f01210297422f440617cffe84b7f262e7d9d7d3a096ae35ca6671b82a7f68e1aab06e54024830450221009d6d2829a6d83292daec5464e3b898140af57342256e05cda5e30d7f6aa62ddb022075dc9856a634e8c9f63d915d6bf843040fe501a17443d7ddb608beea54ba3304012103ce1fae187ace35be51966b8c7f297071574a3b38f3c5bbfbf86c8040d42355b9024730440220513cf06c6282679806451245be1925e76a4ce12acdeb3d281bf13bebb31e778c02200abfe90103eb0b5fa4618509e20726df44e624925d6483a3d38be85948e4844e012102b1d57fd3af19ce5db8e5ae1cf49e35d950dd0af9064b0341540219c9eb7f568402483045022100e884e5becddf0ce3f8869c15d9afdcd5935fe2e4e894ac63c0f446f15f75e939022013e53c863dbea2eafc69989c3f051218037af4cd1997b84eafafcd512627520a0121031f1a78704b847e8457a305250b714193be3f91be4af76014a48e458c65e04ffa02483045022100cbef8325309828f85ec85db4585682c17acdb73bedf7ad5af4953333440b2c0402203177d3fd532d1b3255b432398c83c4db7caa2df8bf6b9b747741b6679e6bad750121024c1e587d8fea9bf063989908c6fb809fb934aaf837e728792b66bd6f9c5ec8c102473044022016b8de72ad65106bf52949d09fad3c61c8d02657bb5cbc33510478861ed179b50220541efa16fb89e35dff3cb4fe1531524f24af71926c47c05ff630f3cdc33dfc70012102c6b802ae2655b29b595f732870010b5ef41e9d17b7d55266663ea0ee6b7e73490247304402204ab6eeee3accc1cdf971f0b97842a082490d57df9626d85de1bfcb99233b798e022001da59296c31d5bc86fc92fad105d73827c19d707d8b4d25779521ab7a4ca57b0121028c81744700b83080588a27afa0299fc579ffb976ba18d22272286e1fc6d3461302483045022100b3ba9f23d2a6e7eebf7359383fef06bb6c5705bfaa24cdfe7c4d4cee6087c1cf022029e28d2c51aec457ceda44fe3fef2202679a9457b6f0b99931616063e76faa8b0121029fbfbeffbbe78216e4706243ca22e51b0fec0f27276315eb2ae787c01553fc8e02483045022100ee9195a10712c2b324861f691b53f51caee993173db7899d84438688553c6044022061b231a4fa2302588f871a16caeb01d8c823e03388b4bd1f4bd7cd3d1a4dbd6601210337c70409f2c81395ffc6166e77bc6f55b94384d2698d51b331959c61212a8b0a0247304402200c205017bac4c7a697e4f2c26e1b2e509d741ff678fee863d22d55a381c7d5a0022049eb959696482e15ef4ea87930ba65827c9e82a5faf5c943b50bee6fbe9b216a01210372516284152ceb93928a317b490992a2852b946ff6ed88188fa02f07eac9da270247304402200d67009ba1086d1fa2c4ca84695f2f147e1bc959311680c8858c06cec34f7ed302202e6d9b77ecc8c4b219a1aee5bb77d254e8224818e0539bb7c025bdda8a0c4522012102a103ed174acf2efd20a439c05758be0aa68342aa84ac137e34755909990efe19024730440220486c257316f9c2ac355a47c5583877614b55c19bdb524202ce5e36f3e9c6df420220054645195ee2826360b827fc149df2ce690a02f6280ed74b6ac057d2b6af4c49012103841fdc55923d84bbc127106d9d1227fa43d8182232b1b052af9748f1f74151800247304402200457aa9d4a2f93947538badba604698bb524f2b9b7d50ccd272148d5f520aba302206821df8877d1c80b399fb7e2d303996808c961eb2201ce483f5f0e80bbf2435e012103c725e032fee4278bfcda8ed4f7c016b53d172105fbb6fddb15006930334612d00247304402200efd62819dc59b033d1a93d57ec891c6e73f74ab16a80a98bbd9fe2c4e3c51cc02203e612db06c70b6768d5b9ef7139ee43a67051b690a4902be6e0bfe47dcfd61e1012102cd61fe8579146ab5886fd65cdc6f0d7af82c60a69c7bdda0dd069840c32e422202473044022011222f0b85a838d2109ad59be0d170be7410e4b0b9f1fc55b4d6c57d90412fd102202fe3550872be7b1eda2acaafada0618b1a57658a1bce850b935c0c6c3784340901210261e5751a99aaa19f55d433b933795498ebd024216e9deadcde5a3b0e21567ea802483045022100e4cf8b8c0cacade19754dfe9b71fe16309e967c3054b3f6aeb2a3d11015fdfb8022048bddc40fb827c5f6eff4495228dcba01bda33481512d25c8e8a441a8cc5f8df0121039973ed8c16473b8365db648ed9483125d7b362b1a0aebdc73c2f50b1ba4f722802473044022002db1fcfa3d3ba7385b454d00b3acabe26c5131ccdcbb7c7fa502f8ba0bdfbb902206ebb9b31530162c73786faeeb1ce62f63f80d6051fd128ef2fc137f8ccf2fda2012102ba74d3bdf4ba42dfd71e58748141329161b5960db1031695c501546f594d7a4502473044022073bd690be32b35a65cfb4e1f75325ba9ccea9a2b51e1dea08f08d1cea9715db702203d9c66c0345bad60615a30bbbe6f4a6df99088bf1abfaa2ded775ce6ff0d4080012102084ac811562f8d1ef3ebe69942d4b181c0564909325283a4625a2e7ecca3079f024730440220199bb1ced776dd8f5f6fb20270ba41bf4f00c281ffc96221a138efa10b9426ca02200fb2cf9d1b1226294a9bac3a66ce451ad017adfbf1cc466f23c47b3d505ae6ad012103563d229d8316ec138126bae20d5e47de8811476d61667b54a6729dcc99b2673a02483045022100cae241de5241b92cacdcf8e41a08df64ec8ca0e2506cb62ad67e50472eebe80f02203b9fc91f71cf1a70126f2f871d6a38d214e71dc67fbca9213c7f45b110641d4a0121029aa4ce980696a57e1682d013870bd0d7c93a5f5c12784205dc1a92ff5ec4e14e024730440220070c1667041ec5191270294572102f9b3d55a00de5c420820701845f8824431302203ce7d52e405209945629e1261f024462ea307d2c43a3c803b100d2eeb25b83140121035f1beedee72f9dc99e5b090bb48e61a80e562100b16d31ef79a7cc9ac49796b302473044022064ade1e9322e9cb30c9438ce47835487f0a2d177875f587a1d040ef33ca39599022046ac705129edba5d69170b23198cb653e3cbd5f633925ef37e9d0330ff4d9b2c01210275badf52793e5dadc7760a267ea8506102f3b2fe4eec683385e9917f950056f70247304402202129f6ca5bf62781bfd91ee0009aff3bb5dd44f645de9351bc487efbd7820f460220698e503d84508e5e6224690b1fa6a830098b6dc395544c83c22ffd2d9f04e069012103087e36546486c87ac8688dea4329e3be45260e7598c71e95b70807515361174802483045022100889b3a5510d759f916ed7b0b8de2a699330485a4bf0f4732340e04d22e248303022048a754cf5299e38a130eea0034bfb305d5fd43b1c3f3665c8b95bffc5bb1f8a6012102bdf183bcde63c8dd5da7d5f59251575e16ed0100cdabdef6ba909050a810ede102473044022057e61f8169de97844a6b614ff72b3cfde0829eeedf327398d4e35b099491929002202556a74d7c9e9ba52885d9208dab96dd4501761daaf2a02107c58aaa35383b87012103d2f80d82d7386c964c79f41afdf574c1a354e821531a31eeba5b7d24dbff6e0702473044022024f5bdc1cb8a0f28cb31a7118399b269e1ae3d3a0ada297a8abc7b49dbeba621022044a92b5e27e6dd05c515965248d1c3e9748febb03816c2c30c873f5bb90a8d5b01210292be5af5ff81b9ef324e985fc1b462fdd379455c16f2266ce980881f4563f6c602483045022100ca9921fdf8d1f8c7774f33f09b265585945f6e677bad28d6a4269838178cdc4002203c8c846d18ca3468dca960462d9211f27a30114806ea0e095affaf6c36158a03012102502c465e4daf7cf57b6f1c422fda20e2fb0e0130c4ea6d280b44ed15c5baf49c0247304402205638a8892e4267366b60635b185bdce9aecbb976c094a0e2a0c85eb5eff8545a02204c862d5b5bcb45f5dd76785e8211556bb7d99e647926f1e2e1f0bc387451aa64012102569f6dc169eb0be825809405a862685a714f8f176c6a345e2a861a5afbc944d202483045022100ecd82847fa98f9806ab3f96c425aef73fbeb836e48dba8638b041546003135ca022045981af499a473623db929da3fe626497ff5a0d1d9ddbce81c2b357dabd45cfd012102c99f9837b73ffc3c836f731a859eaaa02710c8f17bde5ae7eba7278183215b5602473044022032e661e8bab6ad7c370d34d5aab2c8aa13e0615fbf7fc3eb94b1814fcdde8fc6022015a2e2d33f03a920e8b0309aa006bdb472411fd5cff9ee3dd2413fb5c59711ad0121031837bd9e027764f3895d3072e63a0f5b9ee1ab1b4468016336c3a6553821ba680247304402205b46ffd3cc47d135c645dfcf9690d96a7e0eab97c11db912e0194d15e25c659402206a7b5100c3a34fcdeb192d43fed900fb6b57ec7adfd6bb2034a131fcd48ef8a8012102576a907953cb4273afbeb646b796817f4576095cd2afdb2c98c958e1d0a8d9dc024830450221008f3442265107ac941b6973f658e944a9f5c8953416d0621c5de6be3d4f94733b022025168f4154a9e9fff01d269ce887539338a7d644c3fe21de9044f554fa3230e8012103612e2d1fb0d47f37e20c2cf94758e56c2e2906ebe1aa6cfdfc4ad0b8e1e414b002473044022100a554a6cc8daa97fb5f9d1847ab1f89519fd8e93de42ff04f111e1433303d4ee7021f24277dca5ae1636299c3f72620b667e94a7ffbefd5803c5fed60706f1c02f801210327d818f8b41f72378c1b0d7a66e0ac2c4b71926414f97cbadefb279f68d40bdd02473044022054710c5705c4c3e1f6377e2afa0862c12ab4d849bbf230c30f785ba0625e3fbf02205e6f51dc752821b0e0b574834df9f227018a4fa84f0f6051fb86f1f30bcfde6701210298c65155d0a11462961d4772c77f46413bf1b8abdcfa38364d6df03074d7061002483045022100929734557ce6040e216b84e160a5fd9082841e87e5cb04ef98388511be7160610220183210244bfe59ace43386370a6eaec75e9c175f3aa9730426ca620897ccedeb0121023a82af5f9f2086511549b68f3a086468c59c4b9523ceb911d13d647dcf0a711a02483045022100901bebbe9a762a861ffb98182916ad03a09dc7ece3d7a22f78a39363e554e8d002205ea956e24ef0f50a6ed97bee744433186d4db7431ece807eb4f25538605bc94c012103d1f3c8ceb8faaeeaa72b4f80bc00cec024f84b6142acc5f26a391e279e47054b02483045022100b1a1ba42f29c1f7cd5003030cad4abb5a1b4230cd36255160354f197a6cbccd3022051a2a5e0d0242185e10b1ad8832ef606ca8ba82142ca3db852ad3423c941ef82012102ef0c7ca2f809cbd0789742af4a5ff64b7c224d6d1cf98304a796243477f0ad730247304402206cfd94652530d201742d0afc9a209922a707ef1657824e5e37e5bc6d06d2069102204858ed1c8e951ceb8a6bd05c2649e71406b4cfa062270011a703180ef48c24bb012103ea2c67188488ffb5539c7739ad741e1e06993434ca6ca2c0fedb338b616d893302473044022051788c730b6a3813d9e9aa7ec6d9eb00e242e152f73fe751ed08fad55b4fa0630220054924322f027325b0a0343cf76cd3eb368c9c9e0f002d48e2271cacf5e3985c0121030bb12820b92043d6dba28201e1716832209be90e7a3f734f97597671fcd9abf602483045022100ea66aad186ea5aa9d544f1ec035ecb3dffc730a7e37a9914a36b2a15a1f528d202207df78cc86478e2bff81b4108de186c4d6e909d6c4f90c08f06a462fa8951ca57012103c6bcbd9beadd3097818b5cda3b0de22a977e52ef4311da75473212c3e8f285bd0247304402201d327b2b41956a99555eb403546f56b2a93f8dba82d8743397a0f3611727a30502204a0667875f78e0c8fc6f634cf08dcf4c6ede87cc5c145039f2d58c91f3a8f789012103350c3f29355038a27bbf3b4c341c95f897f5101fc92b5de809316dc385d5a6d902483045022100d79aff561fc547426cb3f2f53c05fe5c777933a5bc09f1f59f26f07bb1aee0e402200677285c537d5cae799d42ae327980b1685687c7c9131f3b42a864361b07eba4012103ae1eede14077bee7d15681d2067957b6788b545d3150b794f64163167f9a0aab02483045022100e9021252e013e1bc35702141ed8d5801c86ec478e44c63a138c1f28d9e3c449702202b5a54fb9bd951572e6b13f596a7360a163bee54a50ed7845c5115d084a2d5fb012102f2a5569b2dcaefbe315aecc369ef91fc015d9de39087110c1842a452270cce2502473044022058ddf44b46986f00285557480be978ebd7f19167b6d4a933693a801c866e75860220074cf2c71a6f7a9b81885a8de87a954410639eaeab73f0c7a516f87188c9bf3e012102bae5f4360c9b54ca39637bf3647fd2d27f4eed62434b54a35b4b187b976481bd02483045022100c76364f1d4e031982b54c5f58f21a48701a1ba8a7f73739b96b4fd8a4e5e8e3e0220374d241f426d230b6aad969c8ec498f9615667bc08f9bdce6f583b30738f0e45012102d487b38e6c89d2c676fe4d58cd7aa87af96bdcd56f70f725870edc0735421b6502483045022100f380ea9506607973c22c5a7661a3dbaf7241b23ae8a2ab8952d7b4c9da27be4d02201600ed41da446239a5589b7f408af0c4d6e84450482e430f5bc9e64ee35c4063012102c084123cd7edadcbe9b46383324d9ac7b859ba5dd83a83a987fa953c30f3c54802473044022038b0acbc41bd607dab810703156abfb68be6f7ab1ac9a8e9e6fb31f9258630ac022001a147063a006e15ef18ab4c489c18165d666bf0f53e473b07656533cbe613df01210205f18b211c6ae487a77149aa5fc9c8cdc7e46277ebe35d42fc0075f1df02b6180248304502210090fc509374b90c92d6e3f69d0b0c8e9f1b69e31308687fed99ef900ffbac2a3102207afaa4a83f25678ec22821cb23364f8aa0448b1d9ad58e6ff74278aef32adfce012103f49090cbf658376fc23aef943a1673fcc20b2f43debe87e077341d438f02a21f0247304402200bf353072de9d62ecbeb26cbf6719d481b92da763e18ddf38587ba2eb5e7b73102204d35e3887e990824d06f0800c596a000aeeeb0b8d10fc589ad1ce109184002ac012102f6e517ec416289de83f19de790666971566d0a85ceda305d2d7d49317c91298f0247304402205272489a79fa0886fbecfd0502165cd8baad5840f02e654c792af4517bc465f40220486d8e75fe7f709d642fca86957b2be02cac41215015dbc4d370d5c39a123b5c012102746b9fe695b0112cecd6fd8c0aedd4e7bc958a4410a6fca3b41a189f4ce389840247304402204053a17f2626a48bb1c04090ae807878ccae30e5fa457ecd199bfdbee0ef9b4d02204069cb619cefd99a59ac3b7473c8a8b31e12a7299f3565c8fa01082e9c969a290121029e88d4e04d9a1196804c732cf53e1e2f8c97968a4ce1fbdbbd67a724365fa92b0247304402207d03e263351cc42d75823cb6db0d8241d31740cf45043a4563dba02393f2650402200934b5675a7e9857b17792456b654c54928cc01a42212557475c9abf4e2a07f801210236f32a5c6fe05bf86f009ef5f7e52411bdfd678c6b7459a37b3e107fb096286e02473044022064c69bb855d9aa62744ab0f83570dff3544d0fee0f2c1a8c79c0e5829326ec0a0220706d778ba06f5a999296f594f81923c5c7c1cd4bee778e49757790d258fee179012103af2887aec7d097f54f0f25b522a39a4650186888ce7af752302ad38aab2f5cdc0247304402200546ba5cdfe018da09abd386b106ae45e9c6da258ea25025efd35a832c353a47022032d62308c00745ff634c6a26d8c7bb4fd0bf4c77b265ae7c636684e381c4ddb8012103ad78d1527d1ac50052b38cf87cc0c8b21f18157f7bf3df26f818822915d81d6e0247304402200a2e97b35496bedc0c33ede16aa79cf69b0d957e637712ee0e6b71c24b99582102205449224ff532280a28516491e259d219dd4dddbe2fedf2ffbfcc74462c280b3d0121036242c693811d0322af9ae9a702a464df00636c9c247b12aa6429edb2a6add40c024730440220036427ceb063da7af99f99ba096bec89076587e0748b917833432f05eefc491802203b1a9c2316c390a0240c513f421a27df0e0698b10fc2fb20e72080a71939faa9012102607ce01956997ad04e78bc1dcb0dede2a3e16d3c9d1d5a65525a27c6e56b8c6702483045022100e02f52db5f6f7e19371f8d573773750af3a0f6583d07ae9878df0fc78cd23bc90220074abf0ec40e8156ec7ee22ec6871b70b2674c6be1ab4a36091ef2fcb23e1df60121021ef81716b7c521bc725e30c082024f5eee98d530a42cc8b856ef360ff77cceb602473044022008f2ca5df5947cac8f8c71e2d8ee988fb4ff70909009a8e010d938353caedb4702204011337adc03ef84b1d40fe74b227d2811ee8218543d295a43e214500b0d0991012103e4c62f5dbec739dc8f4f1ba2f8fba3e4c379fd2edf981068ed738ddad2a7b63c024730440220726d1059ab00960b22625ba225ec84e2c72122c0c75d145a15208fb2627367480220536a971f786a6c9d135ce8221b237c93593d5c9e25c5a5eb83f301c68e8a5368012102345fd6313df3ff61abb4a4dfa5b29a3aced1c59218099322d5ca1a4e480b97d602473044022054663988d1e4268b75e95678b490167e0b56f090d5da166bffc0184fb0b0a7c402200ad26ac931ff4a48e32eedcfa6fc01b552d1f4e65f0ebcb40e49d519c1a802550121033078832b98466d296842675a092dd5a844c35b1042f6f6e8641f691caef6386702483045022100cb00c8fab71bdb7948a36c911ff22de21946bf9072b370441d482662b6c6fc0802206d9c92f3f3ed0dde2d75121c74d57126e4a876e4eb6b51fd39f71feecda9f5b80121036ecf41e461d399b61f950d7ce6c0c4472d876fef3e338524ab3158fe155f1bea02483045022100f200319f4fd1dab6fadf52eb9214bfc33681451df3ed4d73f7c85f74992ac7a0022012238b4b0251c81ad68ab264b246869e461c8b25bb2c87caebb3aa154ef577d9012102ea01decc330841ce783c52aa689526866e26a550b45e0adef16fb91af00ba55b02483045022100b7862322a6929ab542a21de55b6258cd988dcae6bc2a81950c92212e95e2da1702200194bd2f73c02c50069890dcc893dcbae124208d887e9a681ff64be072a9d85a012102e0bc445d85752b875bda77ff689d9cf1a0bff22b12fc9b9ec51a579f6a47b46c024730440220699412c3aefd9c72a0d714a4ce7588220a588d101d2030e7f43482ef00842fc502207693962a884739211668e9ac33219af886ca6ece96d4219d647291dca15899c00121039b9a47ce2500d0a9d80e813f8f9d1d5db0bee1409ff2e30fba1eee9309c0b7cc024730440220277787de7653f16356c8d9a518e612d14af43e40479d8cac4be8e9500a0c8a4a02200f14a8923a3e676cd73c230a1421aa959ed08f93ae3c2efb569fd7b69b0fd39301210253233efbd49d8c3cc0eeb7d9324d15de3b27d1c72b7242dee00a2af28f0402fa0247304402207cd572f4b0ebe4b72ab2e1327db56b911c5267abecc0951ccc1c9a4354b010c30220650a0233dc87901ffdb3ccff40adc3da12522f9eddef33d64230902a1ec205f1012102ab099b991b3932b2026642775f1dcbc1a8540528f2c67e10d1c8d0bcaaab013002483045022100a7e34c84d9266b9f323ff6ed81c05ac7d80e7f47f488f4bdfbd36120399adc1502206076bec9fd0a47d7d22b21f3a41ead2cf0175d35460b447cf16f8c0be23d6f4d0121021d54e1ab39ea2963b95ddf53ea16a9be892692287d0b9515f3f5b1fe4117257c0247304402204d3e9facdbf9107bad3e382d33dd5b3fe888c7e7945c24a6f68ba62922911de302206aeb8c693ff8e70a507a107073850ddacafeac2ede997f03a25c4c68cfa4eb1c012102cddd7587c323a979a8516fc245f94f3f00ec63888c3a893e48aaa6958aaab6da02483045022100809a65b908f767621f104a7e4cc81bbd2da432c816e2afc4440bd62216cc97ad02207f4fac54c5bfd0a4bc0bf168d39c56ee824a086b9cbb837868e06f66f972346e0121032468940d6100ef2a9f685e180634c476937cdec1c9c36eb9da366095fe2336d30247304402204ebd4331734c2533d7adae2b69ee76ff4bc1ddc6c9ceb51bc9d3412b9738bcdc02200e7e0f569fba6fe35843eb5acfa0ed91b0b697099a8803e4d78663393a5c2177012103a76df1fc9c88bb1f187d515a3a069efe8443f15677479fe0cd2b40fb8fe16fc602473044022020b9f07f8f3da078fc3e7dc30101fe5534034c7c68cb20ca762806dd9aac009402201c78fd2d2cda64effc1ad28d5f0ac73f5934e3e50d91133ed535263c954a7fd50121025dbc5252304aa6c6fb39d5ad1abd3a1053ff9164c96a1bbbc45416b1db7f86aa02473044022034d55b776082d01f8712537e331ce17bdef66a1819ff28679a6e7236089994610220785b07347d1806bbd5aa42a6f7040e392cbf968109e58ccb8c11d899ec68960f012103509416c83437dd681ec998913231dba828439de8addb4209638293837f29230b0247304402205c8344c0428464a8b08c8d5b880c38c7ce9144682b68ab2880a3501f0458e85a02200d58f002fe2dce5b7a5f3cd38e0a503b6fc1a4fb25669734edfcf90adf4489ad0121036946ab831d45359388fbe156cb1580bce4c857ebc1174094abf76d93017d866b02483045022100e372c19e30b8892fffd9046564752c4077565c42c213b168e2ff54553dde9c500220130b63f097aafe9af62e5df52107874e008147db6ef0d1a85f0599f3ff8ce9fa0121020d17d4a9621273d2e7019d070a23878ea6bba62ea5ffaec56519cfdcb33d0cbe02483045022100a2d28693078225669496e1fcccac2a74717ddf15b8c78cebe2df0c6e33753f9802200e7ac783086200020e850f318991a873b9ea8dd4774e75204d85d9434b0627a30121021da147d3836c2a0a8d043383c97e39ada42eb9eb68b89a92a76040e6cf5e074e02483045022100fe5a906ad5dc5e606d24ce13e772a6ee67d93802fd8873a571e09025317e7f4d0220555eda7146b96cad862312539afdf69ebdf2fd8cc6687adb4db2270eef47a3950121023f4acff0b31f68be9ac7958026a9488d9763d8d3b51761367014846fbf8d44bc0247304402205568b46b254485e0a4191edac9c571b65da6e5a6e267870372dc299bfd0bfeea022037cb7dcf9345761e704b7a71222e722c286695e5e7cc6698e665222dfa30761e0121026b7e8db02de8bc41e26cf7db502bf488f68ca36955c94937c68ce84d3421f77a02473044022033d365e61bed6b22eedd3cf0788a7fde4be69b7dd4e82cf15c3a725ec475006a02206e34b67113769991893d18dd5e309aa80fc950066b6cd1a6ef814f16ac29acc6012103cd0320e6a0c40a0225a7407aadfa3cc97cdc92a74140a3e8e79d657fe4834a5a0247304402204e122ff0505a7730ac20ac75750b92f6795d8a9f5381c7b49cf3a5fa5caffe80022010755719a1bc9650c0e21034806c640aa3e752cf51ee257ddcf9c3d61ca0aa7e01210287604532a855517f4e5d847d73c0d56b6855de9646866934993e7bf29c2a4b2702483045022100920c0a4fbe3c35c8a0918b38b99a4c026b1fe2caed60d4dfb9ef05c6207716840220770de6ce1f950fed729ad927d49ca3def7db777fd9e1f76e7af4f38e2dcf638b012102e7be3162cdb5a06d80f053e04c1911857bbaa3434b5d779655d9c1f59c83cbda0247304402203c3ec97c10776f6b446a92a29a06db5920e4fbd00c5a9bed1158954dbdb0462202205c81bac02be56def57bb961d29d3a3675b265b7bd4e916df9b65788d8cd5c799012103d7f3937d473912d871e114c2b1edea130b266e400b60abfbedc452f3907aa4af02483045022100cc2ac9bffd947a80c29852db3ff96189b7e27e9e3f468be28062bbe051a56f480220340ad59b5448103221fd828db99cfbb95f4ab01ab2ed0757dcdc9042f218bd1001210335b15649a9724f688780d96d07d423d3db82af9930257c9148a4346e53ddcdd702473044022079633c368de61803c61ea8b5cebd5e7fb733a9ff723885ac8598769ff11ed2e102202ce95d374db197b2f0faac5caaf9913d6c736416404d10f17f8b8cabcb8b16e1012103bbd0f99eb54b0c31fcc3049ae3e285a93bb7dd9114699bdd432deb3cfd4d4e35024830450221009389640b406320e4934b4a315d9d7d592e1d71bbc84de86c256969ccf4d23de70220777ac1e873bbe7c80a251385d4a1196bf5742fbd42be40bded802e379a08c9a20121037275c5f8581d6b13c2d6aae85d04cbb852ae37319d6182811384203b5da326a7024830450221008ca270b36dbc5831c5880222478ca969a526e567dbba5d46dbfeef61631a537f0220666d8f17f3370b2f7668e34523d21327fdf87e1da88a2b237a01127543727290012103dddcb85320a559b844f3ee118b2c6afc27bea2f9a3efe841e22af69de788164d024730440220567cad052a1279f50ad9d716bff6a86ed898345193be50e3c8240914fbb89b43022020fafdf44134eb83f760e3664b638515c094b499bade8777364fb4c7f8b96310012102bc5c3ccc349a6fff2d93f23bf4fc1c7dc5a4204af25db19e1e84627d1b7d306502483045022100b7cdbd1b5f7697fd603374bb76e45a7910fe535ae7de597a4d1ef0f5fccc539b02207b112ef060619a72210d357f82e2577470449fd9a14289692d9abdb86fa649ee0121025479d2d2dd0494cd208cf428705d01ebbbd2a1b7ab466f61a871dad0ecf44a9102473044022068d5a7f099802fd8836b8f6eb8caa5f67274bc77a419fcf1c93df39b8c521200022041d5cc5e570f507560237b2b2bafd39b39258a9c6beb053f00c5e46212c952b0012102c46fef722f6a91b855ec492ac5ea236e6ae19c7fd063bfc3a7b564b34fc0569502473044022041dbc2166dfcbfb776950e2e5dd62d3d79ce88e0d950dd57b9c1c2e396cdda5d02200140367dad0b5b16b832b797380a03ab6cd8e1e50c76abef421296f96217a2a401210322b89531a89dabb8fd7a58ddc756ae280d6f5f34cc39aff544c832f38fd0230f024630430220719a8e183599dd9d67cbea5e233a334b8e204d6ebfb8ef527c5395ecd5de406b021f797f52cc167f4b1c0bb68da79bc6356f4bf0b23a65dbdb223e1fc44b3da8fd0121034ceb45d36c46d20f3cb094ebb152e74bd34376744b9579be71970d93ec73a84402483045022100ab12edcba4d7e3f9d1a05a5968b78d7bcc977c1b8e5b971c0834dc43c5b52fe402203f7a2cbd2f2c6f607f7bfb8ee069edce974824ba63f85be5626b972aa88caf5a012102be16c5f02b2ad9666524afe75008496567c7b32f5e7fda6d935bb5cf1bf6969e02483045022100901f21573f2b43861bd5c052f05eea416eefbe06b513d2482e7abf767e2c96c402203092fd94396127aff55d0d36ea0d52abd5acfca922ccd5c18ea0d669bca057070121021f5c2e322c5cd812bf53aa768b2d10c2f805ebe8c5e8ec844bde5229ce267fc80247304402202ed9261934d488439b8a237b8e9a6fea5273338fd347036a63fafd59b0ad5e7d0220771364eb54e52197c9b7c98ee33b4e76f9ee95c3ff719f5f28d45b9785ecd2b20121030085c1f67ec8c77fbc59e14dcbe96e1f326e437cf594cee45e2802fca8db13e5dd8d1800","133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62":"0200000000010149559bf09c3852fb1d592d23093a8880be3c9ea9f2156dd72caa46065f0893330300000000fdffffff037891d20000000000160014cbdcec59a36d7704220d9ea57e8f73a34a320b78807c4301000000001600142a4bdab5f3f1948a9db9d18446f4035e92fb32a640787d010000000016001455c75624f2b23bad6cea420a2d99d01365c752b402483045022100e8a0c1a5294abd5b6ce8d0708f1130f65978182ca0e812fc8b86f7fb9d2c816c0220496c63ddd1ffa0d706f2a862c89c76a815eafe69e1c20fe9c115001167f09b8b01210232600ebee335ee2d9daa5fc9d29cb87f08d373b60c2afe4a746bce1f4dc984c16d851800","18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf":"0200000000010150b3256328301865fcb86f579d4cc05279270b1ac54f77e29d5831b0cc2352310100000000feffffff01b65b1e0000000000160014dbc4abdfda4cd130c45f8911f67f7a9eca700ab90347304402205a1a37dc879e813c57231a090a3357388f74d76d8a9f56e397cfb2f484a0568702201b47a2f627bc869aeb97e0ea5cbaf099fd3b7448a6ac04a9bed0795aee9b529501201239a020c26f312b2f98ef962f57f2174d59d51877626c226ba6ce16d034cc046a8201208763a914f8e749b7c5ad3d1c21e6cee72e5e8fa746408f77882103a19107c132cd0dd808e6a475fa7198097c0d4276d69769e2839596ee72e8cee6677503f30c1bb1752102f007cc8ee8b72377a7b12665cd365034ec214b484e90346ed677907729d020ef68ac670c1b00","19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801":"0200000000010152de38a6a717249c1d310245e707bb8c075fdeba85921d98699c88a316f9390b0000000000feffffff01f282010000000000160014812fd7350514958c87af121e5d0e7d54cc8a8a4f0347304402203f975ff11898eed3d6084b66e9b7f139988186f9cb62b8852e3c2728d65fd5c502203de7c73c40250805d45be0294e0092ec18cc12e09da22c845c6bbab4d0ac4fe8012047174e7713a89f09472a106b5af1e06ccd4f5d8b0cc2b66358fc3035153c78a76a8201208763a9145f4851882715e86b9501742bc93852a309777b238821020b7152458b69109bde6e9b72b5e1aa653a10dcdb1bf03ba20c043851af76577e677503e00b1bb1752103cbed54ce4d89084052407f4f50004ab6073b4ffd6a0e72a185eaa034541ed1db68ac00000000","1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04":"020000000001013e54401c5fd14de41059da0ad630abd985e7be8fdc27c0d448b1cb4860e7689e0000000000fdffffff0240420f0000000000160014c51f076c0643a392c8e57ace3095f12f8688c9ed80bd210000000000160014ae73903064f86d0b99dd8bae9176256088c8930f02483045022100ac4f389e545f6c77157cb5adb8e97d2f60c7c369367c7c6391800d9d668a75bb022026c2aea0e28c99f276a0b92e11be02e6979484973ba0e3d89a9b150c3d6e45bb012102aa77796e54c0f5ca83028dab8d24960b489bbcc8fd85f75ba79c0fe1ec1d74e7cc011900","22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f":"02000000000101383d0a7fc6353806e9800ec0b713413ecf3c347068c93459c263c5631c7a27620000000000fdffffff013286010000000000160014e063bee32b0c198c3130ce3a89ddb0325e553fd702473044022030fcf2a1efad64cf01dbecb9903798c2d3379d613be4ae96f656a46147b87bb602204df7be8916fa840d9525acfc60d08c69303ed534fbdf95b456bd446d45db2d63012103b13579b2ea3924a211616fcc7ce452ef873a471c52fb55b405b9e7ee6b2fd01520d31900","26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a":"0200000000010139e0dcbca436ddb26fe5502041b65971c6ce2f22bef6be11850d0be74f96b13d0100000000fdffffff02a0860100000000001600142c9782073483b751e482ac2cb6034f2c2ee39d2e08897800000000001600149e8d4f2a05da2dae2160d663b1a2b302c8831b8e0247304402205f456814f66be3f60f32bd91ab08bfd8fbf0487d68a40b558a438ffcfd89de8602202bbc9456605497fb7e0ca1eb64a716e06381864994eedf1a5fe8566af905d27201210208f544d0d78ee385be2b724a8ed4d9f74b830ceedf501569c5a926ac094d882214121b00","2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d":"02000000000102f733b90182901888d7d39b03b4f9031b57005a8b1b53a6c07d52945c110cbfb70100000000fdffffff02782afe5f2893a26f3f63b6b175990a5b53801d37f90e13834511e3456696e80100000000fdffffff022491040000000000160014af4630039750572676d4ca29f6fb635967807bfd20a1070000000000160014a340a1cdd08a0cf976a8dbc6138d1ef765621b740247304402200d854abdb8c02b2674e107a6b7e2423a67bf95c28eded829606445637ba126c202207ac0df686d158ab6592f5dc052bd45e913a632da64470762f691854fe81bb3bd012103167f921ed4937224b895a2ac440dee060a06b42e39d1b3e7666035f7a358d9f40247304402207449cc026cd0358ddf30a58fecd5f4c7b69dd90e08dc2a0cde73770c15261a8102207984312685f3a6373d2f3930162a71376cc9c4f8197a3e47eb76034b7e9e8fe8012102fc2767de8ba05c5eb6dc6939d931896eb8247822f08172f76d5e934462756c808e6a1900","3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549":"020000000120dede6d2a24385961cc01676c53dbcd389fa87aaf08be4c56ae3a463dcd91ef010000006b4830450221008d31c621268af558f512fc2f66f08970cba3c566b0bb3aadacb808215bfa9f1402207d259a8967224542c1dc1ac5d02d203f07466475e7e8c8028bc5751985b15a39012102440d6e2887ecf49cb991d9f4a271ebb71bca3f21ad4a4c3c599dbf0945962a96fdffffff04a0860100000000001600144ac213e7cb8eedec54f0757687026aa92fc17e8b400d0300000000001976a914426cfb6ee69e5f2f42fa9095ea296bf28494d1f088ac4c436600000000001976a914bcb2f82a41a751bd4230ab975bb93102321c0d2f88ac0087930300000000160014d02cbb783c1b8602561df3525e762580dedda016c6841800","366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f":"02000000000101359121955b09561ae4e67782e6daf8c464bee327671faa38f942d6faf54d16e60000000000fdffffff02c8cf0300000000002200208ec9e8e9178d72d3e50b4a94b89f4f3a1e31cc779bb824243c3cc0a2237e518e90d00300000000001600146e6d9fb0cae5e2d4b8851991b872ad1f5e9fdac9040047304402205018996c947617eb1de1d24bca9440614f088c268da15f1b2f69a86200ed3e8c022023d7eae5b65fb37b3014f9aaecc666006bb436bfbeb9d1d6c8df4b131737d0b001473044022019a5e7cadb28f61ede4d026b4b5cf74ff0605550a45a505e3c9742a5f995d2ce02202519d966e88aa273fe7f9a84f231905cefd0d50e659d3bd3990b4922f06b1de70169522102981da3a3bb6082918cebababf2fbb628131381f42e92068dc1683ffa3d34c3c02103542fd6968a94190934a6774d254f9746915b94d96ecc79233b1d858e458748ce210379f32d6f67f41ff1bbf60964fb2d36053e6f2c4bc2237f6824e935778594417d53ae90a71a00","38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3":"02000000000101afacb96c27ac15bb895caaca37799c8b0d2590829951ddcb8ca3a2e14ca66eff0100000000fdffffff01a971ff0000000000160014a82e8b28759a033b524f181474ab04bfd1c964e60247304402206aa506d16285f4c4c6a26fc2dcffd5cfc0c180b6a9de79e47a05e5b1804b54ed022039c699e64c6b5da70c99895e79d4767f2bea343f68c1573a0c281078c9eaccba0121021a38690801dd07e240835beb9a158632f0b8910500f6eb4a47811454573ca1fe00000000","40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1":"020000000001018ed0132bb5f35d097572081524cd5e847c895e765b93d5af46b8a8bef621244a0000000000fdffffff0196a0070000000000160014423a76ff423eb1d9350b1ef6d730054ab45a6759040047304402200f568dfd6ca12c4a6e9bf04da1758a914356abfda487c6cdd952ad618feffe9d02205a8844aab29ed07bdbfc6fac91033aa48621d2ee82777cec158f357ca77010c90147304402202184d71c11ecdd543b4b8f981684704aaae9482658dd3fd92a1ea13959612df902201eb66dafeb59ee0a45c00fd4aa5e56d12542e11ee4e898bbd78f2747e148ef4601475221020402f62988efce421d9e971a398ce3894dd22308c760473ec0d0e8f681b9986b2102111bc325b384aef5a8bef0e8ca62cda01557523e69af3c4af781b3b5ae07a2cd52ae956a1900","4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e":"020000000001013d43805662199ffb80908707c514a454b42e64f493293778863fee8c1459720b0000000000feffffff0132f0ff0000000000220020a321733bf4b16e7945a15d1d943aaad6f7be574ec677e39bdc2d74a76318ea2a0247304402203deb1aafbcf32d2f88e0a9badfea82ae45366e105acb8d15b2dff333935f2fc802200275d262de1a1a3480df91d1150a3638944908ea15cef36af431f1c90ab31d9e012103cedc8cbaaa215317369d477073a56395645a75eee4ec9c05ac65402d78fb7e1d396c1900","42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab":"02000000000102540b948e0398359964d663e1141b9d4d0ff11d0acf41c10ebe5921d1b7fc0d600100000000feffffff1bfe68f11fce4d584911c02191fd17784c9b284b76ca26aad6fa9f497cbcde6b0000000000feffffff02a83e0a00000000001600142a826cc7b1111ad48a96f5710e816a60b1487e3e8096980000000000220020b4939bd05d78f36b9e9f6bf03c30c4e67581cf7e0ec4268bea9f744bc3ebc7c502473044022029c18f6d794db6ad46233415ed487887fb44f85006b8bdcd87dcec737dca8e760220350410e5673f49ed54cd73b47df334ef97af263be61c45c3bfbd0f3a13e853f10121027b395c8a3a2a6af10e0a9801ee565504bcb1ebe2fe4018a2aa197bec04ba7b8d024730440220599d00baa756438f24cb27619e470732ac470cbf2dfe6e5642dd6dbdb561122d022046d3a8e9a0162919afdfb226525563ddc7dadaee47f3af77053f8c5461a5df2f012103e1d3f1ed07e6fa904398e66ccfb9412ccba22b48310f1a73ffe725488b37fbdfda0a1b00","43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15":"02000000000101e98933d883b3a54aab90f73b3ccd1674508da33129f20b509339d152d2fd9ea00100000000feffffff028096980000000000220020c4da95e05b7a693f6aa2d0d292d55e423d50a540062a4761aa7c92f4f664d7e764fd6402000000001600142fac9d67c04f1b7508847b3618e32c5ba325e41d0247304402201bf43b3220ab0c87399ea634dcbad9e490ec42c51925872ee4aed891e0fd6a6e0220541e5848e2a3eb155013bd08a9b80e2fed893850b1f841fdfd6b432272ba1c14012103f10b7f777cecf6c778206f1d111b7197a37d573646abaeeed27ea77629cf43ea5ea51a00","43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35":"020000000149559bf09c3852fb1d592d23093a8880be3c9ea9f2156dd72caa46065f089333010000006a473044022075917a07b65b7c6e28e6a15bb3891b98e1fad1ffdc6f2079af4bc16a6d8bd306022058f14e7059b1d9769d142ac0291ba7aaa926bf0cc55af253976c55bf6032361c0121027987fe586ee2f7f8896ef6564d6db49d5536bcaea939d5a0b755bc943859edc3fdffffff0274850100000000001976a91469a44dc6dedf4cb2c89a5522ede64b693b0d19e588aca086010000000000160014ed50e5e4b61bd709b7f4068b6d02245e69cbfa1915891800","442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3":"020000000001023f6e08e1ae725b5074b78e797cc5a0b5e3e5b2076299accd633ecbea2f706f360100000000fdffffff4a1484d03c738eafa35ab4aa92ebaa000b55471f380cdbf40297fcfdf35add6f0100000000fdffffff02400d030000000000160014a97a9bdef041d20b06496a33f9b0808a67d47c4eb4920400000000001600148bca6c534ba0caa3b1e588791bd4c52a53da1b6e0247304402202c158809e554ba3c91a4cb6ae322d5d7bddc512dfce0a35756ea66f33352939f02204ec4cce554bd9c8a6ba041f0a97ba95c09ad4cb6c6abc51a1f210cb572f865600121033817f61417c86451ff44ca34187f7ea0d07f4e04b090649cc11f6caf8bdc80ae0247304402205ca1d185512100ca1710d936c6f76bf34e88f42d4026092a15f0e9c22218c86a0220520e5488687d77412b455e2eb5fe04ad252a5c4b2273cb3cffde817504d794280121033817f61417c86451ff44ca34187f7ea0d07f4e04b090649cc11f6caf8bdc80ae6eaa1a00","48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6":"020000000001012e6c3b9be30d0974b63bdb87b39b94ebca7568e0c3ce17b75552eed484a95d4c0000000000fdffffff0288130000000000001600143d3554dc9cdeb53544ae5bd013edf6bfc08723b8f86f01000000000016001422724d9409827b7b272a393e38f1686e953d0d620247304402206012d0f629daa8cb0fcd499d0c868d9be6910472b4ce39f580bb459706152dd802200db21446afb153e4895402171db935aa215c9d924b6b729428ce1232079cdc62012102bc73056f47c2519e6a613c901af81d402a63edd4fc21c3515b3ee50271d4d2f807ab1a00","48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a":"020000000001018297092e2ed55a0000f3dba9444324c5cf0b8da5525f9cb24034daf23851b74c0100000000fdffffff013286010000000000160014f3c4a55d9fbde53d14c162a4eb58c11a5e160aea0247304402202d07ce05cbbefca036db44de0960bf59f10b6c9ed8662aceb063d90ef919503a02205a209883954fc55dcd02c7dca46d86c94d64ebaa22506096b7b7f6699234bec501210266ab2af4f31a58980235db11a40b1a8067979d7a96a0a2ce1aa4bd5e04d0ef7cf66d1900","4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e":"02000000000101040b9b5f22e27b170c53b346669666275a447ad63aeb6af0630e81fa184d8d1e0100000000fdffffff0220a1070000000000220020ae6dcbda47596fa706490ea05121211e89fb4b9a03a5f699ed22cdde3ea8632b981b1a0000000000160014786e2613cf8948471629ed179886b38152bdb3c60247304402202f6c4f50493168e1534211eda87a0b3b262894c1261c4af1ef09c0699d3deee502204b0a0e770945d16f7581546b8ec635d95e1e1f32f1bd477179122f27f0ac2abc01210327a2c042e02ea2460408e02ceca407b07f92536e9743017129629fac8ab441518e6a1900","4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e":"02000000000101e9301cdcd380e585ead499efdd8b0baa464c088d723d9eac0f1869f3279976110100000000fdffffff0248840100000000001600149e2926cb12c16b71c46de2683ad9da137f6399d3a086010000000000160014bda0bbeea75d72018b818a042f8269ac6223f41402483045022100aab3034f55cd37ce2c2d4e88aeba123ac1235ed78bdc0536e289022022e26c7e0220126f4f43f21138324dc3466579624ac81da55dceb20150c286f2f975d7954cb701210371975765db5b2a45bb4bae75da6f3132001373c34bf3dff4ea34394ec856560baaaa1a00","4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755":"02000000000102529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0000000000fdffffff529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0300000000fdffffff016623a000000000001600140685c025d82f9b1d8095d359dab267933f6c5b5102483045022100d59c596c8936db15109f881c61c4fd1b1a2f87e40ca8eec5b0222b970a90703602202a32c287cb8dbe8febc7a48a824a50a8859866ac8951f601301fa10ca33d8fc60121036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f02473044022008a4a1e4ce6be8edd5c42a2bb10704e2ea516256543d4a578457ca68d1f2ce21022043b3de363e10ac2cac4e956d58e4eeb7de8442aa9a590a7869308d0c603f637001210200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc72851800","50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc":"02000000000101f428f94ec07b2db855bf3c4fa1f2cf52cafda8549c7276a30eafd34620bb99750000000000fdffffff01e884010000000000160014c40f099e35be3549b1ff204c9a8857390617b00e02473044022054b9f765cfc34f3466a8140e58fcd0728592dea248f8780fae8894be4d3c92410220022c0934c2ef669561abea5d7725717b79985e0cb7dfcb4285d8f2350e981f790121020882352af1f0e881af998db72440362227b8a704c313a346a9f31ed69183f61f86451a00","56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b":"020000000001017a5b8363fcf2d9ac45bccde684036064a57745032413979f5717f8c57ada18ca0100000000fdffffff01faf8ff0000000000160014a951edb4ea492241dc29d0ebb6cc975c6fc5a8570247304402200517f5a2d82af25ececaddc99aa60e2add01c9e75eafccf7edcea2c2dac84e3502203f96021cb8863c9eb54822819d7c91f2b951e3ee7d81c1519c32f661371c81cc0121034a0d29b76f5e49294f06383756e43fc7ab878c8c44be405a8ef4a35b7e99eaba00000000","5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b":"020000000001012c5783edec9a110c012a72ed737acde1a97ca9b4cc618384416f61adde5732dc0100000000fdffffff0220a107000000000017a9148d2301dae8ecde65a8e40b5c032f08733323f05187ec2c2c01000000001600147fb29ef38d699a9d5c29c640c0e1d94d1c141e5d0247304402205c97a7dbb86412bbe591f2e251bc7a5e3bfff3c0b0962d3c0f04394413c1866b0220145fb4f7f5ce695a73492897032dbc97dffd4b4fd84eddf4e9376c95a7c0b8fd012102fd3e5f7f16949777fcb27232fc4264afab433c7f4dbfb475ce3921aa7b10c06a45a71a00","600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54":"02000000000101359121955b09561ae4e67782e6daf8c464bee327671faa38f942d6faf54d16e60100000000fdffffff0220a1070000000000160014fe58a514623f94907f1cc0767030a301fa70e3a0b0400f0000000000160014887c8702f254d5e44c9a170c14c7b200c1d04d360247304402205b8f5cc055c3403828cadd6d9e689473efc39ba6c71f009d659bb87481b6d33502206f441eb6a069d9a8ce30264bae05d78c9306f0050594f0e773b8594f378a4f53012103a18179864ca030e8f840cf38828a8c2d9999692f3d3280b35f6bad95948ff21a07ab1a00","62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38":"020000000001019dc51e473824610ab60c03941b7b0652c778a301398ce1ac2eea74fd3a59c02b0000000000fdffffff02a08601000000000016001448fe6a045d2b3791698137a0e59c817cf681441df709030000000000160014dc40ef96b9f1b1c54cd9389ed8b9467c1e670c3d0247304402202dd9810e77538682880396afda3dd371bdd283c9a9938b457ec779675b22ce45022061bf32a97f00bd6c1a26d1816457c41fa8aa9b4116eeb1d9cddb6877c8a2e1880121023243fea612362b6815e77bcd8ca683c25ca645d8f46a182d5cca9d869f10561c156b1900","65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc":"020000000001013f6e08e1ae725b5074b78e797cc5a0b5e3e5b2076299accd633ecbea2f706f360000000000fdffffff02a0860100000000001600142274a4e2250437331987f4779e70bde6831c75056048020000000000220020b0f76bb39db4b448e02b627ce44940f16c3259f05b8c6a7741508110c8104df3040047304402201c52ab2c60da000cebd61afbd2243c04a8d4a8546f45b71dd1b8022c4eaa87720220555afe691270a86680ad5bcebbca2fb061c631e9b15c13209cf29403f73520a10147304402203e9e699d4d7fa4abdeffc29de54ec9ee919e9d64ec6232ee9abcf9fb160467b8022043d064580f44cb013a7a4eb54d6f0899ffbd9b0412ac4ea288e2df8fd121322f016952210225dd8bab4e55853f7f0f29738e7a65d2d80b83a36f25a72383fd29ff756b876121038ccfb1267cb871b7d2e9786fd023423384089e7155496ba0f5adea95ebde160d2103c71bc2b94e5e425d1a4065e4caea72c52bae848ff87e8dd5cf55d3fc171881ee53ae90a71a00","674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4":"02000000023fe8d4ed23e0ad785a584148f229d7e05d7ccf81870f0557c4a59f5e0ae8ed5411000000fc004730440220597b8e7b29af25d8b923c8890125cabd34c3743a69256b72a6873cbf4e9b20d8022063327f53c2187dacc1458130921674bd712a46b2cb2974e899e14a34a2ffc7dc014730440220579412961b76a459283f9ceea056e0b53a437a674fc9b83338c0b87cb7b5f0a202200649fedebef31927ea3a72bbb97fdba74b6d96bb7e91189dd027e1f753e1f665014c69522102ba13fa0ae4c4ff16286cb456ecc99d8b62d2ca3ed641879a707e937960ed940821033c6e501ff0f7c5e10ce2d275a32ae249171ae1cc42a471e11cdfde866b8108cd210349126e756301ce867810a498561f4c1090acf8f7ea8ac59d8d165598411d208e53aefdfffffff0c92a43b0dbf840769e79cda80dfe927ea05aea7b621d1f910b78b57b9b8acd01000000fdfd0000473044022047b474c0f1324b0a555eb2ac31dba97879953bd51f2659e056b16106523cf473022028db2ea106ddd6c848b950e5e2842df3c6f999574b93d7b47ba7d427fa6c20cf01483045022100a3b2efdfcab6909c81759e63ae77b7779a34e5e507455cee3dc576dc6cd545e70220404b292295b468fa7549490a1c3bb39827104ed1411c58b0c69c6dc6dd375d5e014c69522102713f42b3eec7346af75178a1c8932c071e9065fc29e30c95cbf016424c294ddc2103256b6de1c6a0fa29eef2dca9f9eb352a2cb1bd846bb0a7816f80f377254249b721036adc4dcfcaa42d0ba4445dcd66c0bc35a9255174b9d15fd299132a653892b82b53aefdffffff02885801000000000017a914e02e44af71e72872f27aea597b44510c8c1de43587a08601000000000016001411455e3a8b9dde490455ed429bb1ee1872849185d1011900","69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1":"0100000000010189f26971e043ec9561455b1d53313e25c551fa0ba152313beac8f0831b4d348b0000000000fdffffff0210270000000000001600141d9032937978b2d467720b06b3d1430280912bd0b8480000000000001600147aa10569c55277967928b922ee954bd9f85a38fe0247304402203659c09a488db68421e5ce205ad2dc0ca9dd2faa53f54999a3fcafa5d54a5e39022057a82693a173c8ef26aa87b3c24f8e0e36c29442210405addead5ff3fd7e8836012103d1b4228f511421436cb607f4be5ad2d42e3f149704a5a50ca51c2f04831c2b5d608f1500","6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b":"020000000001014b51252a3ee4a4d1a1a23653d55ac6345cfd78eb9239ffd47b82549bee0fee5f0100000000feffffff02a495930000000000160014be4507a9f1e38ee275120ca142329e337705f5fe809698000000000022002000a71ad29758efb8d6b0574d5b0fba16ebc3152516d238ffc5bcc7287aa688db02473044022033671e1b249517b9d5473aa3b12bead09f6aa2974dea6d96699c164d99677ef2022013740ae88da9e92dc1517d4b8b91c0078be0f1ccc133e5237355f2753148c1c90121031befa5da2f79eca0f75b6000c5163e84a15a9486771420d84745986f69178514da0a1b00","6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc":"02000000000101670c80d8306cd206e010033b76b3dec4876fefe58aec73d56c3ed915e06d2fd40000000000fdffffff0220a10700000000001600141d369d439b6fef2180abd3be463765d3c5c7cdebf4a00b00000000001600140c42a6f94892bfe1e85d7d34b472b914383906e102473044022056e0a02c45b5e4f93dc533c7f3fa95296684b0f41019ae91b5b7b083a5b651c202200a0e0c56bdfa299f4af8c604d359033863c9ce0a7fdd35acfbda5cff4a6ffa33012102eba8ba71542a884f2eec1f40594192be2628268f9fa141c9b12b026008dbb2743d151b00","6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a":"02000000014b51252a3ee4a4d1a1a23653d55ac6345cfd78eb9239ffd47b82549bee0fee5f00000000fdfd0000483045022100827147968d97eb2c839d6ebd18af1c247aeef1d9914d3d69f7bc423364cd6d1302207bd7f3744ca312b4fffa54ed44ae4628fdaa77f04578c07f2bb6f79f54a721d10147304402200bef8afbc94f839296961922e7f3bb04659c7b33439112e8aecd1287d6d6ba7202205df39f4212e51e39662480d221a56f3bab9187714ad73909fb4ec806fd670ff1014c69522102b6aae77f674ef3eb5ad3a3bba9af5ea7a9374822950ce21a87e44a9245b72c182103b35498d1b18fd0ffeefe9ce4188a526a590439a42833d660ee39a04a9c0fb2502103c0696060ec15e5e62748353eb0a001ef6fc1b59f09a1def067990d78c0a9ba4c53aefdffffff0200cf03000000000017a914aafe0188ea6c7cf51b39fbdb1fe6b508dbc646e68790d00300000000001600146e6d9fb0cae5e2d4b8851991b872ad1f5e9fdac97aa71a00","7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506":"02000000000101eea31e951f2da9ed75369f1ebca77d13cf582b413464df36eed75f968358d8d10000000000fdffffff0294c00000000000001600147723e79c1157fce6e189ba4ac4dd1d104e412109a086010000000000160014ee9a849b02c4a1725d9a6d716781908ed0d935480247304402204fc7aa5709889fd53ad5b30023b307ac95de8d4dc4088f3a0c07db6cafb1c4b702207e07a06b3810cf89506a0ca6c0ff708e12b99b552d214231a960fc6f4b63cd7a012102a8688635248bc44a7c70f5f0dce799ed3f37e52418817919315d6b239700f7706eaa1a00","7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55":"02000000000101217fb607501f5f2e6660638402924c5380c1232c99201d8bd753135b4325b08a0000000000feffffff01fe2c0f0000000000160014d90fc04ad75354dc2262d10835cacfa4e84ae48703473044022056c6d8ebf46d9f68fcb83e0e6960a1785d42ee2384d64f8604b4074b9c0de0a902206fe977d57700504e85e6c761e788e9816643999f8bc885aa79ef20db7b08dc810120cf62e39adfa255e3fd410b31b3f402c1f9c1cdc6a935ff719f091226b954e6aa6a8201208763a9148cea0d0439af5eba9f7bd617673c1e69d54ce9aa882102c2e4b61054447297b7a2b9b15aacb0a21713a5676a1a6fcdc069387811935984677503e00b1bb1752102674adead3e1548cb4298eac9d6d62db66c63864b752f48f66fba9be12f0bf1b168ac00000000","72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390":"020000000001015fdf6216a1edf986878f35e92ba5747885eb06e12fa2aaf277ddfc557cd7c3220000000000fdffffff01c485010000000000160014128c89173dbc0f4a0f8dbb6a61e7ab094a60c6460247304402202d2e7c9a9bbc234dc4db6e306084701ad7457c1ff2f123a1687a9b4b40651f5802200f505b075926efd011c6eb3080f3d524f47ec3ac6a4fe8cc1da0e6ef1666b131012103ee66ad83bf88fb3f79929075aa9cad3df5f9f9ab9e679624daf19a90ef1a9f443bd31900","7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4":"02000000000101040b9b5f22e27b170c53b346669666275a447ad63aeb6af0630e81fa184d8d1e0000000000fdffffff03a086010000000000160014fac05c98a743b6aeca8e9cebbcf81310d0d98557b819060000000000160014fa89687b7c7f0b4b8648ea3e47f0dfb94e98140820a10700000000001600141ef267f1ecb6538d987a4e1503718d35f5e094b3024730440220134cc1a95deb967ce52d4024649d77f5da8a0a1ef67e8ed5caf983895d26bcad022062820d0ea14925eba8b018b1c9133a5ae67e77bc43b52ee59277d4cba4b90bae0121039015d8c169a8a12f689763b2774e19f7c00e20ab3f009689254a79e65f880efde5601900","75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3":"02000000000101771806666e0ada1dab1ae15b601fe9e475fae4090adb0b5f2da2bc31ec25aafe0000000000fdffffff02400d0300000000001600147da9196571d573ae7956ebf8b7720f0472723de95c1a070000000000160014f6bc8c5cfcc7517546f57a469d9cda96f11f01860247304402204e0440c23b2040eb929d9fb613c2fe6786591f533209753485d6cfe5dc21dc7902200d38da5c8cbdea107cafe3fc807bc070884903d2aef5daf147f3cf859736f03701210370e25ec03a8ce13b40c17fa6de50c2571309f0c4c64735414caf3139e4a4cd1c41121b00","781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05":"02000000000101a34b263554ad4996c506866afa47e378782cb99d544c1d299e05688c844627440000000000fdffffff02d8850100000000001600144215d65c968be07ebe7e52cf0dc708437604378ba0860100000000002200200ffe3c46c354b37cf108c0817fd97a8f406d983acff859adf0fc90f4693d70c202473044022045efef7d761014f60621e1874d5278572e0227881b676f37becee445876ad699022026115c79df4edd5652ac384b835ca81b484c54cf2966efff6b665def71ed3f8f0121029a6fa868e54dac3ecd33b63ecde197e936f2afeb1cfbe4ed610e342b154ca30135151b00","7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d":"0200000000010115fb06d9938141ffe4fb9072559498c12620b4eb4ada6a8b29a9105c9f97d4430100000000feffffff028096980000000000220020116efd5fc9445470bb55ef4c922e14a13a97e1833861d5b7680b1a976ee221991c66cc0100000000160014e61e7a33764582d1d1c10310bd348e8d214b807302473044022026585abbca5c31ccf751cc9948c06de75d2ab8a2ba0f2a0d2a4678769860ca01022064e1b14ea596e7a9bdd351351433a52ff07bef6d1275ed3f88a4171843e6dde00121029a412b2f222badbf5f20b13dd9d1df711c84e2d4cb15c5d798f349433c757b9861a51a00","7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067":"02000000000112d004b67c3f20e564af8a1ec46db16279be55ebec9f727b9db66c1a9881592f0c0100000000fdffffffb395dd32cf04fa8785c4374f4399d155eb641e43e326bfb41f22549f08f1b8380000000000fdffffffe1ff9efb1c7b3175d6bb7fd91cfbaf37169b735f0160a1f9e61a4ac2c35eb0400000000000fdffffff9a52d653d1ef6567d22ca994da08557e78fc07f93d68e2dfb3507c0ea315f5480000000000fdffffffcc7dd19f508527a7199d7afc9cb79411b8e2e0319c8a7a30d06dc03e860d03500000000000fdffffff383d0a7fc6353806e9800ec0b713413ecf3c347068c93459c263c5631c7a27620100000000fdffffffb44190a315a60c1f6e67ad5772db3987a06486da223d3fabecbece3c078d4c670100000000fdfffffff157f70aa01e34954220e38ad8111d9aae7eea4d4d3205abf4f76bbdcb3a07690000000000fdffffff908331680dea290ae117d18b4a5f9878b9f75ae6b3c164fab9d533608f9c52720000000000fdffffff674835df83f869ddd411828709559267e6201d934153c0ba7a905401be3087a10100000000fdffffffe620ec4adaa37e408b6bc8abd32af295462f1060abf94c3244bd6fa5e01f1eac0100000000fdffffffe2bffd7aafc4fe2e64fd46768ffe8c328d41c9fdc95030aaabcc76abb4541fb40000000000fdffffff528f3ad6bed1c06de1464e4e6c5a623df2938c030d4676b1a767c109b929cbcb0000000000fdffffff528f3ad6bed1c06de1464e4e6c5a623df2938c030d4676b1a767c109b929cbcb0100000000fdffffff56b1cdeb5224a6fc0a22593cdc936626670b41f5e43932749697e35618bc87d50000000000fdffffffb2a0f40c1b6e1b8d42554d4d58d31893491ec88dedb60ab6c32074f7ee8755ef0000000000fdffffffb2a0f40c1b6e1b8d42554d4d58d31893491ec88dedb60ab6c32074f7ee8755ef0100000000fdffffff3d1ddf60caebe1fa31bfe5e80e0c6117e916fb4f0ede32391989fc210164a8f80000000000fdffffff01c09f1d0300000000160014708c9e61aaaeeb2a935317613e606653564add6a024730440220394005e6d811a7e27a1d450885e0274c3d3a7d62a7d8c1ff1d4b73d01b11051302202734f752c8ced1494c2ff24cf99ca1f5cf5d76268561331aef9457268470f1b1012102f6d8119faffc8650cb12f120de5283d4798d8a6d20191630b78c7768951122ae0247304402206e6a5ce1d9b3d4fca6d9bc3485596ad593891593e30c4bc770d17abfaee395a5022018dc4747470dd43c34634059766e73a4fe99a0b1e7b7af8c06474fc17a0518e60121028b210ec4ccd3217670690f81d1fd75390f6792bc558a6a93ad9b15620a0281f302473044022001baeaff5cd7ef8adc937467bfb4b6498bd66415d39fffe05343affbd895155c02202184031dc89aea513e3e3c1985d36ac1f32e8b102744b69b77e558f2f4340457012102850bf25b060b0a79d72ad0110247e7856d5c6452d684785daa3137d5d8c90a510247304402201ceccce75d2af1f8941c02ee421f4e6f404e87e56c246f79ce19c09ee0e119f402201eb003170de726a60ce7a60f9d53b3c4e748cfb9b9994c681acda0e72a92b26a012103ad9cf6d8b2d59e6e5de9b74a720763f55d95db451dddc7216df5625d82028a6302473044022071e07a61b137def04dd364b6d2e807b18d48bada99ca4d71cf8439b1efc3af0302203acb936c7f49fa2c025feecd5cab330c6a232c1401e89fbf8775b3d375b2c9d30121030178b7d23e9ff9b451ad76616e3f031be6e7a113fbbe6ac91f4278555b4cf8250247304402202650920eadc36bb635f45812795201c9765bb3bb4457b598860d8da7ea6f073e022012d99927e13b19a257c0b42fb41a390942e943548ee8d091c0010bef6513e6da01210221b33a4f3c68a1da5546bf165fcd07d2d12d030c32c26060945ee7173ba3b84602473044022031288b3af2d909a0b7b5408811c33f1fe99a3c078a66f6a106facc0fc5cd2ed902202f35bc8da7d3142b8ee51da13b7f1dbe1581508675b4ce98f1e782d2495e7d04012102c379796daf31645f2126029d2affb43ecf6c4734efb0b546ad18890b38a1cd0d0247304402207a8ef2d0cf753f0459b74b391de09222035f75ea12c428f1d78ff116bb4640f302204d018f083a01d37ce62f91faba1b4982e7ac8b103fee83ddb0e74cbdd5bf817d0121039fe845d96ba9abbdc152323e25f20ff58dc9623bd6cc3fbc82c4830eb011d3db0247304402200d9cd669cce905a5c0baf4a982431420daacfb5890b1a090f0f3fe6862ae8c7902201021b5cfd837d58a684a583c9086dee35d7961681314bd907e07ef3759fae8e9012103b682489cb0bcc9f5bc430e987981f4688e89910cbfed23abc8e12aff5bbbbabe0247304402204280a064196292677439e6d818385de519a79e570600e42a07e94e0c24ad89bb02206430a3625e8940ebb51ab59f0251aaae61a9c9999214007cb4a45dfc9ce7a44a01210210b0619e1ece91cbf8e797e410d2fdfc60b855133eddd5f870b22b29423f6d710247304402206995c91f7da5ccd15bb2b4d4d9dc1efbffd26611fb54023cbde92d279ec8983c02202b3b7e47f488ca5d018f5d1da157bab728e12b2e271cbab80d87d53767775e36012103b3280e632280f8cde9e19120147358b77ec3f7b6f4ba9c84f164cf3b7ba7a79c02473044022006028b175a23c4da30f1609c30a80ce6b5d5c08d9857f5b2339f274acc168e7d02202574a3648f2833f7b5ef697f0b85d5bed382ecea840aa844168b471b2023250c01210320bc7ac7e9b9e01db74bdd7d5b4905647b657b0e57d0a91cb0c317fa9d4672bb024730440220014ca3e53a8d0a950d3a18172e3df1fbf13cbd2c587a9b5aa1c44340697b2461022024ede3373892f1ae502a4ffbb761a7d484fa567d4f975c20e06c430b1af268b4012102a8c26f0ca20941e5675c847ee6ca78e387c63845f4e49b8b9680d3717114530802473044022011d98a594c48af630a7f0632a9d5d7a311ec85c48a0f37a681df5f97105d7666022035871815b14eb8f46532184f7cdcac0d38683b984355f484053c5f8664bbfec4012102dd3eaa75f9b5bd1f26451b2b048e1b58316d708743c2a3c482eb0e1ff12d789b0247304402206e9b717de5d1e698dd7f084278c26e1213fae0ea2b47b70f4f4918ed0fee4b940220428170ca0826fe3e83dc0fdec06c202d5a96fb82679790952ef103e15e1f8c810121031fa8cc56975d8aa5dba41a2ff5c3de5a0ad760c4b2885b960404c1f933c801e80247304402205459cb2c56fa278652f40cfdffa98eea37b34d20974f754d66deaea3ea93a1c70220264bed176d268e37c5f3392121d885f3543b93cd97ad027a926c7ec316b5896e0121025d87558f20c8b9f2d0df278303833cd35ea5b85111d9259a18f93a505f026ee4024730440220799ab8195364fd824fb1cf3f42b01ba2da3189527eee5036befe52f0edd4433202204355621e136912fc3c6f157a4e650b67815617662cc23079936269b2a9004adb0121020a99e99729a8d03f3f4d6ce6ecc811b4beb24cf4cea715f59327b46215da08c1024730440220741c8364e4972b6e32fbd27e43a3863196dca67cfdb012a8534dc21328535a8c02206811e3866778ae8606d087ca3ff2fc91ee6671b8d22a88d14701859176d0bd4d0121037f5d9cdf431065cef3a5e6d5b2d9084f1db6b28f91caa840bf6249ac23e91858879f1a00","81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a":"02000000000101824a4760117708acf040b536e8826465df9c961ec7150070c5b98ecff0aa8bd30100000000fdffffff01b4dcf505000000001600143d8b00a94c9727eba5f04b20a99f6972ce6fd62d02473044022003fdcfa7c316345121bd632d58f752bb2494d2855b79ea0be3c177945df439bd0220606b17fbbf4a691c4dcc6b9c3cf850acd3bf0196548e673dd3bd6a8f212a27a00121031b10087aa732c0d30d30374036078e32b3d27ba6cc87e806234524017f30c5ae390b1b00","85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834":"02000000000101a169675d425773fa59147b2bec9ae28a9d70200e9bc40173652c3060cdeb12a9000000001716001422309b95e817d27c649e57573eaafa0bd362170efdffffff02a086010000000000160014e910558aba5388bba5a6769060f42e686843f10b88360f000000000017a914098f87a72795ed628c0e9789ede7984a7c035ba687024830450221008e3b7ea45faaff213ad719a16f5052cb265a2af5fc19f0bec38874551114da6e02207911a3022a37a78eb034de7d1b537e7854fe4abd4b1045b2a757c50c46df9e7201210240dced583553aeddbd0e6a75d08944fe638ed89d3c3dc80f330af09a575400e752aa1a00","866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff":"0200000000010114456eae9cac04d9192527ce27b77a2ea5ab88def7669b416fed14b5b87a5a950100000000feffffff02ffffff000000000022002068eed609d74d05b2ece890a372e78d27a94c82d80f02a2a177567e353f3790f4ba535f04000000001600147d774619028616899ec8224d8cce21019a3510d40247304402202a35e3caae822a5071e3cbe16895208571c0aad44ace141ae47deba35149501902204b40d3b54f712d8cd4d1845587840d1b60eb810d7705e66469bca2f269a62fb60121031e449109ca6f091abd125eb2734530e0ed7ffbf7b6b69e6c92176732a1db5c0c3d0b1b00","8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c":"02000000000102f7116fa5f4728dcf1151b1b2d133a190bb69904c6f452651c377c1a4c57d90af0000000000fdffffffa0ddbc2ef967817407d33c3650e04f81c57623ca8a4b3bf21a58cc1b979447c20100000000fdffffff04fc160600000000001600144091d4a839df35629d96c18b9545111b2065aba258a0070000000000160014c0b9e09a8f63caa12f97cf98676250284fbe590f20a10700000000001600142ed1ddb67b7cb178e7d12599f4136e4527787b4b20a1070000000000160014a65bfd8e799d4e5b9a4dd337dcf8397a1d32cdb602483045022100f8fe2ce0c9e49d67684a5f4b8c227e50771f066da68e301ede49d838520572f602201e53c69200e21c151b6159beb9ac7f04a719b3992fca19b3b3ac9ba7fdf2e398012102bd47e76302733aef1d811e6e093900692b70fa6dddf4eb86fdd39d8a196c079602483045022100a7139ea2141e99a3c4f0c881ddf0c248b97b26cb9a28de3b45b59e7d64d01b2602206f517b08a1d679778a52c36134cebee55e56bef0eaf8048ed63100dd31844e580121020402f62988efce421d9e971a398ce3894dd22308c760473ec0d0e8f681b9986b9c2d1800","901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366":"02000000000101aa9b9f6533ee33a07c4097d5ff2e13937019aac75e8fecea1be720b1145b4afe0000000000feffffff014f95980000000000220020ac4b15ebf67e62e9f9f1671e8502a21bd615bfa95868b9a2525bc63de8b8a4fb024730440220106edf9eaa446e1a097371a43d14d54461b05070022f8c01d28f46c6662153a202203d3c74ccfb737ab0ddea3e4f632b2061d5878e223eb2a72bf3bc318f39b5c67b012102677bcfd76eafe311d0972baaf6166b62e5e545b684a81d905b85250e5917bc013a0b1b00","934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5":"02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800","94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d":"02000000000102f428f94ec07b2db855bf3c4fa1f2cf52cafda8549c7276a30eafd34620bb99750100000000fdfffffff428f94ec07b2db855bf3c4fa1f2cf52cafda8549c7276a30eafd34620bb99750200000000fdffffff028c180600000000001600143f52667962c0a452fdc9ae57afe6b6571eb22c0820a10700000000002200200e08faec6735beb913f0a7179e0dd14f0ae292b0233654413495a1006eec84a70247304402201d0a22029b241f4cd29d263ffc03764c8bccee849967ad1ec390a32284add4dc02205ca46bbe1e9252bd6c0fc9ff6bf3b5d81e69913c29dbfcc8693889f7a4266c27012102f622e0fe7a26c9c9c212d99bf7e03963f7aa1d39b2e8a557552122820ef44db40247304402204623b53d41297def5b685437850f2a0a5ba8a9531496de641b70b0d5587a0b920220346eb070e7cb7267f5f9178321011eac5c700d98f66ad54157edc7801ccf22ff0121036efd65489509807fb2602db4fd51479b285135f0be25d59bdf3b78b9e7ca88218e6a1900","94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9":"0200000000010195729410827cc4a43abb89ef0a7faa4e41f7fc66c64dd81c16fcbf6e8d2f51c20100000000fdffffff0204870100000000001976a914ff9a7edffdfb4c3742a6f362ffaf11ddc63d344788ac94d3010000000000160014c6f7c545dc197cde1102a2aab7fa157a1a612315024730440220137098a9c5342a5f5e235798c554603343be6da4334b116b7f846cb07b664ce602201433b8ff720b0d8629e605352996d53710859355d9028cd58f560a0deffc9eda012103eb10d3530984c437f7061cf7f4b1ed5677471cab80b18cd76c3a89567d15f56b08ab1a00","955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514":"020000000001010a23c987f7dc02b8b02ce98f1c8a7c331cb5fc508963767783adb6c75478c9810000000000feffffff02c9879600000000002200208a2f43a27d9dc7872cc662081c533e258990f681a90babd8fa61efc7466eb83c52545f05000000001600147164dcfdaff09348c9c7a32fe0cd9533721a2a2c0247304402202563f9705946111c6f13c3a44ee5e8915582281eb64a2c28dd39519bc2b13aa102202a7adfcffc7cb90fc0240493f842615ceb512aee44169d7c06926cf09500f0e301210308df6757696a7870f75cd4b1de04a10fcb3163c3e014e8c090581882b59c35f03b0b1b00","9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd":"020000000001017102c148a879068bc7eb23559016a9a6e69606420548f291dd3c2be8529a68ae0100000000feffffff02ffffff00000000002200206cb96749dae46fbd7d3c6e14d44e2ba3d2ac43b71769b4ff75e02c65d1fe8d97d0dff503000000001600142d28cefa680a84210d2edcd45a7a8cd5a004886c02473044022002aaead668b30f40bc44842f43febac77ea0852e81db006652f3379919a48466022045fbb17b7be757c637ec8e45abd92e52d1e51ed8498005e740c1c5e8b1571392012102a57eb2dae8cc038ae16e778125b854d06ed9cee36d55314c7c46e57c7290fb66e2981800","9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2":"02000000000101217de7a4c5cc7c86a28780a3503fbd675553b8fe33179ec1a75f7abee4f426ea0000000000feffffff010b9598000000000022002019d8c5f534f0421556786462fd239cfd48b6c074ee4fa00e5ce042d79e8ef49602473044022025d64859e81e55318279e5f83bdbc4b39cc7b7325da7dd78d8dbfac1fe3f044402206ee91f4bb9e7c804f588cf340fdd5e4b2f844ccf87e4817a95ca2a5d38fb6aa40121033188e837a2145b5c1b98f0e6b05232c4af07462c4d5fbdc325291808aff56db33a0b1b00","9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e":"02000000000102bec4a6f2975596519dfe162c636967c2716cb12e5e93189735a0e341d59cc6090000000000fdffffffe546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930000000000fdffffff028800310000000000160014700d20a48be81e702f3c851b7724aae52c24888c404b4c000000000017a914e2ccc6c05b1781dced3a1bffaf6c68d6e18bb5da8702483045022100a2d5c8766f25c5805beb3401e6c31577f1b33dc2ee991f5e17dad677058723a302205e2f745b03f57c463e6e90c634c2f26dbb3d787bed6927e3f04f7bd31a2645620121023a87f5598a10286ef18f50fb042436b5c6ce51aa43f10fa1bd60e2d2adcd7cd6024730440220510db041d21264d41cfc8ff691aa78ee9e3a25bdb763c4d69d17ae9c309cfb9802201b519ab7d491820b8bbbea848274cd0e3329f19c7e6e0f8732333dc995bf60f80121023d393cfe08065459ceb17325fee071dd983a12d376c2bb34e90461e7488811785f8e1800","a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9":"02000000000101942d53c23fc9fb59f37b73180418427759c4ac2ac027d76734b473403d35ddb90100000000fdffffff0280841e000000000016001426b9ccc02e06cbb2b9b266dc37fbe54b26f464b1ac94fd0200000000160014eb23a5265f7119c1f7201511b4bee31b185b84b202473044022000edf5442332503900710059f60e8312a61cb1a5a0037cde603b3870f1cc654002201f14e9e0b81bec823d3a6df1e581f9eb3a51ef0e041006177b9cc28483d2e01a0121039b1b0b2c9994e4a3cc2070b99eafa35412afde11c06756f6cedf06d4a64709506f9f1a00","a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867":"0200000000010187a621e402bb6dc6afb8b699720967bda2e2a6d6b51d3352c3d6341416a9860c0100000000feffffff02ffffff00000000002200206fb3cae6030e3a497646453ecd3c05a0c253a79195798f402c452a8d5fdba315a0def50100000000160014d7c5b337571d08ae2c34252d3ddcf6ca7b3cae81024730440220795ad9ba37fc9e7b34222faf54bf9723131200c2264b6dbf45fe5913ecbaeca3022063b6d3a65af1ca38993deac651c35094b9e3f76731f6914b3c1825499e0fa5420121031359f4b3250369fd496f35c8f37a3db10c84eabb3ad9de13907618ec5c5976a9e8981800","ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6":"0200000000010161bdbd30a49e41742ec8b41ab69801687becb44540706bdfc8aa034f6515da010100000000fdffffff020000000000000000056a0333221166030000000000001600142650fbbd243b1d1d81fdfb1a94e5fd31dbf5f7fb02473044022008da90e08be152bc377405e8f8021a36d84c313a2f466b83a8e0628eec7d11a6022006c80ab66477f05c5e86479af9fb1f8141f073d7f2de5c91c1c5668bf63677eb012102fea1fd504bd69df76fc8ebadd921192c5d83fa45a300a4177e28fc0920c704f6f39b1800","ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271":"02000000000101cab7d8923a6c14dfa0c81132d9390df75d2802eca35870c3ac6a4489a700ab0f0100000000feffffff02ffffff0000000000220020688742f8047a47d131b9767e0f88c92f1e7b511b04b597bc822d2d2bd26dfacf68e0f5040000000016001407f487bce4dff09d8ece41fe9ff97047490d42aa0247304402202064ac3ba7625d658bd51919e2ef6faeb1b0bd78643bb031064066b9dcd71807022050037c48ab9ca6bc862ab8c5d034d9e2d7c4ff16167667f2ad1dc5fc8a4e91b1012103706703ea1266b396a24f58dfbdf8e31e55071003b5bb69c82d5880ac4814d585e28d1800","b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5":"020000000001018c6d5a1b0a0b34113c0f73cbdb61ad490743dca7b9e3f335999f209c1eb4278f0200000000fdffffff02780c030000000000160014f45e8133945134b7fabaa27ebd7727ccc8adce17e0930400000000001600147aec8922898ba60e41db587b75261b305ecf947d0247304402201f24f822ddb9c1792a912089389ff7161f98f7fe001fdae299bcfc6bd13463b702207099f7fceb0e94d2c63215fa0265a60ac074f0b09a66fe2c18a2fb8d181f085f01210228fe856f58094e4df1531cd385d0864e8cfaec9631f31aa64ffa593a96e8b5cc679b1800","b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2":"02000000000101d004b67c3f20e564af8a1ec46db16279be55ebec9f727b9db66c1a9881592f0c0000000000fdffffff0196a0070000000000160014b8ba47facc35b991ff3fae74d0c2316ac7121b730400473044022068dde34b246cb6905476240e97d2c7de9477602e6ab9a22dfe66d01a27166106022022903cb12a0588602048a4129540e15efed8a41d2fba7607dbb1e75dc50fdebb0147304402206e8409516f1a99a8b346b7cb7360dfcedc0da6ec05b39e010d7644edd3f14c0e02205552e5456a204edb84eee4713e4c6f93e8c74b753c0f361c0cc157860c3dd84201475221028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c521030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c52ae9c6a1900","b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7":"020000000001018c6d5a1b0a0b34113c0f73cbdb61ad490743dca7b9e3f335999f209c1eb4278f0100000000fdffffff02400d030000000000160014d125023bd52de48ab81d54facbb7b2538c102ec250920400000000001600142f92766f7866248de5f682d7dd2bab088341457e0247304402200b42c87b46bf34815d16998c13b80ba473d5755992fd277a1a4cdedba1cba2af0220028b8f3dd379bcb0521a450bdb45d1f5be28b94e1f2cf20bb65c2b8bbd6581330121024c296513697547826cd2f98280dbcd44965390ce5f40f3e3321a2de83ded6620229b1800","b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94":"020000000001016710a176ee982b948f2fba92d16f3533d3503b806a771769dcb4cede3f4fd47d0000000000feffffff02570400000000000016001457a5c06d1b02bd52b89fd90167bfec31768e8ab6dd191c0300000000160014ecb59e37d4a1b9fa13cf611fe88ee651413568ce0247304402204d460fde321c0a82648af3d21b19acb228bceba8040ff677a5c6985d7fc050b5022066c7f1a692f415d7a159a5a2a3e2cf457e8f824184a87b44618cebb6b28b16de012103a45a5e3b05ab7c340976ad921dfaf018b3720cd86fab014603237ea10de4a250889f1a00","bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9":"02000000000102529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0100000000fdffffff529705cd939fc7ab0d3632530ae7a704a892be6889a5e6f9250eedea6e54334a0200000000fdffffff0186c4a70000000000160014bef230e843338f455977773d701d61feecad0f5d02483045022100eb2be404c770b0f8dbff2506c2924334d025efdec9a055a26d729ea0238b864102201ac7a7e5417028dc3554e4f5c90fcc2366e238225fb7b4d512bfa9bfae2f9f3f01210240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba350248304502210089c43ea1b3274a7b1afa9aa5531c44344116dcfa921186a4736ba52f0e86853602201b330b2daa20e0f393548d20968ad55b32ea646d04ade550e4be7842c3d493310121024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c872851800","c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0":"0200000000010141f2de02db45f99c3618e4bfb51cd3e5ec64db096886cfd8253bdbaf0bba58c72b01000000fdffffff029002040000000000160014482cc4416e2652675d1924f952aa3385be6c247140420f0000000000160014cdde578f988e214b8c857af2caa60c1cf28e7dc702483045022100f51771e279f9f95bf46289d0a60a13ac2165c5724ed5296ec7d5c53e5d237b2c02206b56552ef0b56a23778d876a5af2218f099491e5563d6e762e63758299ef73fb0121024fcb297ca268e5b7132fd523435cd7f2c1ec712e8368f5f3b3f227c68a9b427a902d1800","c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295":"02000000000101a34b263554ad4996c506866afa47e378782cb99d544c1d299e05688c844627440100000000fdffffff028c36010000000000160014cb2c4764c925113234cfd8e031aee29e09b2370f605b0300000000001600149ff30c0926b285e32324c862b1c4d6bc4471e37702473044022036c27642095bf69a46324dd69a9eb3c5f48c6120e64fe39cce09d15527d3136c02207a58e175550c26d44bbc30dc51561bb3cd0400c2efe80db59fdec9a3aae26bcc01210383b014bca7874eedf690c8acfc93e3e3e1b99313dc3633454fd8e515c868de826faa1a00","cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52":"0200000000010249559bf09c3852fb1d592d23093a8880be3c9ea9f2156dd72caa46065f0893330000000000fdffffff358c51e22998661e230319657ef7fdbe7c745ea95957bd8f5227887b1edafc430100000000fdffffff0274850100000000001600140e7afd8a4412d95c2e168621c2d612554eb3d19ba08601000000000016001478fa2398d808a6ce80387e704a58225e2de155ad02473044022009368af6729bb888ba1cbce9a7c80787c15c6d7f1d443b70b800e86021a14d44022030f4d53818b7aed34b390b29accc03f8925bf6ae39502c1a48c1b4fa0bc08425012102c645db0de1449a56f1a38adb5367076b7cb9f36fe3876c0acf5e806c382a53140247304402201a91a39082623db2482525f8cb643c44bb68a33a1f685159b2d1537a780b56eb022008bd74e10597d3568f5bcb3a98b20e223ff9e2ac65dbf7072f7db96f255c308f012103af7486487df942e4c20dad086d96b0ad33f72ad0fa8d0416822e47758746b39f679b1800","d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee":"02000000000101cc3ac5653962a63e971ff7c6ebf70779f146c5525a77baadd36797742d6d43650100000000fdffffff01ce470200000000001600142b7d2fda455b197c6a7e16f0f54da4bc67a664cf040047304402205dc27eb2447a07cffdd1b9d41c1dea2b4e45b7d013e00acb8b165f79025cb121022006c9cdca43be5ee62f77543dd84d8ba05c0fa40d9bf54948fbe18286dec2430a0147304402204ac25cd19c6c3e604d11aeb4beb0ed34632c9974e751dfb5306d3e626fe6bb8e022069a00a721e67077fa8b42cbc5c8e3d7ddec9e0e1dfa88b591bb032d2a76541ff01695221025c78967ffec432318ae59e44c3a4ac7ae1d74db1d8587b862e0779055b35f7ae210386a7718a016752d5178fb48712b8f1a99502ce491991e8c7d453c43c67aa77ea2103e26f7ffad9d6d3c7edaa106b229eeb8b59af12d865ffe3ea763dae95dc5857a353ae91a71a00","d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057":"02000000000102cc3ac5653962a63e971ff7c6ebf70779f146c5525a77baadd36797742d6d43650000000000fdffffff3478939200ea32cb65763d56f945b49f591a8a0cf554e688c4e9090aeceea1850000000000fdffffff02108501000000000016001431bcbd222d0ae1522fe2cada061f2fa81bdfc74604870100000000001976a9147b359a3a5876d3c2d499dcd78584091181229e6288ac0247304402205d1e1f0b69a4fb4ef306d7fe135f1558fe24f1f9801a5d49a6219fe3b83e6020022043b5cd2fde26f62fe446ef8251c110bdff3002e198fb1ecd1f812541458be1c7012103fee46fdcfdabf0a6b242ef391d0a0b2186855f21a26063cccde92d8fc6b979fe0247304402203d2a6146a4c6ca24868931df2f324b41ed9b92a2147b8dccc3692a4aa0c5acee02206ded2bd809b983978cd19ceebb7b7ec4ec95624fe8f538457fe4490832d1d24d012103d8db57b0a0888bf7f7da4961d70e9ea65794ada7c35376a5c93cea0ebfefeb5d6eaa1a00","d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82":"020000000001031ec5794688bf5f80dff5cdde661666ab8cbb2b5b914f9f616532cfe38cb4480401000000171600146d5b6b42476e7b56ff74858ad5773354be8763e6fdffffff46cb528747263a9719aaeb269c03f78f5276313c0f2bb6647d4ee7a4d335918300000000171600140bdc8086150c54076e3f150968206549b56a8276fdffffffea0ef7d769fc5b523db55466dd41f8d3cc243df4bcd1555ee86d7313839d4de60100000017160014c2749b374708095072eaa445903ab1814a2b9e4efdffffff02002067000000000017a914d5c8280a6559c9bab4ebc50ac6fba885ef5b054a8700e1f505000000001600149379c1cb1f9e5293334b433c39c03d370f467a1d02473044022014c21c9e700067ed54b99e0548e6d2b639634fc04ccc7fdd4461311910ca23ec02200c8a0ebb81943009dc271f4ba39fbf3851d63e5d213251962a2bfccecc73d6180121023e68b3754a77dc99c93a2ab24c4bee22d9b783ca38a694f49608b0475d332fa60247304402205778a997e500520be0de51a7c707e1f77ad08e17cb738e8e56174cbe99f74495022026c087ded7e008a3a63deb93898537400bd0788fbbf16d0028a9fd0075a54836012102dbceafa500b943f93007d40dc22ae90a375d94ba29bd7591dc998450b1a858360247304402205b12f6c8ed0462be812e649534bdba7b6a5f12197c9f6dde1c540936e6c01f560220614bd71f02d4d4c8cc88dbf508ab8cb3394f27292db0b2bc181ff7cee503ebcc0121025a5823b569b8da23bc9d90537ac493731bc3172f908c6237c2b761cc827fb247da0a1b00","d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67":"02000000000102b01ae0d3a3a34b48f73c0d0b14debed8059352cef7962c988692986bd6dfe50a0000000000feffffffbf2434265b777bcee606a05bdef28acdd8ca0cd2d03632a49ba7fbb610b9b5180000000000feffffff02dc42130000000000160014911eee03b9976e5fd6f1bce8fa4be1b9433d07db404b4c0000000000220020f3a22dbf7450c4125db98f5ec2ea36bd1c9dd8fe687994a98f370459d502745b02473044022003f036d8ab8c5c992fa4f01cac538d9ade369da5178ca1367d4eeab78b844b9c02207d082c7339133b0f8054377da99d292a03b400424c9db2dac3a93a53b275f48f01210368c3e8ffd98ab4b4c8ec61b3f875845fa5d0c4fb7f6f3b75a082ab1eda1215790247304402205cd416a603fcb62a7fcc02c78814ddb60ce9f8049db5ff8b11cc9e127e453a9a02200ea3b6265db9cb35aeae35fa1191657e2e901cc63550d6305eebacaa8dec37fc0121030f2ce9b4596792eb06752c5b3ef42cc72ad61acb9812b4f6b551dca86aca74848c131b00","d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156":"0100000000010136d1de175112984486c4b71f83b0be17cd8ca87ef2119cf1b265e23daf6964450000000000fdffffff021027000000000000160014aaf850e600893f139b1af7bfc9ee84777a4b2270ec4b00000000000016001493ebec8c80210a1d82c7412226b1665cf848623102483045022100a4ac1a65b8320f23dce6f9f6f671e92d606a571b5dc0dfc54a5cf1d18d3f3ae802200479a5b4352fe953ee07df21faac56be6a49a8ae583bb8a54004cb21c19cba97012102a239fa04fc1dbc55e017b3a9c72f5349f96aaf39a7fbb63148e5a0714697a6e0608f1500","d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3":"02000000000101a3a2d1a5f27a5e8a3a2a48bbe28071f15742f89e8ad1c8e86580c740d858e5750100000000fdffffff02b485020000000000160014250c6af08066897017215dfa79d217048b5601dee09304000000000022002060e574a9c598622248101ccc426053b8ca8424d56bd4f4ff594b8c4a55e1f6730247304402201bb64d16d67b2f4a871aab1356ef2d748608a93882b4dd70e38a3fab6ef552f102205ecf13c839d7a90de296ee75d8b43029195a730a49cb6f62f952087e4dfa9b4701210271cefdadd18ed89a0b6348027ebe5ea6c2406ab8db4bd420e80b86acaa8654189f141b00","d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5":"0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800","dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137":"020000000001015b71ffdecfbd7bf6c6cdc12c44a3beb578a5bad3aee0535aa03f04c34e4a33560000000000feffffff0180f8ff00000000002200203128e44db8a47057e5802a7c33f6114127ce1919864a01eac587957ffd7847910247304402207e7b02eef6952ed3398ce77776df2be2b4ef8084fd431a9d234cdaa45b4b710e0220367a4e683d8ebe8e5c2523be9b35e371c4e0a95fe2f91fdcfc8183a1f62c81bb0121028ef2e4200940b329f6e9fafeea0f0b91fc5baa14be629f90c4460d1b61f06577396c1900","dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c":"020000000001017d23dff709affe0292e1c54968fe59999ab38aca7a56105f5673b0a019ed58790100000000feffffff028096980000000000220020392a484af4656d87203187746cfa12eba4986fc13819042b30b88587ced64b7fd4ce3301000000001600146bc11c4c1c79a06f0778a981903037caad4ed7f70247304402205da8ee1405f03ad943a4d1a95323b1c8eaad9d8364dd685f97d0de3b38869f810220649507c82fb80de316ea42720e309276dee373d3f73626ba6f7e27e27deff6330121022cf26a8a8f1cf65705a9f3889edea9f9577e39798e06d78df7ba0f23810245c061a51a00","e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336":"0200000000010161b73d74dff04648ebbeed7fd5ea4f700221dbf819b2d53503438292cdb571030000000000feffffff014f95980000000000220020cf2ba59021e807102f5ff2a6bdbdff172e30620ff9054a3b3c576847aebe24cf0247304402206053be0c12eecac6b899974f708f64a2aa4a163d53437535a7102b1ef20fcbe0022013d92de946a017714cdef698d808dead778ee62f7183e95e6c3a61b842bd4a6301210285ba6775416f686355229226b22e707990fff303e2001c0cacbaba482bf9ab613a0b1b00","e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135":"02000000000101e98933d883b3a54aab90f73b3ccd1674508da33129f20b509339d152d2fd9ea00000000000fdffffff0220a1070000000000220020106442099ed51039e7ad438e8b0022ae7cc94ce527fa13099031471700272abb98e21600000000001600145db4ac28816705ecd5447c4ab0797815e1369a07024730440220279aaf8ff3bed63a72c8de6ccad8a33d4bd9eac0e0ff8402aa503b420dd4bde4022066d3d755901952b26823de847a3af1e3d7fe40df37af1871bdaba109280cf53f01210240aa29029982d026322d2bbe6db0350df770591acf1f9aa480ac023a4546c8a288a71a00","e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802":"02000000000103f54c9d06153e05829192def700a5281f47b67d1e894caf1e5a95a744102c09b30000000000fdfffffff54c9d06153e05829192def700a5281f47b67d1e894caf1e5a95a744102c09b30100000000fdfffffff733b90182901888d7d39b03b4f9031b57005a8b1b53a6c07d52945c110cbfb70000000000fdffffff024c0b030000000000160014fb4d770f4456d38b5e493c5504e433541aa3c98020a10700000000001600145d6bf0f43f89e01fb76f6af70de8517bf6e383a40247304402204aa2a2b5143cd3dcb538e8e3c7d4dcbe05443944c14e0b2fce73309b1064e393022018c007b74ea00e8852ff1039f3187cf247a297436b0040bdb6df3a403d9cc57301210263fa9bbbfb7fb212bedba0bbe6af9ed90aff45aea0511fc552f3cf146da21f5f0247304402201b2db5e3f150b468a703901b35c6bf19ca6d97d498bf149e2231cf6f4b22cf86022075a8947db53db5d40b55c6e14dbd5a9ccb46200c2a739846e5476e4f66df5f080121027483e3dd39649553ef51b8e09fc498f866d67faa164d45e266c97f94dda8dc9e0247304402207f01ec9439be25ca4c4ee7e4c7a9cc2cb21eddfcaa56e166eb0c3e00243575e8022059760010fd9be1132b9eeccc5c1c410809367ec26e6f0424282433ce51bd99640121030682ca2df861e021334ef8a40f98ac4f3eb05e3c2d94a698cbd9c5c6f52817d29f9b1800","ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21":"020000000001012c5783edec9a110c012a72ed737acde1a97ca9b4cc618384416f61adde5732dc0000000000695c4280018595980000000000160014839075e373684720629a85046859b5903dd22b5e040047304402201bde7c53efca4b7c9fbb30a31f0dd45c0581d542dd079af899217453c0feed4702205a9796300301fab6c5e7ee2f3127dd610cfcd4c8b0c97a27e27b001c97ae58b201483045022100df6e6660127f2cff1091c660a635bb3596097b24436a80e282d6ffbf89370e46022039ca0bce8b7b890d24fd70e246f4d87d75de49b360ff4874b36f2a1c22afba7501475221023f60881fb2837e6b3ef8baaee95cdebe12f775fdbf106e35ae54092afba47f5c21026d33825007f57b2a60af2127fa120ee2a13d9f5391eb43b918b618b8e3b80c5252ae8ab07920","ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2":"020000000001019dbd55d238dd32954f636199e4558fc9483d7d0c1db18ec29d412a50da3b36940000000000fdffffff02840a0300000000001600148b428c50ec0b6f19d51ded15aa22fffa5c227be7400d030000000000160014bc543a42e0b8556a63f5d7b651c8f27287606e5f02473044022064773aecc89daa7279f5f5631c224fd75df5446ba8c32344ae7e24ee98a7117d02201f36dbbac9a5b76d55be27a8b3236391a103a3e61fc2b0efd546c570ff1f6781012103d0671a7e8bdef7d3cde1adf2320fbaa65f4dfd0456dc9ba3159df04f40948c895f701900","f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d":"0200000000010102782afe5f2893a26f3f63b6b175990a5b53801d37f90e13834511e3456696e80000000000fdffffff028083010000000000160014751a431cd38ec2746a042008dd40c6ff7ab1b14004870100000000001976a91437b99663ca47bdd5e3b0be4fd1cc7c0faa79f58088ac02473044022000808b6ad3e3a3560478304038f5edac7a82066ab0597508c9852a7dbd576f66022035ecec51ad06f4760446b4a7ccdc574adaa7aa44ccfb820c95490ddd88d005320121026cee9d63a491119f300a505a640b07010cec8682086b447c5330148e5f3e7a7feaf71800","fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa":"02000000000101abbfac32862eaba435a0a5ad260a61e5fda1cb594c33a5ea523786f33ef6d7420100000000d279618001c9959800000000001600145297f0c582b75cd671bfc1123bb4fe07cfed501a040047304402202b533376d653e243ce38893454957413692dc0ad326aba704dba7524ff8cffdc022011d82213cfbb2677d445a54ad10bd9dabbc7c67baa7f736232864119695e6e7e01463043021f6a6e269cb49db20fe9cf787c7b038d4868f228e527b16b8a6efabd20664194022027cb81608e5b8c3af8ead51d2404c29482c639321e7f8705113273ff6983779801475221024b56076bcfb784b0f58edf280290ab20daaf9c079e40642b158d5d3fad77faa321025f074d4778e25d5f10e8d4b20715edb1f5fdbbcf314f47111026719cfec7ca6c52ae736df320","feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877":"02000000000102abbfac32862eaba435a0a5ad260a61e5fda1cb594c33a5ea523786f33ef6d7420000000000feffffff551df8e2dfb3dbf8bb7326d6d1c647adcc51281133fd88978b25ee7111a751720000000000feffffff0264280a0000000000160014f7c3839fbd881cafc37b8b67dbc4fed63e60565e40420f0000000000220020c0accfb484c7ae9940d7efc0c7ba921a1e69cba406303b384c37de34866788ad0247304402201ff5533f8f6ed979b93e8e349c934c46af6a70d35bea800782c999a9337ea1aa02201ad823951ecd1b6a203a26d396bf1bce13151e86800a3a4026236cc1ebd63c2c012103d488a84c6c19a5acf3b36a45852ee4132cfedb1278fd55368344732d6747dedf0247304402201185cb9ffab376d55ed9ea64d1715da6eeb5e20348b67b8b35ed02340aa8e71602202f8c48e228236e27ce5faeb09aba2bb65195f7997ba603f137a068f76225dd62012103c115d4fa6d752e4629a05d0073fe2ed2cf88b46e1b680a6813e96d67f340cb32f40a1b00"},"tx_fees":{},"txi":{"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be":{"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw":[["bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9:0",10994822]],"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp":[["4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755:0",10494822]]},"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0":{"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj":[["4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e:1",1711000]]},"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687":{"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658":[["9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd:1",66445264]]},"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04":{"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4":[["9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e:0",3211400]]},"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f":{"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf":[["62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38:0",100000]]},"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d":{"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7":[["b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7:1",299600]],"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr":[["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802:1",500000]]},"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e":{"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc":[["0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d:0",16773292]]},"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab":{"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0":[["600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54:1",999600]],"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu":[["6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b:0",9672100]]},"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15":{"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9":[["a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9:1",50173100]]},"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3":{"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7":[["6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a:1",250000],["366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f:1",250000]]},"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e":{"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj":[["1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04:1",2211200]]},"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc":{"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn":[["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4:0",100000]]},"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b":{"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0":[["dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c:1",20172500]]},"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54":{"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y":[["e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135:1",1499800]]},"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38":{"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t":[["2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d:0",299300]]},"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b":{"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3":[["5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b:1",19672300]]},"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc":{"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw":[["d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67:0",1262300]]},"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506":{"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4":[["d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee:0",149454]]},"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4":{"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6":[["1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04:0",1000000]]},"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3":{"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3":[["feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877:0",665700]]},"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05":{"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4":[["442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3:0",200000]]},"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d":{"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts":[["43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15:1",40172900]]},"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067":{"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq":[["cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52:1",100000]],"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl":[["ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2:0",199300]],"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s":[["38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3:0",16740777]],"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev":[["d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156:0",10000]],"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw":[["a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867:1",32890528]],"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r":[["48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a:0",99890]],"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl":[["50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc:0",99560]],"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya":[["0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0:1",1210800]],"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p":[["40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1:0",499862]],"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7":[["ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2:1",200000]],"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s":[["b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2:0",499862]],"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq":[["62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38:1",199159]],"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c":[["cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52:0",99700]],"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr":[["69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1:0",10000]],"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta":[["f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d:0",99200]],"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33":[["ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6:1",870]],"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7":[["72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390:0",99780]],"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g":[["674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4:1",100000]]},"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a":{"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0":[["d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82:1",100000000]]},"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff":{"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv":[["955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514:1",90133586]]},"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c":{"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx":[["c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0:1",1000000]]},"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366":{"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd":[["fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa:0",9999817]]},"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5":{"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z":[["d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5:1",13999800]]},"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d":{"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5":[["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4:1",399800]],"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw":[["7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4:2",500000]]},"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9":{"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt":[["c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295:1",220000]]},"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514":{"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2":[["81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a:0",99998900]]},"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd":{"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6":[["ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271:1",83222632]]},"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2":{"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp":[["ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21:0",9999749]]},"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e":{"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn":[["09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be:0",4712097]],"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg":[["934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5:0",3499600]]},"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9":{"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw":[["b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94:1",52173277]]},"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867":{"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x":[["0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687:1",49667896]]},"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6":{"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t":[["01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61:1",1000]]},"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271":{"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp":[["0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca:1",100000000]]},"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5":{"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg":[["8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c:2",500000]]},"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7":{"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28":[["8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c:1",499800]]},"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94":{"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0":[["7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067:0",52273088]]},"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295":{"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r":[["442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3:1",299700]]},"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52":{"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc":[["43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35:1",100000]],"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd":[["3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549:0",100000]]},"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057":{"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43":[["85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834:0",100000]],"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p":[["65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc:0",100000]]},"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67":{"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9":[["0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0:0",4273050]],"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9":[["18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf:0",1989558]]},"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3":{"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f":[["75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3:1",465500]]},"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5":{"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0":[["133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62:2",25000000]]},"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137":{"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h":[["56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b:0",16775418]]},"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c":{"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh":[["7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d:1",30172700]]},"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336":{"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp":[["0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761:0",9999817]]},"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135":{"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw":[["a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9:0",2000000]]},"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802":{"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv":[["b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5:1",300000]],"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c":[["b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7:0",200000]],"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4":[["b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5:0",199800]]},"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2":{"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w":[["94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d:0",399500]]},"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d":{"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe":[["e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802:0",199500]]},"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877":{"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn":[["42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab:0",671400]],"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu":[["7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55:0",994558]]}},"txo":{"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61":{"tb1q3dvf0y9tmf24k4y5d37ay0vacyaq5qva7lg50t":[[1,1000,false]]},"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761":{"tb1qsmk2jc6fzr0e9xkf7w9l3ha8s0txha3vruffrp":[[0,9999817,false]]},"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be":{"tb1qptq7mkutq0m6an0npf8t89dxvtecqp08uqphcn":[[0,4712097,false]]},"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0":{"tb1qk7u2mcu02v7fgvls9ttuwq49a6e5kae5kxkts9":[[0,4273050,false]]},"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d":{"tb1qkahwe0pkcnnm9fzwy3f5spwd9vv3cvdzk5dkkc":[[0,16773292,false]]},"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0":{"tb1qg26z824j42qrl9tssjpjkyp4n042y35sre6yya":[[1,1210800,false]]},"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687":{"tb1qsrgn2zg9lgyeva68tgjqv0urs830vcnsmajg0x":[[1,49667896,false]]},"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca":{"tb1qwg8fgt97d7wm3jkzxmkznwe7ngxy08l89v0hxp":[[1,100000000,false]]},"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62":{"tb1q2hr4vf8jkga66m82gg9zmxwszdjuw5450zclv0":[[2,25000000,false]]},"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf":{"tb1qm0z2hh76fngnp3zl3yglvlm6nm98qz4exupta9":[[0,1989558,false]]},"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801":{"tb1qsyhawdg9zj2cepa0zg096rna2nxg4zj0c0fnvq":[[0,99058,false]]},"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04":{"tb1q4eeeqvrylpkshxwa3whfza39vzyv3yc0flv9rj":[[1,2211200,false]],"tb1qc50swmqxgw3e9j890t8rp90397rg3j0djy9rz6":[[0,1000000,false]]},"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a":{"tb1q9jtcype5swm4reyz4sktvq609shw88fwzjz9jg":[[0,100000,false]]},"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d":{"tb1q4arrqquh2ptjvak5eg5ld7mrt9ncq7lae7fw7t":[[0,299300,false]]},"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549":{"tb1qftpp8e7t3mk7c48sw4mgwqn24yhuzl5t9u4fzd":[[0,100000,false]]},"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f":{"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7":[[1,250000,false]]},"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3":{"tb1q4qhgk2r4ngpnk5j0rq28f2cyhlguje8x92g99s":[[0,16740777,false]]},"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1":{"tb1qgga8dl6z86cajdgtrmmdwvq9f2695e6epp064p":[[0,499862,false]]},"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab":{"tb1q92pxe3a3zyddfz5k74csaqt2vzc5sl37fgm5wn":[[0,671400,false]]},"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15":{"tb1q97kf6e7qfudh2zyy0vmp3cevtw3jteqa0qupts":[[1,40172900,false]]},"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35":{"tb1qa4gwte9kr0tsndl5q69k6q3yte5uh7senrm7fc":[[1,100000,false]]},"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3":{"tb1q309xc56t5r928v093pu3h4x99ffa5xmwcgav8r":[[1,299700,false]],"tb1q49afhhhsg8fqkpjfdgelnvyq3fnaglzw74kda4":[[0,200000,false]]},"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6":{"tb1q8564fhyum66n239wt0gp8m0khlqgwgac8ft2r0":[[0,5000,false]]},"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a":{"tb1q70z22hvlhhjn69xpv2jwkkxprf0pvzh2z5p24r":[[0,99890,false]]},"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e":{"tb1q0phzvy7039yyw93fa5te3p4ns9ftmv7xvv0fgj":[[1,1711000,false]]},"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e":{"tb1qhksthm48t4eqrzup3gzzlqnf433z8aq5uj03jr":[[1,100000,false]]},"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755":{"tb1qq6zuqfwc97d3mqy46dva4vn8jvlkck63c3y0mp":[[0,10494822,false]]},"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc":{"tb1qcs8sn834hc65nv0lypxf4zzh8yrp0vqw293vdl":[[0,99560,false]]},"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b":{"tb1q49g7md82fy3yrhpf6r4mdnyht3hut2zhahen7h":[[0,16775418,false]]},"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b":{"tb1q07efauuddxdf6hpfceqvpcwef5wpg8ja29evz3":[[1,19672300,false]]},"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54":{"tb1q3p7gwqhj2n27gny6zuxpf3ajqrqaqnfkl57vz0":[[1,999600,false]]},"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38":{"tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf":[[0,100000,false]],"tb1qm3qwl94e7xcu2nxe8z0d3w2x0s0xwrpahm6ceq":[[1,199159,false]]},"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc":{"tb1qyf62fc39qsmnxxv873meuu9au6p3cag9slgh9p":[[0,100000,false]]},"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4":{"tb1qz9z4uw5tnh0yjpz4a4pfhv0wrpegfyv9yl2n7g":[[1,100000,false]]},"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1":{"tb1qrkgr9yme0zedgemjpvrt852rq2qfz27s832yhr":[[0,10000,false]]},"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b":{"tb1qhezs0203uw8wyagjpjs5yv57xdmsta077qkazu":[[0,9672100,false]]},"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc":{"tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh":[[1,762100,false]]},"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a":{"tb1qdekelvx2uh3dfwy9rxgmsu4dra0flkkf80t5z7":[[1,250000,false]]},"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506":{"tb1qa6dgfxczcjshyhv6d4ck0qvs3mgdjd2gpdqqzj":[[1,100000,false]],"tb1qwu3708q32l7wdcvfhf9vfhgazp8yzggf5x4y72":[[0,49300,false]]},"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55":{"tb1qmy8uqjkh2d2dcgnz6yyrtjk05n5y4ey8qzayyu":[[0,994558,false]]},"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390":{"tb1qz2xgj9eahs855rudhd4xreatp99xp3jx5mjmh7":[[0,99780,false]]},"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4":{"tb1ql2yks7mu0u95hpjgagly0uxlh98fs9qg00hkr5":[[1,399800,false]],"tb1qltq9ex98gwm2aj5wnn4me7qnzrgdnp2hwq7pwn":[[0,100000,false]],"tb1qrmex0u0vkefcmxr6fc2sxuvdxh67p99nsqnklw":[[2,500000,false]]},"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3":{"tb1q767gch8ucagh23h40frfm8x6jmc37qvxpn8x2f":[[1,465500,false]]},"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05":{"tb1qgg2avhyk30s8a0n72t8sm3cggdmqgdutdvwfa8":[[0,99800,false]]},"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d":{"tb1quc085vmkgkpdr5wpqvgt6dyw35s5hqrncml8sh":[[1,30172700,false]]},"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067":{"tb1qwzxfucd24m4j4y6nzasnucrx2dty4ht2h0lud0":[[0,52273088,false]]},"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a":{"tb1q8k9sp22vjun7hf0sfvs2n8mfwt8xl43d68xml2":[[0,99998900,false]]},"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834":{"tb1qayg9tz462wythfdxw6gxpapwdp5y8ugth7fx43":[[0,100000,false]]},"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff":{"tb1q04m5vxgzsctgn8kgyfxcen3pqxdr2yx53vzwzl":[[1,73356218,false]]},"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c":{"tb1q9mgamdnm0jch3e73ykvlgymwg5nhs76t8jv4yg":[[2,500000,false]],"tb1qczu7px50v092ztuhe7vxwcjs9p8mukg0gn9y28":[[1,499800,false]]},"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5":{"tb1quw4g923ww4zs042cts9kmvrvcr95jfahqasfrg":[[0,3499600,false]]},"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d":{"tb1q8afxv7tzczj99lwf4et6le4k2u0tytqgt6g44w":[[0,399500,false]]},"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9":{"tb1qcmmu23wur97duygz524t07s40gdxzgc4kfpkp5":[[1,119700,false]]},"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514":{"tb1qw9jdeld07zf53jw85vh7pnv4xdep523v96p9gv":[[1,90133586,false]]},"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd":{"tb1q955va7ngp2zzzrfwmn29575v6ksqfzrvvfd658":[[1,66445264,false]]},"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e":{"tb1qwqxjpfytaq08qteus5dhwf92u5kzfzyv45kyd4":[[0,3211400,false]]},"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9":{"tb1qav362fjlwyvuraeqz5gmf0hrrvv9hp9jgv3ap9":[[1,50173100,false]],"tb1qy6uuespwqm9m9wdjvmwr07l9fvn0ge93mzskzw":[[0,2000000,false]]},"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867":{"tb1q6lzmxd6hr5y2utp5y5knmh8kefanet5pvgkphw":[[1,32890528,false]]},"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6":{"tb1qyeg0h0fy8vw3mq0alvdffe0ax8dltalmjzse33":[[1,870,false]]},"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271":{"tb1qql6g008ymlcfmrkwg8lfl7tsgays6s427pjlt6":[[1,83222632,false]]},"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5":{"tb1q0tkgjg5f3wnquswmtpah2fsmxp0vl9rarvgluv":[[1,300000,false]],"tb1q730gzvu52y6t07465flt6ae8eny2mnsh7drhw4":[[0,199800,false]]},"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2":{"tb1qhzay07kvxkuerlel4e6dps33dtr3yxmnf34v9s":[[0,499862,false]]},"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7":{"tb1q6yjsyw749hjg4wqa2navhdaj2wxpqtkztzrh8c":[[0,200000,false]],"tb1q97f8vmmcvcjgme0kstta62atpzp5z3t7z7vsa7":[[1,299600,false]]},"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94":{"tb1q27juqmgmq2749wylmyqk00lvx9mgaz4k5nfnud":[[0,1111,false]],"tb1qaj6eud755xul5y70vy073rhx29qn26xw65nanw":[[1,52173277,false]]},"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9":{"tb1qhmerp6zrxw852kthwu7hq8tplmk26r6aklvcgw":[[0,10994822,false]]},"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0":{"tb1qeh090ruc3cs5hry90tev4fsvrnegulw8xssdzx":[[1,1000000,false]]},"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295":{"tb1qevkywexfy5gnydx0mrsrrthzncymydc0zz4rqx":[[0,79500,false]],"tb1qnlesczfxk2z7xgeyep3tr3xkh3z8rcmh4j95gt":[[1,220000,false]]},"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52":{"tb1q0raz8xxcpznvaqpc0ecy5kpztck7z4ddkzr0qq":[[1,100000,false]],"tb1qpea0mzjyztv4ctskscsu94sj248t85vmggsl6c":[[0,99700,false]]},"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee":{"tb1q9d7jlkj9tvvhc6n7zmc02ndyh3n6vex0d8fts4":[[0,149454,false]]},"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057":{"tb1qxx7t6g3dpts4ytlzetdqv8e04qdal36xg9d7zc":[[0,99600,false]]},"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82":{"tb1qjduurjclneffxv6tgv7rnspaxu85v7saf9mfj0":[[1,100000000,false]]},"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67":{"tb1qjy0wuqaejah9l4h3hn505jlph9pn6p7mzjasnw":[[0,1262300,false]]},"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156":{"tb1q4tu9pesq3yl38xc677lunm5ywaaykgnswxc0ev":[[0,10000,false]]},"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3":{"tb1qy5xx4uyqv6yhq9eptha8n5shqj94vqw7euftmk":[[0,165300,false]]},"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5":{"tb1qwzhmm9ajms63h5t87u2w999jl5akptkl4e5d7z":[[1,13999800,false]]},"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c":{"tb1qd0q3cnqu0xsx7pmc4xqeqvphe2k5a4lhjs05h0":[[1,20172500,false]]},"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135":{"tb1qtk62c2ypvuz7e42y039tq7tczhsndxs84eqj8y":[[1,1499800,false]]},"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802":{"tb1qldxhwr6y2mfckhjf832sfepn2sd28jvqykgyfe":[[0,199500,false]],"tb1qt44lpapl38spldm0dtmsm6z300mw8qayy659zr":[[1,500000,false]]},"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21":{"tb1qswg8tcmndprjqc56s5zxskd4jq7ay267phaefp":[[0,9999749,false]]},"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2":{"tb1q3dpgc58vpdh3n4gaa5265ghllfwzy7l8v786fl":[[0,199300,false]],"tb1qh32r5shqhp2k5cl467m9rj8jw2rkqmjl9g0tn7":[[1,200000,false]]},"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d":{"tb1qw5dyx8xn3mp8g6syyqyd6sxxlaatrv2qvszwta":[[0,99200,false]]},"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa":{"tb1q22tlp3vzkawdvudlcyfrhd87ql8765q600hftd":[[0,9999817,false]]},"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877":{"tb1q7lpc88aa3qw2lsmm3dnah3876clxq4j7apzgf3":[[0,665700,false]]}},"use_encryption":true,"verified_tx3":{"01da15654f03aac8df6b704045b4ec7b680198b61ab4c82e74419ea430bdbd61":[1413374,1536860419,1141,"000000007d7349e92f81e9e8ffe1a46eecdd3a88a4a1228de227e14faeef69a1"],"0371b5cd9282430335d5b219f8db2102704fead57fedbeeb4846f0df743db761":[1772346,1592576908,59,"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e"],"09c69cd541e3a0359718935e2eb16c71c26769632c16fe9d51965597f2a6c4be":[1609187,1574268754,55,"0000000000009e7b077d1b0e51098bf744bbe94ad1c6593e45c34efd97bc425a"],"0ae5dfd66b989286982c96f7ce529305d8bede140b0d3cf7484ba3a3d3e01ab0":[1772375,1592591253,21,"0000000000000167e02b208939cf45855de05b8c3ac1d355b240798be1386ba1"],"0b7259148cee3f8678372993f4642eb454a414c507879080fb9f19625680433d":[1666105,1581963801,275,"0000000000136445a3ea6ebb32ab280d744eaaff37da4ddc6ceb80496f422438"],"0c2f5981981a6cb69d7b729feceb55be7962b16dc41e8aaf64e5203f7cb604d0":[1665687,1581525379,234,"00000000001b6c273648dabe0b50b9d5df20a9377a4e685df7c3fef5889fe89e"],"0c86a9161434d6c352331db5d6a6e2a2bd67097299b6b8afc66dbb02e421a687":[1612005,1575945038,152,"00000000d6f0ea275bd4e4c678c76e3b3089a2426f85fc433c63bd41b7f56cd8"],"0fab00a789446aacc37058a3ec02285df70d39d93211c8a0df146c3a92d8b7ca":[1609182,1574265630,31,"00000000000001eda6b8ae2f48779ebef52e72fe13431a404b9f7bcb60f6b670"],"133167e264d5279a8d35d3db2d64c5846cc5fd10945d50532f66c8f0c2baec62":[1607022,1573058015,15,"00000000000001dcecfa9ea4a8637479884a5ba7b3aef561d1634376877b1d49"],"18b5b910b6fba79ba43236d0d20ccad8cd8af2de5ba006e6ce7b775b263424bf":[1772648,1592710653,68,"00000000000000fd9f5f67fb99e41d09f272b03798ef18c4db8b6db49430a313"],"19a666acc1bb5656e49a1d99087c5a0c2a135c40f1a58e5306f2d85f46786801":[1772373,1592590852,11,"00000000000001265ba5ac89f4321a5abd421f80dcf57b7f665a4593d3bd7f99"],"1e8d4d18fa810e63f06aeb3ad67a445a2766966646b3530c177be2225f9b0b04":[1638861,1578439053,39,"0000000000000172c2b2a077ccda3b5c7e63e0574a557d3216630e6488b6fd2f"],"22c3d77c55fcdd77f2aaa22fe106eb857874a52be9358f8786f9eda11662df5f":[1692449,1585622204,20,"00000000000005a7bab64ba095994085cf29576d433277b27b07ae8e91dd81f7"],"26b1fb057113f6ce39a20f5baa493015b152cc1c0af312b3ee8950e9a4bbf47a":[1774145,1593267834,30,"00000000000000427fe2218fdcd0bb7e6b21be6c0a729a42ca5aaa61ca59c163"],"2bc0593afd74ea2eace18c3901a378c752067b1b94030cb60a612438471ec59d":[1665679,1581516561,96,"000000001b9a933341a6414ba96c9a2bde2778e20b2ef3a8139953a6709d869a"],"3393085f0646aa2cd76d15f2a99e3cbe80883a09232d591dfb52389cf09b5549":[1606858,1572981006,74,"00000000a58936b6caab26c619fe393ff581e4751942c9e32060ff99f3e95846"],"366f702feacb3e63cdac996207b2e5e3b5a0c57c798eb774505b72aee1086e3f":[1746833,1590770265,47,"000000009124abefd02c781857eaa680da3232c076e546e787cf568daec28751"],"38b8f1089f54221fb4bf26e3431e64eb55d199434f37c48587fa04cf32dd95b3":[1666768,1582726987,20,"0000000000172e1a5d7dbb64ae5d887aa9f9a517e385b91964da899c1ac3df10"],"40b05ec3c24a1ae6f9a160015f739b1637affb1cd97fbbd675317b1cfb9effe1":[1665686,1581524171,191,"000000000000090571d7b168808d0999972f4e839709e18a7a60273242f15185"],"4133b7570dd1424ac0f05fba67d7af95eae19de82ff367007ac33742f13d1d8e":[1666106,1581965019,272,"0000000000009176617a2d62fcab4ae45ed25bc745f36a73cf554637e50d73b3"],"42d7f63ef3863752eaa5334c59cba1fde5610a26ada5a035a4ab2e8632acbfab":[1772251,1592534504,34,"000000000000012e0e87b7c6185f0b1123c9ef0225c1602efe9b7615d4b76cb6"],"43d4979f5c10a9298b6ada4aebb42026c19894557290fbe4ff418193d906fb15":[1746271,1590159393,57,"000000000000048c1958cb2946020e4c90998be2b29e3f045a2e421e78732b3a"],"43fcda1e7b8827528fbd5759a95e747cbefdf77e651903231e669829e2518c35":[1607959,1573496148,4,"000000000000017945c56111ba49057d5373e9a2e4c893b302a60134763dbd2c"],"442746848c68059e291d4c549db92c7878e347fa6a8606c59649ad5435264ba3":[1747567,1591210595,5,"0000000000000324e02615bd6c65e356d64d4d0d1155175eed5b89d86d6de064"],"48a4cd850234b2316395959c343a3cbfd57e706e256399ac39927330f03268d6":[1747720,1591293898,64,"00000000fb526ae2018b173272874512c78add000cf067affbbccbc66dfae71b"],"48f515a30e7c50b3dfe2683df907fc787e5508da94a92cd26765efd153d6529a":[1666551,1582490047,393,"000000000000250d0f7d2e82e138fe0e365a7611ecccabea6cb23e66154e9622"],"4a2421f6bea8b846afd5935b765e897c845ecd2415087275095df3b52b13d08e":[1665679,1581516561,216,"000000001b9a933341a6414ba96c9a2bde2778e20b2ef3a8139953a6709d869a"],"4c5da984d4ee5255b717cec3e06875caeb949bb387db3bb674090de39b3b6c2e":[1747720,1591293898,62,"00000000fb526ae2018b173272874512c78add000cf067affbbccbc66dfae71b"],"4d2a61de0bbab8ba1e44faaba4348ae03429df973f2d188d94cab6bc4b479755":[1607028,1573062661,84,"000000000b69680b3a72b998dfbe15b9f2e4d1d9ec55514c793df17ea52757ea"],"50030d863ec06dd0307a8a9c31e0e2b81194b79cfc7a9d19a72785509fd17dcc":[1721735,1587735242,78,"000000007226bf7af17b4d4ec148796fd3f958498a1af61170450fa5dda5ada8"],"56334a4ec3043fa05a53e0aed3baa578b5bea3442cc1cdc6f67bbdcfdeff715b":[1612072,1575992282,151,"00000000001638164f3c334289bad17de9a9cb79dcc004b4897b64a69a5372d6"],"5fee0fee9b54827bd4ff3992eb78fd5c34c65ad55336a2a1d1a4e43e2a25514b":[1746825,1590760652,58,"0000000039adfa26118ca702d16bd9ad54ebc22a80856778b5909673647ce9db"],"600dfcb7d12159be0ec141cf0a1df10f4d9d1b14e163d664993598038e940b54":[1747720,1591293898,63,"00000000fb526ae2018b173272874512c78add000cf067affbbccbc66dfae71b"],"62277a1c63c563c25934c96870343ccf3e4113b7c00e80e9063835c67f0a3d38":[1665815,1581628321,458,"000000000004437da930fc00f9576e5762d92036c1b682d3d9b6f1695e9038b3"],"65436d2d749767d3adba775a52c546f17907f7ebc6f71f973ea6623965c53acc":[1746833,1590770265,48,"000000009124abefd02c781857eaa680da3232c076e546e787cf568daec28751"],"674c8d073ccebeecab3f3d22da8664a08739db7257ad676e1f0ca615a39041b4":[1638866,1578441818,95,"00000000ae9551c6b0e4ba47dd13695c9335e5d02b4f2e382220bfb59a0322d3"],"69073acbbd6bf7f4ab05324d4dea7eae9a1d11d88ae3204295341ea00af757f1":[1413150,1536757748,470,"00000000000000214096fdc98df2896f0305325d07aa2bb21f3a86bddfd49681"],"6bdebc7c499ffad6aa26ca764b289b4c7817fd9121c01149584dce1ff168fe1b":[1772251,1592534504,33,"000000000000012e0e87b7c6185f0b1123c9ef0225c1602efe9b7615d4b76cb6"],"6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc":[1774910,1593691025,44,"0000000000000070f822bcfbcc2e01a3972e82d8c040b0df210dc29fe29de264"],"6fdd5af3fdfc9702f4db0c381f47550b00aaeb92aab45aa3af8e733cd084144a":[1746834,1590771466,46,"00000000d4cee21f4cc91d30467bf8b00415e2f6965b26305546cee9e575f9a4"],"7144261bed83ae5b65b366e4ce88c24ca310aa265c2781e9949584f64baae506":[1747567,1591210595,8,"0000000000000324e02615bd6c65e356d64d4d0d1155175eed5b89d86d6de064"],"7251a71171ee258b9788fd33112851ccad47c6d1d62673bbf8dbb3dfe2f81d55":[1772374,1592590926,6,"000000000000016ae199e46406b0de734fa90af98c0006399ee1f42cd907cec7"],"72529c8f6033d5b9fa64c1b3e65af7b978985f4a8bd117e10a29ea0d68318390":[1692476,1585625983,27,"000000000000048eb935349ddde604b91cb4b6d441900ebed6c2e85594a57b79"],"7599bb2046d3af0ea376729c54a8fdca52cff2a14f3cbf55b82d7bc04ef928f4":[1663206,1579613987,27,"00000000000001402754827057f3ca9c064191971d1e7529cce4c525b603f9d6"],"75e558d840c78065e8c8d18a9ef84257f17180e2bb482a3a8a5e7af2a5d1a2a3":[1774146,1593269035,11,"00000000cd1509e282e3894d8318236bbd1079c789ef7f36d4bb0540a43e5ed3"],"781ecb610282008c3bd3ba23ba034e6d7037f77a76cdae1fa321f8c78dbefe05":[1774902,1593687024,59,"00000000000000eb1417d08ad9fea42729f18b05161630ee35179a5acebb2b05"],"7958ed19a0b073565f10567aca8ab39a9959fe6849c5e19202feaf09f7df237d":[1746274,1590160929,76,"0000000068f5999a101d92927dbe76ce2e56f7c9b62db2c6043ce25090f069df"],"7dd44f3fdeceb4dc6917776a803b50d333356fd192ba2f8f942b98ee76a11067":[1744777,1589559214,2,"00000000000009ed8402e1a75a94863b07bd68febf8628226c1e237dd67d028c"],"81c97854c7b6ad837776638950fcb51c337c8a1c8fe92cb0b802dcf787c9230a":[1772346,1592576908,25,"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e"],"85a1eeec0a09e9c488e654f50c8a1a599fb445f9563d7665cb32ea0092937834":[1747541,1591206287,12,"000000000000058bc2dece8e8009648f324eb2ba8819b30df1500c1c4cb718fe"],"866ebfebcfe581f6abaf7bd92409282e78ef0ac0ad7442a2c1e5f77df13b6dff":[1772350,1592580676,38,"0000000000000152f21c3d41160026c6cd07b5236f2de66fd54a5df857345edc"],"8f27b41e9c209f9935f3e3b9a7dc430749ad61dbcb730f3c11340b0a1b5a6d8c":[1584541,1572497343,45,"0000000000000123f0bbe9f0c6dc639a68afa054c3cec5bdc440fbf1dc60deea"],"901856f632b06573e2384972c38a2b87d7f058595f77cd2fcca47893bb6dc366":[1772347,1592578128,55,"000000000000573b617c6814f0b063a24d87fcafd9c081fbe845b17a5a14fb7b"],"934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5":[1607022,1573058015,17,"00000000000001dcecfa9ea4a8637479884a5ba7b3aef561d1634376877b1d49"],"94363bda502a419dc28eb11d0c7d3d48c98f55e49961634f9532dd38d255bd9d":[1665679,1581516561,212,"000000001b9a933341a6414ba96c9a2bde2778e20b2ef3a8139953a6709d869a"],"94f858cd16722ec5875ea2d6fe357e413acdd235a31e5a4536818579fbfe67d9":[1747721,1591295101,21,"00000000acaeb070125b32d6ee0b247da309103d8c6895ca3b9db110f9570393"],"955a7ab8b514ed6f419b66f7de88aba52e7ab727ce272519d904ac9cae6e4514":[1772348,1592579365,68,"00000000000043e46d2c439cc1381091d2a899a7e73cef342fcd7a8ca30510e5"],"9df4ae094d99d1a471697dc4ceeec6cd8bbbf5f10f1b1420868edc82736412dd":[1612004,1575943833,107,"00000000000002071db8bd4eee3dd79446fdc25fe4494bd149d2dbab8f8fc351"],"9e0789481c2f5b2fda96f0af534af3caa5cb5ddc2bbf27aea72c80154d9272d2":[1772347,1592578128,96,"000000000000573b617c6814f0b063a24d87fcafd9c081fbe845b17a5a14fb7b"],"9e68e76048cbb148d4c027dc8fbee785d9ab30d60ada5910e44dd15f1c40543e":[1609312,1574367249,46,"000000000029a561c0e454997df161efaf13a84f337049ff4795b66c22c9e887"],"a09efdd252d13993500bf22931a38d507416cd3c3bf790ab4aa5b383d83389e9":[1744791,1589564965,32,"00000000000004b1edb3a19b30988ee533e48ede0d14d58e842eb797aca9edf7"],"a18730be0154907abac05341931d20e667925509878211d4dd69f883df354867":[1612009,1575947371,78,"000000000000009804f234e6cb02c39ada4c01bdc9c2072cab789146c518e9b3"],"ac1e1fe0a56fbd44324cf9ab60102f4695f22ad3abc86b8b407ea3da4aec20e6":[1612788,1576617716,233,"000000009e157af1f4140b003477c9e8a9cd43b93b0798973125dc35c526a2a6"],"ae689a52e82b3cdd91f24805420696e6a6a916905523ebc78b0679a848c10271":[1609187,1574268754,157,"0000000000009e7b077d1b0e51098bf744bbe94ad1c6593e45c34efd97bc425a"],"b3092c1044a7955a1eaf4c891e7db6471f28a500f7de929182053e15069d4cf5":[1612648,1576479029,104,"0000000007d38aeacc48c15a3ec552ecbf5c77e967fcab05a5f4f86600df6df7"],"b41f54b4ab76ccabaa3050c9fdc9418d328cfe8f7646fd642efec4af7afdbfe2":[1665693,1581531211,147,"00000000451769f59ad32d65ca059982480f3071e9b30557ad380022c7efb0eb"],"b7bf0c115c94527dc0a6531b8b5a00571b03f9b4039bd3d78818908201b933f7":[1612648,1576479029,105,"0000000007d38aeacc48c15a3ec552ecbf5c77e967fcab05a5f4f86600df6df7"],"b9dd353d4073b43467d727c02aacc4597742180418737bf359fbc93fc2532d94":[1744777,1589559214,3,"00000000000009ed8402e1a75a94863b07bd68febf8628226c1e237dd67d028c"],"bf5f0f66677796aee47e0289374601892b15632aa64357928edf15e75f8024c9":[1607028,1573062661,119,"000000000b69680b3a72b998dfbe15b9f2e4d1d9ec55514c793df17ea52757ea"],"c24794971bcc581af23b4b8aca2376c5814fe050363cd307748167f92ebcdda0":[1584540,1572496183,11,"00000000000000d1cd960abe8520e3fd13e54f77022dc602dbd7b9797774f0ad"],"c2512f8d6ebffc161cd84dc666fcf7414eaa7f0aef89bb3aa4c47c8210947295":[1747569,1591210894,8,"000000000000031396814946ea32d808d10a94fb17034f1b5f66cb9d8aaee2b6"],"cbcb29b909c167a7b176460d038c93f23d625a6c4e4e46e16dc0d1bed63a8f52":[1612648,1576479029,103,"0000000007d38aeacc48c15a3ec552ecbf5c77e967fcab05a5f4f86600df6df7"],"d1d85883965fd7ee36df6434412b58cf137da7bc1e9f3675eda92d1f951ea3ee":[1746834,1590771466,64,"00000000d4cee21f4cc91d30467bf8b00415e2f6965b26305546cee9e575f9a4"],"d35f97f4a8101fed76f7cbf0a901a123629b06d85af312b769886fe21e429057":[1747567,1591210595,6,"0000000000000324e02615bd6c65e356d64d4d0d1155175eed5b89d86d6de064"],"d38baaf0cf8eb9c5700015c71e969cdf656482e836b540f0ac08771160474a82":[1772251,1592534504,38,"000000000000012e0e87b7c6185f0b1123c9ef0225c1602efe9b7615d4b76cb6"],"d42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67":[1774477,1593456977,34,"0000000000016871a9502945205abed1fd579c8b5aaf0f0523bf658653ae64ec"],"d587bc1856e39796743239e4f5410b67266693dc3c59220afca62452ebcdb156":[1413150,1536757748,460,"00000000000000214096fdc98df2896f0305325d07aa2bb21f3a86bddfd49681"],"d7f07d033e67702361a97bd07688d7717e5255a8095bd8d47ba67cbea58a02d3":[1774752,1593613036,61,"000000000003bb43eb7b8b15e70eb6233e149b612514d3d486bf2c7ddef6c249"],"d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5":[1607022,1573058015,16,"00000000000001dcecfa9ea4a8637479884a5ba7b3aef561d1634376877b1d49"],"dc16e73a88aff40d703f8eba148eae7265d1128a013f6f570a8bd76d86a5e137":[1666106,1581965019,243,"0000000000009176617a2d62fcab4ae45ed25bc745f36a73cf554637e50d73b3"],"dc3257dead616f41848361ccb4a97ca9e1cd7a73ed722a010c119aeced83572c":[1746274,1590160929,77,"0000000068f5999a101d92927dbe76ce2e56f7c9b62db2c6043ce25090f069df"],"e20bf91006e084c63c424240e857af8c3e27a5d8356af4dbe5ddc8ad4e71c336":[1772347,1592578128,45,"000000000000573b617c6814f0b063a24d87fcafd9c081fbe845b17a5a14fb7b"],"e6164df5fad642f938aa1f6727e3be64c4f8dae68277e6e41a56095b95219135":[1746825,1590760652,69,"0000000039adfa26118ca702d16bd9ad54ebc22a80856778b5909673647ce9db"],"e8966645e3114583130ef9371d80535b0a9975b1b6633f6fa293285ffe2a7802":[1612704,1576527199,55,"000000000000008be0f9e93ab4a22da3f67d282c03468bfb9c3c9b479f3a03c2"],"ea26f4e4be7a5fa7c19e1733feb8535567bd3f50a38087a2867cccc5a4e77d21":[1772346,1592576908,49,"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e"],"ef5587eef77420c3b60ab6ed8dc81e499318d3584d4d55428d1b6e1b0cf4a0b2":[1667168,1583123549,18,"00000000000000e73c100f00385fc395b2a74123a95870c34c892e3846d8608b"],"f8a8640121fc89193932de0e4ffb16e917610c0ee8e5bf31fae1ebca60df1d3d":[1636331,1577757044,230,"0000000000007d672215de5c6d66419af6dbb0acb3adb31727433d4d46f9e3d0"],"fe4a5b14b120e71beaec8f5ec7aa197093132effd597407ca033ee33659f9baa":[1772346,1592576908,60,"0000000000007b7b8dac9a0c3863164bc968a6ed59e5b30b81f4c4bf3c23b49e"],"feaa25ec31bca22d5f0bdb0a09e4fa75e4e91f605be11aab1dda0a6e66061877":[1772375,1592591253,9,"0000000000000167e02b208939cf45855de05b8c3ac1d355b240798be1386ba1"]},"wallet_type":"standard","winpos-qt":[750,391,840,407]}' - self._upgrade_storage(wallet_str) + await self._upgrade_storage(wallet_str) ########## @@ -317,7 +317,7 @@ def tearDownClass(cls): cls.plugins.stop() cls.plugins.stopped_event.wait() - def _upgrade_storage(self, wallet_json, accounts=1) -> Optional[WalletDB]: + async def _upgrade_storage(self, wallet_json, accounts=1) -> Optional[WalletDB]: if accounts == 1: # test manual upgrades db = self._load_db_from_json_string(wallet_json=wallet_json, @@ -325,11 +325,11 @@ def _upgrade_storage(self, wallet_json, accounts=1) -> Optional[WalletDB]: self.assertFalse(db.requires_split()) if db.requires_upgrade(): db.upgrade() - self._sanity_check_upgraded_db(db) + await self._sanity_check_upgraded_db(db) # test automatic upgrades db2 = self._load_db_from_json_string(wallet_json=wallet_json, manual_upgrades=False) - self._sanity_check_upgraded_db(db2) + await self._sanity_check_upgraded_db(db2) return db2 else: db = self._load_db_from_json_string(wallet_json=wallet_json, @@ -340,13 +340,13 @@ def _upgrade_storage(self, wallet_json, accounts=1) -> Optional[WalletDB]: for item in split_data: data = json.dumps(item) new_db = WalletDB(data, manual_upgrades=False) - self._sanity_check_upgraded_db(new_db) + await self._sanity_check_upgraded_db(new_db) - def _sanity_check_upgraded_db(self, db): + async def _sanity_check_upgraded_db(self, db): self.assertFalse(db.requires_split()) self.assertFalse(db.requires_upgrade()) wallet = Wallet(db, None, config=self.config) - asyncio.run_coroutine_threadsafe(wallet.stop(), self.asyncio_loop).result() + await wallet.stop() @staticmethod def _load_db_from_json_string(*, wallet_json, manual_upgrades): diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py index dfba11e1b..864091bc8 100644 --- a/electrum/tests/test_wallet.py +++ b/electrum/tests/test_wallet.py @@ -158,7 +158,7 @@ def test_save_garbage(self): class TestCreateRestoreWallet(WalletTestCase): - def test_create_new_wallet(self): + async def test_create_new_wallet(self): passphrase = 'mypassphrase' password = 'mypassword' encrypt_file = True @@ -178,7 +178,7 @@ def test_create_new_wallet(self): self.assertEqual(d['seed'], wallet.keystore.get_seed(password)) self.assertEqual(encrypt_file, wallet.storage.is_encrypted()) - def test_restore_wallet_from_text_mnemonic(self): + async def test_restore_wallet_from_text_mnemonic(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' passphrase = 'mypassphrase' password = 'mypassword' @@ -196,7 +196,7 @@ def test_restore_wallet_from_text_mnemonic(self): self.assertEqual(encrypt_file, wallet.storage.is_encrypted()) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0]) - def test_restore_wallet_from_text_no_storage(self): + async def test_restore_wallet_from_text_no_storage(self): text = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' d = restore_wallet_from_text( text, @@ -209,28 +209,28 @@ def test_restore_wallet_from_text_no_storage(self): self.assertEqual(text, wallet.keystore.get_seed(None)) self.assertEqual('bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', wallet.get_receiving_addresses()[0]) - def test_restore_wallet_from_text_xpub(self): + async def test_restore_wallet_from_text_xpub(self): text = 'zpub6nydoME6CFdJtMpzHW5BNoPz6i6XbeT9qfz72wsRqGdgGEYeivso6xjfw8cGcCyHwF7BNW4LDuHF35XrZsovBLWMF4qXSjmhTXYiHbWqGLt' d = restore_wallet_from_text(text, path=self.wallet_path, gap_limit=1, config=self.config) wallet = d['wallet'] # type: Standard_Wallet self.assertEqual(text, wallet.keystore.get_master_public_key()) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0]) - def test_restore_wallet_from_text_xkey_that_is_also_a_valid_electrum_seed_by_chance(self): + async def test_restore_wallet_from_text_xkey_that_is_also_a_valid_electrum_seed_by_chance(self): text = 'yprvAJBpuoF4FKpK92ofzQ7ge6VJMtorow3maAGPvPGj38ggr2xd1xCrC9ojUVEf9jhW5L9SPu6fU2U3o64cLrRQ83zaQGNa6YP3ajZS6hHNPXj' d = restore_wallet_from_text(text, path=self.wallet_path, gap_limit=1, config=self.config) wallet = d['wallet'] # type: Standard_Wallet self.assertEqual(text, wallet.keystore.get_master_private_key(password=None)) self.assertEqual('3Pa4hfP3LFWqa2nfphYaF7PZfdJYNusAnp', wallet.get_receiving_addresses()[0]) - def test_restore_wallet_from_text_xprv(self): + async def test_restore_wallet_from_text_xprv(self): text = 'zprvAZzHPqhCMt51fskXBUYB1fTFYgG3CBjJUT4WEZTpGw6hPSDWBPZYZARC5sE9xAcX8NeWvvucFws8vZxEa65RosKAhy7r5MsmKTxr3hmNmea' d = restore_wallet_from_text(text, path=self.wallet_path, gap_limit=1, config=self.config) wallet = d['wallet'] # type: Standard_Wallet self.assertEqual(text, wallet.keystore.get_master_private_key(password=None)) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', wallet.get_receiving_addresses()[0]) - def test_restore_wallet_from_text_addresses(self): + async def test_restore_wallet_from_text_addresses(self): text = 'bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c' d = restore_wallet_from_text(text, path=self.wallet_path, config=self.config) wallet = d['wallet'] # type: Imported_Wallet @@ -240,7 +240,7 @@ def test_restore_wallet_from_text_addresses(self): wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c') self.assertEqual(1, len(wallet.get_receiving_addresses())) - def test_restore_wallet_from_text_privkeys(self): + async def test_restore_wallet_from_text_privkeys(self): text = 'p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL p2wpkh:L24GxnN7NNUAfCXA6hFzB1jt59fYAAiFZMcLaJ2ZSawGpM3uqhb1' d = restore_wallet_from_text(text, path=self.wallet_path, config=self.config) wallet = d['wallet'] # type: Imported_Wallet @@ -256,13 +256,7 @@ def test_restore_wallet_from_text_privkeys(self): class TestWalletPassword(WalletTestCase): - def setUp(self): - super().setUp() - - def tearDown(self): - super().tearDown() - - def test_update_password_of_imported_wallet(self): + async def test_update_password_of_imported_wallet(self): wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' db = WalletDB(wallet_str, manual_upgrades=False) storage = WalletStorage(self.wallet_path) @@ -278,7 +272,7 @@ def test_update_password_of_imported_wallet(self): wallet.check_password("wrong password") wallet.check_password("1234") - def test_update_password_of_standard_wallet(self): + async def test_update_password_of_standard_wallet(self): wallet_str = '''{"addr_history":{"12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes":[],"12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1":[],"13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB":[],"13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c":[],"14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz":[],"14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA":[],"15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV":[],"17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z":[],"18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv":[],"18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B":[],"19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz":[],"19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G":[],"1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq":[],"1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d":[],"1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs":[],"1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado":[],"1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z":[],"1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52":[],"1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP":[],"1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv":[],"1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb":[],"1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ":[],"1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G":[],"1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN":[],"1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J":[],"1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt":[]},"addresses":{"change":["1GhmpwqSF5cqNgdr9oJMZx8dKxPRo4pYPP","1GTDSjkVc9vaaBBBGNVqTANHJBcoT5VW9z","15FGuHvRssu1r8fCw98vrbpfc3M4xs5FAV","1A3XSmvLQvePmvm7yctsGkBMX9ZKKXLrVq","19z3j2ELqbg2pR87byCCt3BCyKR7rc3q8G","1JWySzjzJhsETUUcqVZHuvQLA7pfFfmesb"],"receiving":["14gmBxYV97mzYwWdJSJ3MTLbTHVegaKrcA","13HT1pfWctsSXVFzF76uYuVdQvcAQ2MAgB","19a98ZfEezDNbCwidVigV5PAJwrR2kw4Jz","1J5TTUQKhwehEACw6Jjte1E22FVrbeDmpv","1Pm8JBhzUJDqeQQKrmnop1Frr4phe1jbTt","13kG9WH9JqS7hyCcVL1ssLdNv4aXocQY9c","1KQHxcy3QUHAWMHKUtJjqD9cMKXcY2RTwZ","12ECgkzK6gHouKAZ7QiooYBuk1CgJLJxes","12iR43FPb5M7sw4Mcrr5y1nHKepg9EtZP1","14Tf3qiiHJXStSU4KmienAhHfHq7FHpBpz","1KqVEPXdpbYvEbwsZcEKkrA4A2jsgj9hYN","17oJzweA2gn6SDjsKgA9vUD5ocT1sSnr2Z","1E4ygSNJpWL2uPXZHBptmU2LqwZTqb1Ado","18hNcSjZzRcRP6J2bfFRxp9UfpMoC4hGTv","1KoxZfc2KsgovjGDxwqanbFEA76uxgYH4G","18n9PFxBjmKCGhd4PCDEEqYsi2CsnEfn2B","1CmhFe2BN1h9jheFpJf4v39XNPj8F9U6d","1DuphhHUayKzbkdvjVjf5dtjn2ACkz4zEs","1GWqgpThAuSq3tDg6uCoLQxPXQNnU8jZ52","1N16yDSYe76c5A3CoVoWAKxHeAUc8Jhf9J"]},"keystore":{"seed":"cereal wise two govern top pet frog nut rule sketch bundle logic","type":"bip32","xprv":"xprv9s21ZrQH143K29XjRjUs6MnDB9wXjXbJP2kG1fnRk8zjdDYWqVkQYUqaDtgZp5zPSrH5PZQJs8sU25HrUgT1WdgsPU8GbifKurtMYg37d4v","xpub":"xpub661MyMwAqRbcEdcCXm1sTViwjBn28zK9kFfrp4C3JUXiW1sfP34f6HA45B9yr7EH5XGzWuTfMTdqpt9XPrVQVUdgiYb5NW9m8ij1FSZgGBF"},"pruned_txo":{},"seed_type":"standard","seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[619,310,840,405]}''' db = WalletDB(wallet_str, manual_upgrades=False) storage = WalletStorage(self.wallet_path) @@ -293,12 +287,12 @@ def test_update_password_of_standard_wallet(self): wallet.check_password("wrong password") wallet.check_password("1234") - def test_update_password_with_app_restarts(self): + async def test_update_password_with_app_restarts(self): wallet_str = '{"addr_history":{"1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr":[],"15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA":[],"1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6":[]},"addresses":{"change":[],"receiving":["1364Js2VG66BwRdkaoxAaFtdPb1eQgn8Dr","1Exet2BhHsFxKTwhnfdsBMkPYLGvobxuW6","15CyDgLffJsJgQrhcyooFH4gnVDG82pUrA"]},"keystore":{"keypairs":{"0344b1588589958b0bcab03435061539e9bcf54677c104904044e4f8901f4ebdf5":"L2sED74axVXC4H8szBJ4rQJrkfem7UMc6usLCPUoEWxDCFGUaGUM","0389508c13999d08ffae0f434a085f4185922d64765c0bff2f66e36ad7f745cc5f":"L3Gi6EQLvYw8gEEUckmqawkevfj9s8hxoQDFveQJGZHTfyWnbk1U","04575f52b82f159fa649d2a4c353eb7435f30206f0a6cb9674fbd659f45082c37d559ffd19bea9c0d3b7dcc07a7b79f4cffb76026d5d4dff35341efe99056e22d2":"5JyVyXU1LiRXATvRTQvR9Kp8Rx1X84j2x49iGkjSsXipydtByUq"},"type":"imported"},"pruned_txo":{},"seed_version":13,"stored_height":-1,"transactions":{},"tx_fees":{},"txi":{},"txo":{},"use_encryption":false,"verified_tx3":{},"wallet_type":"standard","winpos-qt":[100,100,840,405]}' db = WalletDB(wallet_str, manual_upgrades=False) storage = WalletStorage(self.wallet_path) wallet = Wallet(db, storage, config=self.config) - asyncio.run_coroutine_threadsafe(wallet.stop(), self.asyncio_loop).result() + await wallet.stop() storage = WalletStorage(self.wallet_path) # if storage.is_encrypted(): diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index c1c395487..83d90d2b3 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -89,7 +89,7 @@ def setUp(self): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_standard(self, mock_save_db): + async def test_electrum_seed_standard(self, mock_save_db): seed_words = 'cycle rocket west magnet parrot shuffle foot correct salt library feed song' self.assertEqual(seed_type(seed_words), 'standard') @@ -108,7 +108,7 @@ def test_electrum_seed_standard(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '1KSezYMhAJMWqFbVFB2JshYg69UpmEXR4D') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_segwit(self, mock_save_db): + async def test_electrum_seed_segwit(self, mock_save_db): seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' self.assertEqual(seed_type(seed_words), 'segwit') @@ -130,7 +130,7 @@ def test_electrum_seed_segwit(self, mock_save_db): ks.get_lightning_xprv(None)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_segwit_passphrase(self, mock_save_db): + async def test_electrum_seed_segwit_passphrase(self, mock_save_db): seed_words = 'bitter grass shiver impose acquire brush forget axis eager alone wine silver' self.assertEqual(seed_type(seed_words), 'segwit') @@ -152,7 +152,7 @@ def test_electrum_seed_segwit_passphrase(self, mock_save_db): ks.get_lightning_xprv(None)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_old(self, mock_save_db): + async def test_electrum_seed_old(self, mock_save_db): seed_words = 'powerful random nobody notice nothing important anyway look away hidden message over' self.assertEqual(seed_type(seed_words), 'old') @@ -170,7 +170,7 @@ def test_electrum_seed_old(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '1KRW8pH6HFHZh889VDq6fEKvmrsmApwNfe') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_2fa_legacy_pre27(self, mock_save_db): + async def test_electrum_seed_2fa_legacy_pre27(self, mock_save_db): # pre-version-2.7 2fa seed seed_words = 'bind clever room kidney crucial sausage spy edit canvas soul liquid ribbon slam open alpha suffer gate relax voice carpet law hill woman tonight abstract' self.assertEqual(seed_type(seed_words), '2fa') @@ -205,7 +205,7 @@ def test_electrum_seed_2fa_legacy_pre27(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '3Ke6pKrmtSyyQaMob1ES4pk8siAAkRmst9') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_2fa_legacy_post27(self, mock_save_db): + async def test_electrum_seed_2fa_legacy_post27(self, mock_save_db): # post-version-2.7 2fa seed seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' self.assertEqual(seed_type(seed_words), '2fa') @@ -240,7 +240,7 @@ def test_electrum_seed_2fa_legacy_post27(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '3PeZEcumRqHSPNN43hd4yskGEBdzXgY8Cy') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_seed_2fa_segwit(self, mock_save_db): + async def test_electrum_seed_2fa_segwit(self, mock_save_db): seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise' self.assertEqual(seed_type(seed_words), '2fa_segwit') @@ -274,7 +274,7 @@ def test_electrum_seed_2fa_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], 'bc1qd4q50nft7kxm9yglfnpup9ed2ukj3tkxp793y0zya8dc9m39jcwq308dxz') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_seed_bip44_standard(self, mock_save_db): + async def test_bip39_seed_bip44_standard(self, mock_save_db): seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) @@ -293,7 +293,7 @@ def test_bip39_seed_bip44_standard(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '1GG5bVeWgAp5XW7JLCphse14QaC4qiHyWn') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_seed_bip44_standard_passphrase(self, mock_save_db): + async def test_bip39_seed_bip44_standard_passphrase(self, mock_save_db): seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) @@ -312,7 +312,7 @@ def test_bip39_seed_bip44_standard_passphrase(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '1H4QD1rg2zQJ4UjuAVJr5eW1fEM8WMqyxh') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_seed_bip49_p2sh_segwit(self, mock_save_db): + async def test_bip39_seed_bip49_p2sh_segwit(self, mock_save_db): seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) @@ -331,7 +331,7 @@ def test_bip39_seed_bip49_p2sh_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '3KaBTcviBLEJajTEMstsA2GWjYoPzPK7Y7') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_seed_bip84_native_segwit(self, mock_save_db): + async def test_bip39_seed_bip84_native_segwit(self, mock_save_db): # test case from bip84 seed_words = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) @@ -351,7 +351,7 @@ def test_bip39_seed_bip84_native_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_multisig_seed_standard(self, mock_save_db): + async def test_electrum_multisig_seed_standard(self, mock_save_db): seed_words = 'blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure' self.assertEqual(seed_type(seed_words), 'standard') @@ -373,7 +373,7 @@ def test_electrum_multisig_seed_standard(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '36XWwEHrrVCLnhjK5MrVVGmUHghr9oWTN1') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_electrum_multisig_seed_segwit(self, mock_save_db): + async def test_electrum_multisig_seed_segwit(self, mock_save_db): seed_words = 'snow nest raise royal more walk demise rotate smooth spirit canyon gun' self.assertEqual(seed_type(seed_words), 'segwit') @@ -395,7 +395,7 @@ def test_electrum_multisig_seed_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], 'bc1qxqf840dqswcmu7a8v82fj6ej0msx08flvuy6kngr7axstjcaq6us9hrehd') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_multisig_seed_bip45_standard(self, mock_save_db): + async def test_bip39_multisig_seed_bip45_standard(self, mock_save_db): seed_words = 'treat dwarf wealth gasp brass outside high rent blood crowd make initial' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) @@ -418,7 +418,7 @@ def test_bip39_multisig_seed_bip45_standard(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '3FGyDuxgUDn2pSZe5xAJH1yUwSdhzDMyEE') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_multisig_seed_p2sh_segwit(self, mock_save_db): + async def test_bip39_multisig_seed_p2sh_segwit(self, mock_save_db): # bip39 seed: pulse mixture jazz invite dune enrich minor weapon mosquito flight fly vapor # der: m/49'/0'/0' # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh @@ -439,7 +439,7 @@ def test_bip39_multisig_seed_p2sh_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '39RhtDchc6igmx5tyoimhojFL1ZbQBrXa6') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip32_extended_version_bytes(self, mock_save_db): + async def test_bip32_extended_version_bytes(self, mock_save_db): seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) bip32_seed = keystore.bip39_to_seed(seed_words, '') @@ -498,7 +498,7 @@ def create_keystore_from_bip32seed(xtype): self.assertEqual(w.get_change_addresses()[0], 'bc1q0fj5mra96hhnum80kllklc52zqn6kppt3hyzr49yhr3ecr42z3tsrkg3gs') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_slip39_basic_3of6_bip44_standard(self, mock_save_db): + async def test_slip39_basic_3of6_bip44_standard(self, mock_save_db): """ BIP32 Root Key for passphrase "TREZOR": xprv9s21ZrQH143K2pMWi8jrTawHaj16uKk4CSbvo4Zt61tcrmuUDMx2o1Byzcr3saXNGNvHP8zZgXVdJHsXVdzYFPavxvCyaGyGr1WkAYG83ce @@ -525,7 +525,7 @@ def test_slip39_basic_3of6_bip44_standard(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '1Aw4wpXsAyEHSgMZqPdyewoAtJqH9Jaso3') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_slip39_basic_2of5_bip49_p2sh_segwit(self, mock_save_db): + async def test_slip39_basic_2of5_bip49_p2sh_segwit(self, mock_save_db): """ BIP32 Root Key for passphrase "TREZOR": xprv9s21ZrQH143K2o6EXEHpVy8TCYoMmkBnDCCESLdR2ieKwmcNG48ck2XJQY4waS7RUQcXqR9N7HnQbUVEDMWYyREdF1idQqxFHuCfK7fqFni @@ -551,7 +551,7 @@ def test_slip39_basic_2of5_bip49_p2sh_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '3FVvdRhR7racZhmcvrGAqX9eJoP8Sw3ypp') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_slip39_groups_128bit_bip84_native_segwit(self, mock_save_db): + async def test_slip39_groups_128bit_bip84_native_segwit(self, mock_save_db): """ BIP32 Root Key for passphrase "TREZOR": xprv9s21ZrQH143K3dzDLfeY3cMp23u5vDeFYftu5RPYZPucKc99mNEddU4w99GxdgUGcSfMpVDxhnR1XpJzZNXRN1m6xNgnzFS5MwMP6QyBRKV @@ -581,7 +581,7 @@ def test_slip39_groups_128bit_bip84_native_segwit(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], 'bc1q8l6hcvlczu4mtjcnlwhczw7vdxnvwccpjl3cwz') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_slip39_groups_256bit_bip49_p2sh_segwit(self, mock_save_db): + async def test_slip39_groups_256bit_bip49_p2sh_segwit(self, mock_save_db): """ BIP32 Root Key for passphrase "TREZOR": xprv9s21ZrQH143K2UspC9FRPfQC9NcDB4HPkx1XG9UEtuceYtpcCZ6ypNZWdgfxQ9dAFVeD1F4Zg4roY7nZm2LB7THPD6kaCege3M7EuS8v85c @@ -617,7 +617,7 @@ def setUp(self): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_save_db): + async def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_save_db): # bip39 seed: finish seminar arrange erosion sunny coil insane together pretty lunch lunch rose # der: m/49'/1'/0' # NOTE: there is currently no bip43 standard derivation path for p2wsh-p2sh @@ -638,7 +638,7 @@ def test_bip39_multisig_seed_p2sh_segwit_testnet(self, mock_save_db): self.assertEqual(w.get_change_addresses()[0], '2NFp9w8tbYYP9Ze2xQpeYBJQjx3gbXymHX7') @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_bip32_extended_version_bytes(self, mock_save_db): + async def test_bip32_extended_version_bytes(self, mock_save_db): seed_words = 'crouch dumb relax small truck age shine pink invite spatial object tenant' self.assertEqual(keystore.bip39_is_checksum_valid(seed_words), (True, True)) bip32_seed = keystore.bip39_to_seed(seed_words, '') @@ -711,7 +711,7 @@ def create_standard_wallet_from_seed(self, seed_words, *, config=None, gap_limit return WalletIntegrityHelper.create_standard_wallet(ks, gap_limit=gap_limit, config=config) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db): + async def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db): wallet1 = self.create_standard_wallet_from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver') wallet2 = self.create_standard_wallet_from_seed('cycle rocket west magnet parrot shuffle foot correct salt library feed song') @@ -767,7 +767,7 @@ def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db): self.assertEqual((0, 250000 - 5000 - 100000, 0), wallet2.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_db): + async def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_db): wallet1a = WalletIntegrityHelper.create_multisig_wallet( [ keystore.from_seed('blast uniform dragon fiscal ensure vast young utility dinosaur abandon rookie sure', '', True), @@ -847,7 +847,7 @@ def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_db): self.assertEqual((0, 370000 - 5000 - 100000, 0), wallet2.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db): + async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db): wallet1a = WalletIntegrityHelper.create_multisig_wallet( [ keystore.from_seed('bitter grass shiver impose acquire brush forget axis eager alone wine silver', '', True), @@ -956,7 +956,7 @@ def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db): self.assertEqual((0, 165000 - 5000 - 100000, 0), wallet2a.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): + async def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): wallet1a = WalletIntegrityHelper.create_multisig_wallet( [ keystore.from_seed('phone guilt ancient scan defy gasp off rotate approve ill word exchange', '', True), @@ -1025,7 +1025,7 @@ def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): self.assertEqual((0, 1000000 - 5000 - 300000, 0), wallet2.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_rbf(self, mock_save_db): + async def test_rbf(self, mock_save_db): self.maxDiff = None config = SimpleConfig({'electrum_path': self.electrum_path}) config.set_key('coin_chooser_output_rounding', False) @@ -1266,7 +1266,7 @@ def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config self.assertEqual((0, 18700, 0), wallet.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_cpfp_p2pkh(self, mock_save_db): + async def test_cpfp_p2pkh(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') # bootstrap wallet @@ -1366,6 +1366,9 @@ def _bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but class NetworkMock: relay_fee = 1000 async def get_transaction(self, txid, timeout=None): + return self._gettx(txid) + @staticmethod + def _gettx(txid): if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd": return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00" else: @@ -1384,6 +1387,7 @@ def is_tip_stale(self): wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve', config=config) wallet.network = NetworkMock() + wallet._get_rawtx_from_network = NetworkMock._gettx # bootstrap wallet funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00') @@ -1419,6 +1423,9 @@ def _bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(self class NetworkMock: relay_fee = 1000 async def get_transaction(self, txid, timeout=None): + return self._gettx(txid) + @staticmethod + def _gettx(txid): if txid == "08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3": return "02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00" else: @@ -1440,6 +1447,7 @@ def is_tip_stale(self): gap_limit=4, ) wallet.network = NetworkMock() + wallet._get_rawtx_from_network = NetworkMock._gettx # bootstrap wallet funding_tx = Transaction('02000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00') @@ -1782,7 +1790,7 @@ def _rbf_batching(self, *, simulate_moving_txs, config): self.assertEqual((0, 3_900_000, 0), wallet.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_cpfp_p2wpkh(self, mock_save_db): + async def test_cpfp_p2wpkh(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') # bootstrap wallet @@ -1814,7 +1822,7 @@ def test_cpfp_p2wpkh(self, mock_save_db): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - def test_sweep_uncompressed_p2pk(self): + async def test_sweep_uncompressed_p2pk(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1831,9 +1839,7 @@ async def get_transaction(self, txid): privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu',] network = NetworkMock() dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' - sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=1325785, tx_version=1) - loop = util.get_asyncio_loop() - tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=1325785, tx_version=1) tx_copy = tx_from_any(tx.serialize()) self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400', @@ -1841,7 +1847,7 @@ async def get_transaction(self, txid): self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid()) self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) - def test_sweep_compressed_p2pk(self): + async def test_sweep_compressed_p2pk(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1858,9 +1864,7 @@ async def get_transaction(self, txid): privkeys = ['cUygTZe4jZLVwE4G44NznCPTeGvgsgassqucUHkAJxGC71Rst2kH',] network = NetworkMock() dest_addr = 'tb1q5uy5xjcn55gwdkmghht8yp3vwz3088f6e3e0em' - sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420006, tx_version=2) - loop = util.get_asyncio_loop() - tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420006, tx_version=2) tx_copy = tx_from_any(tx.serialize()) self.assertEqual('02000000015eb359ccfcd67c3e6b10bb937a796807007708c1f413840d0e627a3f94a1a48401000000484730440220043fc85a43e918ac41e494e309fdf204ca245d260cb5ea09108b196ca65d8a09022056f852f0f521e79ab2124d7e9f779c7290329ce5628ef8e92601980b065d3eb501fdffffff017f9e010000000000160014a709434b13a510e6db68bdd672062c70a2f39d3a26ed2400', @@ -1868,7 +1872,7 @@ async def get_transaction(self, txid): self.assertEqual('968a501350b954ecb51948202b8d0613aa84123ca9b745c14e208cb14feeff59', tx_copy.txid()) self.assertEqual('968a501350b954ecb51948202b8d0613aa84123ca9b745c14e208cb14feeff59', tx_copy.wtxid()) - def test_sweep_uncompressed_p2pkh(self): + async def test_sweep_uncompressed_p2pkh(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1885,9 +1889,7 @@ async def get_transaction(self, txid): privkeys = ['p2pkh:91gxDahzHiJ63HXmLP7pvZrkF8i5gKBXk4VqWfhbhJjtf6Ni5NU',] network = NetworkMock() dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' - sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420010, tx_version=2) - loop = util.get_asyncio_loop() - tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420010, tx_version=2) tx_copy = tx_from_any(tx.serialize()) self.assertEqual('02000000010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca715000000008a47304402206d2dae571ca2f51e0d4a8ce6a6335fa25ac09f4bbed26439124d93f035bdbb130220249dc2039f1da338a40679f0e79c25a2dc2983688e6c04753348f2aa8435e375014104b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b2987b4c862d5b687bb5328adccc69e67a17b109b6328228695a1c384573acd6199fdffffff0186500300000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071f2aed2400', @@ -1895,7 +1897,7 @@ async def get_transaction(self, txid): self.assertEqual('d62048493bf8459be5e1e3cab6caabc8f15661d02c364d8dc008297e573772bf', tx_copy.txid()) self.assertEqual('d62048493bf8459be5e1e3cab6caabc8f15661d02c364d8dc008297e573772bf', tx_copy.wtxid()) - def test_sweep_compressed_p2pkh(self): + async def test_sweep_compressed_p2pkh(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1912,9 +1914,7 @@ async def get_transaction(self, txid): privkeys = ['p2pkh:cN3LiXmurmGRF5xngYd8XS2ZsP2KeXFUh4SH7wpC8uJJzw52JPq1',] network = NetworkMock() dest_addr = 'tb1q782f750ekkxysp2rrscr6yknmn634e2pv8lktu' - sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=1000, locktime=2420010, tx_version=2) - loop = util.get_asyncio_loop() - tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=1000, locktime=2420010, tx_version=2) tx_copy = tx_from_any(tx.serialize()) self.assertEqual('02000000016717835a2e1e152a69e7528a0f1346c1d37ee6e76c5e23b5d1c5a5b40241768a000000006a473044022038ad38003943bfd3ed39ba4340d545753fcad632a8fe882d01e4f0140ddb3cfb022019498260e29f5fbbcde9176bfb3553b7acec5fe284a9a3a33547a2d082b60355012103b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b29fdffffff0158de010000000000160014f1d49f51f9b58c4805431c303d12d3dcf51ae5412aed2400', @@ -1922,7 +1922,7 @@ async def get_transaction(self, txid): self.assertEqual('432c108626581fc6a7d3efc9dac5f3dec8286cec47dfaab86b4267d10381586c', tx_copy.txid()) self.assertEqual('432c108626581fc6a7d3efc9dac5f3dec8286cec47dfaab86b4267d10381586c', tx_copy.wtxid()) - def test_sweep_p2wpkh_p2sh(self): + async def test_sweep_p2wpkh_p2sh(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1939,9 +1939,7 @@ async def get_transaction(self, txid): privkeys = ['p2wpkh-p2sh:cQMRGsiEsFX5YoxVZaMEzBruAkCWnoFf1SG7SRm2tLHDEN165TrA',] network = NetworkMock() dest_addr = 'tb1qu7n2tzm90a3f29kvxlhzsc7t40ddk075ut5w44' - sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2) - loop = util.get_asyncio_loop() - tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2) tx_copy = tx_from_any(tx.serialize()) self.assertEqual('020000000001011d1725072a6e60687a59b878ecaf940ea0385880613d9d5502102bd78ef97b9a0000000017160014e7a6a58b657f629516cc37ee2863cbabdadb3fd4fdffffff01fc47020000000000160014e7a6a58b657f629516cc37ee2863cbabdadb3fd402473044022048ea4c558fd374f5d5066440a7f4933393cb377802cb949e3039fedf0378a29402204b4a58c591117cc1e37f07b03cc03cc6198dbf547e2bff813e2e2102bd2057e00121029f46ba81b3c6ad84e52841364dc54ca1097d0c30a68fb529766504c4b1c599352aed2400', @@ -1949,7 +1947,7 @@ async def get_transaction(self, txid): self.assertEqual('0680124954ccc158cbf24d289c93579f68fd75916509214066f69e09adda1861', tx_copy.txid()) self.assertEqual('da8567d9b28e9e0ed8b3dcef6e619eba330cec6cb0c55d57f658f5ca06e02eb0', tx_copy.wtxid()) - def test_sweep_p2wpkh(self): + async def test_sweep_p2wpkh(self): class NetworkMock: relay_fee = 1000 async def listunspent_for_scripthash(self, scripthash): @@ -1966,9 +1964,7 @@ async def get_transaction(self, txid): privkeys = ['p2wpkh:cV2BvgtpLNX328m4QrhqycBGA6EkZUFfHM9kKjVXjfyD53uNfC4q',] network = NetworkMock() dest_addr = 'tb1qhuy2e45lrdcp9s4ezeptx5kwxcnahzgpar9scc' - sweep_coro = sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2) - loop = util.get_asyncio_loop() - tx = asyncio.run_coroutine_threadsafe(sweep_coro, loop).result() + tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2) tx_copy = tx_from_any(tx.serialize()) self.assertEqual('02000000000101e328aeb4f9dc1b85a2709ce59b0478a15ed9fb5e7f84fb62422f99b8cd6ad7010000000000fdffffff01087e010000000000160014bf08acd69f1b7012c2b91642b352ce3627db89010247304402204993099c4663d92ef4c9a28b3f45a40a6585754fe22ecfdc0a76c43fda7c9d04022006a75e0fd3ad1862d8e81015a71d2a1489ec7a9264e6e63b8fe6bb90c27e799b0121038ca94e7c715152fd89803c2a40a934c7c4035fb87b3cba981cd1e407369cfe312aed2400', @@ -1977,7 +1973,7 @@ async def get_transaction(self, txid): self.assertEqual('b062d2e19880c66b36e80b823c2d00a2769658d1e574ff854dab15efd8fd7da8', tx_copy.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db): + async def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db): wallet1 = WalletIntegrityHelper.create_standard_wallet( keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', ''), gap_limit=2, @@ -2061,7 +2057,7 @@ def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_save_db): self.assertEqual((0, 10495000, 0), wallet2.get_balance()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_standard_wallet_cannot_sign_multisig_input_even_if_cosigner(self, mock_save_db): + async def test_standard_wallet_cannot_sign_multisig_input_even_if_cosigner(self, mock_save_db): """Just because our keystore recognizes the pubkeys in a txin, if the prevout does not belong to the wallet, then wallet.is_mine and wallet.can_sign should return False (e.g. multisig input for single-sig wallet). (see issue #5948) @@ -2113,7 +2109,7 @@ def test_standard_wallet_cannot_sign_multisig_input_even_if_cosigner(self, mock_ self.assertFalse(any([wallet_frost.is_mine(txout.address) for txout in tx.outputs()])) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_dscancel(self, mock_save_db): + async def test_dscancel(self, mock_save_db): self.maxDiff = None config = SimpleConfig({'electrum_path': self.electrum_path}) config.set_key('coin_chooser_output_rounding', False) @@ -2316,6 +2312,9 @@ def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, confi class NetworkMock: relay_fee = 1000 async def get_transaction(self, txid, timeout=None): + return self._gettx(txid) + @staticmethod + def _gettx(txid): if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd": return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00" else: @@ -2334,6 +2333,7 @@ def is_tip_stale(self): wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve', config=config) wallet.network = NetworkMock() + wallet._get_rawtx_from_network = NetworkMock._gettx # bootstrap wallet funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00') @@ -2366,7 +2366,7 @@ def is_tip_stale(self): self.assertEqual('3021a4fe24e33af9d0ccdf25c478387c97df671fe1fd8b4db0de4255b3a348c5', tx_copy.txid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_wallet_history_chain_of_unsigned_transactions(self, mock_save_db): + async def test_wallet_history_chain_of_unsigned_transactions(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('cross end slow expose giraffe fuel track awake turtle capital ranch pulp', config=self.config, gap_limit=3) @@ -2408,7 +2408,7 @@ def test_wallet_history_chain_of_unsigned_transactions(self, mock_save_db): coins[0].prevout.to_str()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_wallet_does_not_create_zero_input_tx(self, mock_save_db): + async def test_wallet_does_not_create_zero_input_tx(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('cross end slow expose giraffe fuel track awake turtle capital ranch pulp', config=self.config, gap_limit=3) @@ -2432,7 +2432,7 @@ def test_wallet_does_not_create_zero_input_tx(self, mock_save_db): self.assertEqual(2, len(tx.outputs())) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_imported_wallet_usechange_off(self, mock_save_db): + async def test_imported_wallet_usechange_off(self, mock_save_db): wallet = restore_wallet_from_text( "p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n", path='if_this_exists_mocking_failed_648151893', @@ -2468,7 +2468,7 @@ def test_imported_wallet_usechange_off(self, mock_save_db): str(tx_copy)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_imported_wallet_usechange_on(self, mock_save_db): + async def test_imported_wallet_usechange_on(self, mock_save_db): wallet = restore_wallet_from_text( "p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n", path='if_this_exists_mocking_failed_648151893', @@ -2503,7 +2503,7 @@ def test_imported_wallet_usechange_on(self, mock_save_db): str(tx_copy)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_imported_wallet_usechange_on__no_more_unused_addresses(self, mock_save_db): + async def test_imported_wallet_usechange_on__no_more_unused_addresses(self, mock_save_db): wallet = restore_wallet_from_text( "p2wpkh:cVcwSp488C8Riguq55Tuktgi6TpzuyLdDwUxkBDBz3yzV7FW4af2 p2wpkh:cPWyoPvnv2hiyyxbhMkhX3gPEENzB6DqoP9bbR8SDTg5njK5SL9n", path='if_this_exists_mocking_failed_648151893', @@ -2548,7 +2548,7 @@ def test_imported_wallet_usechange_on__no_more_unused_addresses(self, mock_save_ str(tx_copy)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_get_spendable_coins(self, mock_save_db): + async def test_get_spendable_coins(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=self.config) @@ -2584,7 +2584,7 @@ def test_get_spendable_coins(self, mock_save_db): {txi.prevout.to_str() for txi in wallet.get_spendable_coins()}) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_export_psbt_with_xpubs__multisig(self, mock_save_db): + async def test_export_psbt_with_xpubs__multisig(self, mock_save_db): """When exporting a PSBT to be signed by a hw device, test that we populate the PSBT_GLOBAL_XPUB field with wallet xpubs. """ @@ -2658,7 +2658,7 @@ def test_export_psbt_with_xpubs__multisig(self, mock_save_db): tx.serialize_as_bytes().hex()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_export_psbt_with_xpubs__singlesig(self, mock_save_db): + async def test_export_psbt_with_xpubs__singlesig(self, mock_save_db): """When exporting a PSBT to be signed by a hw device, test that we populate the PSBT_GLOBAL_XPUB field with wallet xpubs. """ @@ -2701,7 +2701,7 @@ def setUp(self): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db): + async def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( keystore.from_seed('alone body father children lead goodbye phone twist exist grass kick join', '', False), gap_limit=4, @@ -2746,7 +2746,7 @@ def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db): self.assertEqual('06032230d0bf6a277bc4f8c39e3311a712e0e614626d0dea7cc9f592abfae5d8', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db): + async def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/44'/1'/0' keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), @@ -2800,7 +2800,7 @@ def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db): self.assertEqual('d9c21696eca80321933e7444ca928aaf25eeda81aaa2f4e5c085d4d0a9cf7aa7', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_save_db): + async def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/49'/1'/0' keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), @@ -2845,7 +2845,7 @@ def test_sending_offline_xprv_online_xpub_p2wpkh_p2sh(self, mock_save_db): self.assertEqual('27b78ec072a403b0545258e7a1a8d494e4b6fd48bf77f4251a12160c92207cbc', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): + async def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/84'/1'/0' keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), @@ -2900,7 +2900,7 @@ def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_offline_signing_beyond_gap_limit(self, mock_save_db): + async def test_offline_signing_beyond_gap_limit(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/84'/1'/0' keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), @@ -2945,7 +2945,7 @@ def test_offline_signing_beyond_gap_limit(self, mock_save_db): self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_signing_where_offline_ks_does_not_have_keyorigin_but_psbt_contains_it(self, mock_save_db): + async def test_signing_where_offline_ks_does_not_have_keyorigin_but_psbt_contains_it(self, mock_save_db): # keystore has intermediate xprv without root fp; tx contains root fp and full path. # tx has input with key beyond gap limit wallet_offline = WalletIntegrityHelper.create_standard_wallet( @@ -2966,7 +2966,7 @@ def test_signing_where_offline_ks_does_not_have_keyorigin_but_psbt_contains_it(s str(tx)) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_wif_online_addr_p2pkh(self, mock_save_db): # compressed pubkey + async def test_sending_offline_wif_online_addr_p2pkh(self, mock_save_db): # compressed pubkey wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) @@ -3003,7 +3003,7 @@ def test_sending_offline_wif_online_addr_p2pkh(self, mock_save_db): # compresse self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_save_db): + async def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline.import_private_key('p2wpkh-p2sh:cU9hVzhpvfn91u2zTVn8uqF2ymS7ucYH8V5TmsTDmuyMHgRk9WsJ', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) @@ -3040,7 +3040,7 @@ def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_save_db): self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_wif_online_addr_p2wpkh(self, mock_save_db): + async def test_sending_offline_wif_online_addr_p2wpkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline.import_private_key('p2wpkh:cPuQzcNEgbeYZ5at9VdGkCwkPA9r34gvEVJjuoz384rTfYpahfe7', password=None) wallet_online = WalletIntegrityHelper.create_imported_wallet(privkeys=False, config=self.config) @@ -3077,7 +3077,7 @@ def test_sending_offline_wif_online_addr_p2wpkh(self, mock_save_db): self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db): # compressed pubkey + async def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db): # compressed pubkey wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/44'/1'/0' keystore.from_xprv('tprv8gfKwjuAaqtHgqxMh1tosAQ28XvBMkcY5NeFRA3pZMpz6MR4H4YZ3MJM4fvNPnRKeXR1Td2vQGgjorNXfo94WvT5CYDsPAqjHxSn436G1Eu'), @@ -3118,7 +3118,7 @@ def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db): # compress self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_save_db): + async def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/49'/1'/0' keystore.from_xprv('uprv8zHHrMQMQ26utWwNJ5MK2SXpB9hbmy7pbPaneii69xT8cZTyFpxQFxkknGWKP8dxBTZhzy7yP6cCnLrRCQjzJDk3G61SjZpxhFQuB2NR8a5'), @@ -3159,7 +3159,7 @@ def test_sending_offline_xprv_online_addr_p2wpkh_p2sh(self, mock_save_db): self.assertEqual('9bb9949974954613945756c48ca5525cd5cba1b667ccb10c7a53e1ed076a1117', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_save_db): + async def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_save_db): wallet_offline = WalletIntegrityHelper.create_standard_wallet( # bip39: "qwe", der: m/84'/1'/0' keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), @@ -3200,7 +3200,7 @@ def test_sending_offline_xprv_online_addr_p2wpkh(self, mock_save_db): self.assertEqual('3b7cc3c3352bbb43ddc086487ac696e09f2863c3d9e8636721851b8008a83ffa', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db): + async def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db): # 2-of-3 legacy p2sh multisig wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( [ @@ -3265,7 +3265,7 @@ def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db): self.assertEqual('0e8fdc8257a85ebe7eeab14a53c2c258c61a511f64176b7f8fc016bc2263d307', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_save_db): + async def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_save_db): # 2-of-2 p2sh-embedded segwit multisig wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( [ @@ -3334,7 +3334,7 @@ def test_sending_offline_hd_multisig_online_addr_p2wsh_p2sh(self, mock_save_db): self.assertEqual('96d0bca1001778c54e4c3a07929fab5562c5b5a23fd1ca3aa3870cc5df2bf97d', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_save_db): + async def test_sending_offline_hd_multisig_online_addr_p2wsh(self, mock_save_db): # 2-of-3 p2wsh multisig wallet_offline1 = WalletIntegrityHelper.create_multisig_wallet( [ @@ -3439,7 +3439,7 @@ def create_old_wallet(self): return w @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_restoring_old_wallet_txorder1(self, mock_save_db): + async def test_restoring_old_wallet_txorder1(self, mock_save_db): w = self.create_old_wallet() for i in [2, 12, 7, 9, 11, 10, 16, 6, 17, 1, 13, 15, 5, 8, 4, 0, 14, 18, 3]: tx = Transaction(self.transactions[self.txid_list[i]]) @@ -3447,7 +3447,7 @@ def test_restoring_old_wallet_txorder1(self, mock_save_db): self.assertEqual(27633300, sum(w.get_balance())) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_restoring_old_wallet_txorder2(self, mock_save_db): + async def test_restoring_old_wallet_txorder2(self, mock_save_db): w = self.create_old_wallet() for i in [9, 18, 2, 0, 13, 3, 1, 11, 4, 17, 7, 14, 12, 15, 10, 8, 5, 6, 16]: tx = Transaction(self.transactions[self.txid_list[i]]) @@ -3455,7 +3455,7 @@ def test_restoring_old_wallet_txorder2(self, mock_save_db): self.assertEqual(27633300, sum(w.get_balance())) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_restoring_old_wallet_txorder3(self, mock_save_db): + async def test_restoring_old_wallet_txorder3(self, mock_save_db): w = self.create_old_wallet() for i in [5, 8, 17, 0, 9, 10, 12, 3, 15, 18, 2, 11, 14, 7, 16, 1, 4, 6, 13]: tx = Transaction(self.transactions[self.txid_list[i]]) @@ -3488,7 +3488,7 @@ def create_wallet(self): return w @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_restoring_wallet_txorder1(self, mock_save_db): + async def test_restoring_wallet_txorder1(self, mock_save_db): w = self.create_wallet() w.db.put('stored_height', 1316917 + 100) for txid in self.transactions: @@ -3537,7 +3537,7 @@ def setUp(self): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_restoring_wallet_without_manual_delete(self, mock_save_db): + async def test_restoring_wallet_without_manual_delete(self, mock_save_db): w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel", path='if_this_exists_mocking_failed_648151893', gap_limit=5, @@ -3551,7 +3551,7 @@ def test_restoring_wallet_without_manual_delete(self, mock_save_db): self.assertEqual(999890, sum(w.get_balance())) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_restoring_wallet_with_manual_delete(self, mock_save_db): + async def test_restoring_wallet_with_manual_delete(self, mock_save_db): w = restore_wallet_from_text("small rapid pattern language comic denial donate extend tide fever burden barrel", path='if_this_exists_mocking_failed_648151893', gap_limit=5, @@ -3588,7 +3588,7 @@ def setUp(self): self.config = SimpleConfig({'electrum_path': self.electrum_path}) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') - def test_importing_and_deleting_addresses(self, mock_save_db): + async def test_importing_and_deleting_addresses(self, mock_save_db): w = restore_wallet_from_text("tb1q7648a2pm2se425lvun0g3vlf4ahmflcthegz63", path='if_this_exists_mocking_failed_648151893', config=self.config)['wallet'] # type: Abstract_Wallet diff --git a/electrum/util.py b/electrum/util.py index b7ec0c97d..ba7dd690a 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1561,12 +1561,17 @@ def is_tor_socks_port(host: str, port: int) -> bool: return False +AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP = False # used by unit tests + _asyncio_event_loop = None # type: Optional[asyncio.AbstractEventLoop] def get_asyncio_loop() -> asyncio.AbstractEventLoop: """Returns the global asyncio event loop we use.""" - if _asyncio_event_loop is None: - raise Exception("event loop not created yet") - return _asyncio_event_loop + if loop := _asyncio_event_loop: + return loop + if AS_LIB_USER_I_WANT_TO_MANAGE_MY_OWN_ASYNCIO_LOOP: + if loop := get_running_loop(): + return loop + raise Exception("event loop not created yet") def create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop, @@ -1777,7 +1782,6 @@ class CallbackManager: def __init__(self): self.callback_lock = threading.Lock() self.callbacks = defaultdict(list) # note: needs self.callback_lock - self.asyncio_loop = None def register_callback(self, func, events): with self.callback_lock: @@ -1795,21 +1799,20 @@ def trigger_callback(self, event, *args): Can be called from any thread. The callback itself will get scheduled on the event loop. """ - if self.asyncio_loop is None: - self.asyncio_loop = get_asyncio_loop() - assert self.asyncio_loop.is_running(), "event loop not running" + loop = get_asyncio_loop() + assert loop.is_running(), "event loop not running" with self.callback_lock: callbacks = self.callbacks[event][:] for callback in callbacks: # FIXME: if callback throws, we will lose the traceback if asyncio.iscoroutinefunction(callback): - asyncio.run_coroutine_threadsafe(callback(*args), self.asyncio_loop) - elif get_running_loop() == self.asyncio_loop: + asyncio.run_coroutine_threadsafe(callback(*args), loop) + elif get_running_loop() == loop: # run callback immediately, so that it is guaranteed # to have been executed when this method returns callback(*args) else: - self.asyncio_loop.call_soon_threadsafe(callback, *args) + loop.call_soon_threadsafe(callback, *args) callback_mgr = CallbackManager() diff --git a/electrum/wallet.py b/electrum/wallet.py index e882c9865..dade7ef8d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -295,6 +295,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): txin_type: str wallet_type: str lnworker: Optional['LNWallet'] + network: Optional['Network'] def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: SimpleConfig): @@ -2187,6 +2188,12 @@ def can_sign(self, tx: Transaction) -> bool: return True return False + def _get_rawtx_from_network(self, txid: str) -> str: + """legacy hack. do not use in new code.""" + assert self.network + return self.network.run_from_another_thread( + self.network.get_transaction(txid, timeout=10)) + def get_input_tx(self, tx_hash: str, *, ignore_network_issues=False) -> Optional[Transaction]: # First look up an input transaction in the wallet where it # will likely be. If co-signing a transaction it may not have @@ -2194,8 +2201,7 @@ def get_input_tx(self, tx_hash: str, *, ignore_network_issues=False) -> Optional tx = self.db.get_transaction(tx_hash) if not tx and self.network and self.network.has_internet_connection(): try: - raw_tx = self.network.run_from_another_thread( - self.network.get_transaction(tx_hash, timeout=10)) + raw_tx = self._get_rawtx_from_network(tx_hash) except NetworkException as e: _logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {tx_hash}. ' f'if you are intentionally offline, consider using the --offline flag') From 7aa1609718661cd78011aba467673270e584eecc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Feb 2023 16:47:12 +0000 Subject: [PATCH 0181/1143] tests: disable asyncio debug mode see https://bugs.python.org/issue38608 --- electrum/tests/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py index ed26800ff..218a0db67 100644 --- a/electrum/tests/__init__.py +++ b/electrum/tests/__init__.py @@ -1,4 +1,5 @@ import asyncio +import os import unittest import threading import tempfile @@ -48,6 +49,12 @@ def setUp(self): super().setUp() self.electrum_path = tempfile.mkdtemp() + async def asyncSetUp(self): + loop = util.get_asyncio_loop() + # IsolatedAsyncioTestCase creates event loops with debug=True, which makes the tests take ~4x time + if not (os.environ.get("PYTHONASYNCIODEBUG") or os.environ.get("PYTHONDEVMODE")): + loop.set_debug(False) + def tearDown(self): shutil.rmtree(self.electrum_path) super().tearDown() From 4e9ddf6dddd022813a3bdf09a166ee2e8f4fabfa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 21 Feb 2023 13:15:06 +0100 Subject: [PATCH 0182/1143] cosignerpool: minor fix, follow-up new GUI flow --- electrum/plugins/cosigner_pool/qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py index 05fab0bee..30fac677b 100644 --- a/electrum/plugins/cosigner_pool/qt.py +++ b/electrum/plugins/cosigner_pool/qt.py @@ -183,7 +183,7 @@ def hook_transaction_dialog(self, d: 'TxDialog'): def hook_transaction_dialog_update(self, d: 'TxDialog'): assert self.wallet == d.wallet - if not d.finalized or d.tx.is_complete() or d.wallet.can_sign(d.tx): + if d.tx.is_complete() or d.wallet.can_sign(d.tx): d.cosigner_send_button.setVisible(False) return for xpub, K, _hash in self.cosigner_list: From 2242a506a9a9dd71f719e81b57f15b82ff424369 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 22 Feb 2023 14:02:24 +0000 Subject: [PATCH 0183/1143] ledger: fix sign_transaction for Ypub / sh(wsh(multi())) wallets regression from https://github.com/spesmilo/electrum/pull/8041 --- electrum/plugins/ledger/ledger.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 2e42de81c..be1ca437f 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1095,15 +1095,20 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, if utxo is None: continue scriptcode = utxo.scriptPubKey - - p2sh = False if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh']: if len(psbt_in.redeem_script) == 0: continue scriptcode = psbt_in.redeem_script + elif electrum_txin.script_type in ['p2wsh', 'p2wsh-p2sh']: + if len(psbt_in.witness_script) == 0: + continue + scriptcode = psbt_in.witness_script + + p2sh = False + if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: p2sh = True - is_wit, wit_ver, __ = is_witness(scriptcode) + is_wit, wit_ver, __ = is_witness(psbt_in.redeem_script or utxo.scriptPubKey) script_addrtype = AddressType.LEGACY if is_wit: @@ -1123,12 +1128,6 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, else: continue - # Check if P2WSH - if electrum_txin.script_type in ['p2wsh']: - if len(psbt_in.witness_script) == 0: - continue - scriptcode = psbt_in.witness_script - multisig = parse_multisig(scriptcode) if multisig is not None: k, ms_pubkeys = multisig From 0af7f68dd8abbaa8991c4a8bf6578618fc111860 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 22 Feb 2023 14:05:27 +0000 Subject: [PATCH 0184/1143] qt tx dialog: fix "preview" for unsigned pre-segwit tx ``` 629.08 | E | gui.qt.exception_window.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File ".../electrum/electrum/gui/qt/invoice_list.py", line 170, in menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) File ".../electrum/electrum/gui/qt/send_tab.py", line 573, in do_pay_invoice self.pay_onchain_dialog(self.window.get_coins(), invoice.outputs) File ".../electrum/electrum/gui/qt/send_tab.py", line 251, in pay_onchain_dialog self.window.show_transaction(tx) File ".../electrum/electrum/gui/qt/main_window.py", line 1074, in show_transaction show_transaction(tx, parent=self, desc=tx_desc) File ".../electrum/electrum/gui/qt/transaction_dialog.py", line 351, in show_transaction d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved) File ".../electrum/electrum/gui/qt/transaction_dialog.py", line 450, in __init__ self.set_title() File ".../electrum/electrum/gui/qt/transaction_dialog.py", line 858, in set_title self.setWindowTitle(_("Transaction") + ' ' + self.tx.txid()) TypeError: can only concatenate str (not "NoneType") to str ``` --- electrum/gui/qt/transaction_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index bbf9db722..3d445b96d 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -855,7 +855,8 @@ def add_tx_stats(self, vbox): self.locktime_final_label.setVisible(True) def set_title(self): - self.setWindowTitle(_("Transaction") + ' ' + self.tx.txid()) + txid = self.tx.txid() or "" + self.setWindowTitle(_("Transaction") + ' ' + txid) def can_finalize(self) -> bool: return False From a56c9687c84b4f446e512c9895d65f408cd54de4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 22 Feb 2023 14:17:57 +0100 Subject: [PATCH 0185/1143] qml: initial async wallet load --- electrum/gui/qml/qedaemon.py | 49 +++++++++++++++++++++++++----------- electrum/gui/qml/qewallet.py | 18 ++++++++++++- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 04448c2d2..729d50369 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -1,4 +1,5 @@ import os +import threading from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject @@ -118,12 +119,14 @@ class QEDaemon(AuthMixin, QObject): _use_single_password = False _password = None + _backendWalletLoaded = pyqtSignal([str], arguments=['password']) + availableWalletsChanged = pyqtSignal() fxChanged = pyqtSignal() newWalletWizardChanged = pyqtSignal() serverConnectWizardChanged = pyqtSignal() - walletLoaded = pyqtSignal() + walletLoaded = pyqtSignal([str,str], arguments=['name','path']) walletRequiresPassword = pyqtSignal([str,str], arguments=['name','path']) walletOpenError = pyqtSignal([str], arguments=["error"]) walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message']) @@ -132,6 +135,9 @@ def __init__(self, daemon, parent=None): super().__init__(parent) self.daemon = daemon self.qefx = QEFX(daemon.fx, daemon.config) + + self._backendWalletLoaded.connect(self._on_backend_wallet_loaded) + self._walletdb = QEWalletDB() self._walletdb.validPasswordChanged.connect(self.passwordValidityCheck) @@ -171,14 +177,14 @@ def load_wallet(self, path=None, password=None): if not self._walletdb.ready: return - try: - wallet = self.daemon.load_wallet(self._path, password) - if wallet is not None: - self._current_wallet = QEWallet.getInstanceFor(wallet) - if not wallet_already_open: - self.availableWallets.updateWallet(self._path) - self._current_wallet.password = password - self.walletLoaded.emit() + def load_wallet_task(): + try: + wallet = self.daemon.load_wallet(self._path, password) + + if wallet is None: + self._logger.info('could not open wallet') + self.walletOpenError.emit('could not open wallet') + return if self.daemon.config.get('single_password'): self._use_single_password = self.daemon.update_password_for_directory(old_password=password, new_password=password) @@ -189,13 +195,26 @@ def load_wallet(self, path=None, password=None): self._logger.info('use single password disabled by config') self.daemon.config.save_last_wallet(wallet) + run_hook('load_wallet', wallet) - else: - self._logger.info('could not open wallet') - self.walletOpenError.emit('could not open wallet') - except WalletFileException as e: - self._logger.error(str(e)) - self.walletOpenError.emit(str(e)) + + self._backendWalletLoaded.emit(password) + except WalletFileException as e: + self._logger.error(str(e)) + self.walletOpenError.emit(str(e)) + + threading.Thread(target=load_wallet_task).start() + + @pyqtSlot() + @pyqtSlot(str) + def _on_backend_wallet_loaded(self, password = None): + self._logger.debug('_on_backend_wallet_loaded') + wallet = self.daemon._wallets[self._path] + self._current_wallet = QEWallet.getInstanceFor(wallet) + self.availableWallets.updateWallet(self._path) + self._current_wallet.password = password + self.walletLoaded.emit(self._name, self._path) + @pyqtSlot(QEWallet) @pyqtSlot(QEWallet, bool) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 31d615ec4..7794a070b 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional, Tuple from functools import partial -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, QMetaObject, Qt from electrum import bitcoin from electrum.i18n import _ @@ -113,6 +113,9 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.sync_progress_timer.setInterval(2000) self.sync_progress_timer.timeout.connect(self.update_sync_progress) + # post-construction init in GUI thread + # QMetaObject.invokeMethod(self, 'qt_init', Qt.QueuedConnection) + # To avoid leaking references to "self" that prevent the # window from being GC-ed when closed, callbacks should be # methods of this class only, and specifically not be @@ -123,6 +126,19 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.synchronizing = True # start in sync state + # @pyqtSlot() + # def qt_init(self): + # self.notification_timer = QTimer(self) + # self.notification_timer.setSingleShot(False) + # self.notification_timer.setInterval(500) # msec + # self.notification_timer.timeout.connect(self.notify_transactions) + # + # self.sync_progress_timer = QTimer(self) + # self.sync_progress_timer.setSingleShot(False) + # self.sync_progress_timer.setInterval(2000) + # self.sync_progress_timer.timeout.connect(self.update_sync_progress) + + @pyqtProperty(bool, notify=isUptodateChanged) def isUptodate(self): return self._isUpToDate From 278486602b025eb4a790abc4aa9f6f88205f751d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Feb 2023 18:20:29 +0100 Subject: [PATCH 0186/1143] qml: add loader overlay, avoid interacting with the to-be-unloaded wallet --- electrum/gui/qml/components/Wallets.qml | 12 +++--------- electrum/gui/qml/components/main.qml | 24 ++++++++++++++++++++++++ electrum/gui/qml/qedaemon.py | 14 +++++++++++++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 39955ac9c..5d69d039d 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -14,12 +14,13 @@ Pane { padding: 0 function createWallet() { - var dialog = app.newWalletWizard.createObject(rootItem) + var dialog = app.newWalletWizard.createObject(app) dialog.open() dialog.walletCreated.connect(function() { Daemon.availableWallets.reload() // and load the new wallet Daemon.load_wallet(dialog.path, dialog.wizard_data['password']) + app.stack.pop() }) } @@ -57,6 +58,7 @@ Pane { onClicked: { Daemon.load_wallet(model.path) + app.stack.pop() } RowLayout { @@ -118,12 +120,4 @@ Pane { } } - Connections { - target: Daemon - function onWalletLoaded() { - Daemon.availableWallets.reload() - app.stack.pop() - } - } - } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 47b9464e8..d0a90ee35 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -171,6 +171,30 @@ ApplicationWindow } } + Pane { + id: walletLoadingPane + parent: Overlay.overlay + anchors.fill: parent + background: Rectangle { color: Material.dialogColor } + visible: Daemon.loading + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 * constants.paddingXLarge + + Label { + Layout.alignment: Qt.AlignHCenter + text: qsTr('Opening wallet...') + font.pixelSize: constants.fontSizeXXLarge + } + + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + running: Daemon.loading + } + } + } + Timer { id: coverTimer interval: 10 diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 729d50369..f8c0a88e1 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -118,6 +118,7 @@ class QEDaemon(AuthMixin, QObject): _name = None _use_single_password = False _password = None + _loading = False _backendWalletLoaded = pyqtSignal([str], arguments=['password']) @@ -125,6 +126,7 @@ class QEDaemon(AuthMixin, QObject): fxChanged = pyqtSignal() newWalletWizardChanged = pyqtSignal() serverConnectWizardChanged = pyqtSignal() + loadingChanged = pyqtSignal() walletLoaded = pyqtSignal([str,str], arguments=['name','path']) walletRequiresPassword = pyqtSignal([str,str], arguments=['name','path']) @@ -178,6 +180,9 @@ def load_wallet(self, path=None, password=None): return def load_wallet_task(): + self._loading = True + self.loadingChanged.emit() + try: wallet = self.daemon.load_wallet(self._path, password) @@ -202,8 +207,11 @@ def load_wallet_task(): except WalletFileException as e: self._logger.error(str(e)) self.walletOpenError.emit(str(e)) + finally: + self._loading = False + self.loadingChanged.emit() - threading.Thread(target=load_wallet_task).start() + threading.Thread(target=load_wallet_task, daemon=True).start() @pyqtSlot() @pyqtSlot(str) @@ -252,6 +260,10 @@ def delete_wallet(self, wallet): self.availableWallets.remove_wallet(path) + @pyqtProperty(bool, notify=loadingChanged) + def loading(self): + return self._loading + @pyqtProperty(QEWallet, notify=walletLoaded) def currentWallet(self): return self._current_wallet From e511701c74506dcd9404472267ca1e8510d60cc8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Feb 2023 20:44:25 +0100 Subject: [PATCH 0187/1143] qml: ElDialog assure close behavior is consistent with allowClose property --- electrum/gui/qml/components/controls/ElDialog.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index dc4e8bc72..ed7865bc1 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -12,6 +12,10 @@ Dialog { close() } + closePolicy: allowClose + ? Popup.CloseOnEscape | Popup.CloseOnPressOutside + : Popup.NoAutoClose + onOpenedChanged: { if (opened) { app.activeDialogs.push(abstractdialog) @@ -48,6 +52,7 @@ Dialog { leftPadding: constants.paddingXLarge topPadding: constants.paddingXLarge bottomPadding: constants.paddingXLarge + rightPadding: constants.paddingXLarge font.bold: true font.pixelSize: constants.fontSizeMedium } From 32d00b298262271a09b4f0903806c6b512e52f59 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Feb 2023 20:46:56 +0100 Subject: [PATCH 0188/1143] qml: wallet loading indicator as modal dialog, unclosable --- .../qml/components/LoadingWalletDialog.qml | 41 +++++++++++++++++++ .../gui/qml/components/OpenWalletDialog.qml | 3 +- electrum/gui/qml/components/Wallets.qml | 15 +++++-- electrum/gui/qml/components/main.qml | 39 +++++++----------- electrum/gui/qml/qewallet.py | 13 ------ 5 files changed, 69 insertions(+), 42 deletions(-) create mode 100644 electrum/gui/qml/components/LoadingWalletDialog.qml diff --git a/electrum/gui/qml/components/LoadingWalletDialog.qml b/electrum/gui/qml/components/LoadingWalletDialog.qml new file mode 100644 index 000000000..4d0595e1e --- /dev/null +++ b/electrum/gui/qml/components/LoadingWalletDialog.qml @@ -0,0 +1,41 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +ElDialog { + id: dialog + + title: qsTr('Loading Wallet') + iconSource: Qt.resolvedUrl('../../icons/wallet.png') + + modal: true + parent: Overlay.overlay + Overlay.modal: Rectangle { + color: "#aa000000" + } + + x: Math.floor((parent.width - implicitWidth) / 2) + y: Math.floor((parent.height - implicitHeight) / 2) + // anchors.centerIn: parent // this strangely pixelates the spinner + + ColumnLayout { + width: parent.width + + BusyIndicator { + Layout.alignment: Qt.AlignHCenter + + running: Daemon.loading + } + } + + Connections { + target: Daemon + function onLoadingChanged() { + if (!Daemon.loading) + dialog.close() + } + } +} diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index 3c9424646..0d58565d0 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -45,7 +45,7 @@ ElDialog { InfoTextArea { id: notice text: qsTr("Wallet %1 requires password to unlock").arg(name) - // visible: false //wallet_db.needsPassword + visible: wallet_db.needsPassword iconStyle: InfoTextArea.IconStyle.Warn Layout.fillWidth: true } @@ -96,7 +96,6 @@ ElDialog { FlatButton { id: unlockButton - // Layout.alignment: Qt.AlignHCenter Layout.fillWidth: true visible: wallet_db.needsPassword icon.source: '../../icons/unlock.png' diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 5d69d039d..e9d384265 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -20,7 +20,6 @@ Pane { Daemon.availableWallets.reload() // and load the new wallet Daemon.load_wallet(dialog.path, dialog.wizard_data['password']) - app.stack.pop() }) } @@ -57,8 +56,10 @@ Pane { height: row.height onClicked: { - Daemon.load_wallet(model.path) - app.stack.pop() + if (Daemon.currentWallet.name != model.name) + Daemon.load_wallet(model.path) + else + app.stack.pop() } RowLayout { @@ -120,4 +121,12 @@ Pane { } } + Connections { + target: Daemon + function onWalletLoaded() { + if (app.stack.currentItem.objectName == 'Wallets') + app.stack.pop() + } + } + } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index d0a90ee35..484ec7209 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -171,30 +171,6 @@ ApplicationWindow } } - Pane { - id: walletLoadingPane - parent: Overlay.overlay - anchors.fill: parent - background: Rectangle { color: Material.dialogColor } - visible: Daemon.loading - - ColumnLayout { - anchors.centerIn: parent - spacing: 2 * constants.paddingXLarge - - Label { - Layout.alignment: Qt.AlignHCenter - text: qsTr('Opening wallet...') - font.pixelSize: constants.fontSizeXXLarge - } - - BusyIndicator { - Layout.alignment: Qt.AlignHCenter - running: Daemon.loading - } - } - } - Timer { id: coverTimer interval: 10 @@ -283,6 +259,14 @@ ApplicationWindow } } + property alias loadingWalletDialog: _loadingWalletDialog + Component { + id: _loadingWalletDialog + LoadingWalletDialog { + onClosed: destroy() + } + } + property alias channelOpenProgressDialog: _channelOpenProgressDialog ChannelOpenProgressDialog { id: _channelOpenProgressDialog @@ -409,6 +393,13 @@ ApplicationWindow function onAuthRequired(method) { handleAuthRequired(Daemon, method) } + function onLoadingChanged() { + if (!Daemon.loading) + return + console.log('wallet loading') + var dialog = loadingWalletDialog.createObject(app, { allowClose: false } ) + dialog.open() + } } Connections { diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 7794a070b..e6f2f1b64 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -126,19 +126,6 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.synchronizing = True # start in sync state - # @pyqtSlot() - # def qt_init(self): - # self.notification_timer = QTimer(self) - # self.notification_timer.setSingleShot(False) - # self.notification_timer.setInterval(500) # msec - # self.notification_timer.timeout.connect(self.notify_transactions) - # - # self.sync_progress_timer = QTimer(self) - # self.sync_progress_timer.setSingleShot(False) - # self.sync_progress_timer.setInterval(2000) - # self.sync_progress_timer.timeout.connect(self.update_sync_progress) - - @pyqtProperty(bool, notify=isUptodateChanged) def isUptodate(self): return self._isUpToDate From 9d425b5b236cee724c399d76e7ad086d895fc78d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Feb 2023 20:57:45 +0100 Subject: [PATCH 0189/1143] qml: move remaining buttons to bottom of dialogs --- .../gui/qml/components/ExportTxDialog.qml | 102 +++++++-------- .../gui/qml/components/GenericShareDialog.qml | 119 ++++++++++-------- .../qml/components/LoadingWalletDialog.qml | 2 + electrum/gui/qml/components/ReceiveDialog.qml | 98 ++++++++------- .../gui/qml/components/controls/Toaster.qml | 2 +- 5 files changed, 172 insertions(+), 151 deletions(-) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 4e4d3e35b..72d44fb9b 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -27,63 +27,67 @@ ElDialog { color: "#aa000000" } - Flickable { - anchors.fill: parent - contentHeight: rootLayout.height - clip:true - interactive: height < contentHeight + padding: 0 - ColumnLayout { - id: rootLayout - width: parent.width - spacing: constants.paddingMedium + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + + contentHeight: rootLayout.height + clip:true + interactive: height < contentHeight + + ColumnLayout { + id: rootLayout + width: parent.width + spacing: constants.paddingMedium + + Item { + Layout.fillWidth: true + Layout.preferredHeight: qr.height + Layout.topMargin: constants.paddingSmall + Layout.bottomMargin: constants.paddingSmall + QRImage { + id: qr + qrdata: dialog.text_qr + anchors.centerIn: parent + } + } - Item { - Layout.fillWidth: true - Layout.preferredHeight: qr.height - Layout.topMargin: constants.paddingSmall - Layout.bottomMargin: constants.paddingSmall - QRImage { - id: qr - qrdata: dialog.text_qr - anchors.centerIn: parent + Label { + visible: dialog.text_help + text: dialog.text_help + wrapMode: Text.Wrap + Layout.fillWidth: true } - } - Label { - visible: dialog.text_help - text: dialog.text_help - wrapMode: Text.Wrap - Layout.fillWidth: true } + } - Rectangle { - height: 1 - Layout.preferredWidth: qr.width - Layout.alignment: Qt.AlignHCenter - color: Material.accentColor - } + ButtonContainer { + Layout.fillWidth: true - ButtonContainer { - // Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - text: qsTr('Copy') - icon.source: '../../icons/copy_bw.png' - onClicked: { - AppController.textToClipboard(dialog.text) - toaster.show(this, qsTr('Copied!')) - } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Copy') + icon.source: '../../icons/copy_bw.png' + onClicked: { + AppController.textToClipboard(dialog.text) + toaster.show(this, qsTr('Copied!')) } - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - text: qsTr('Share') - icon.source: '../../icons/share.png' - onClicked: { - AppController.doShare(dialog.text, dialog.title) - } + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Share') + icon.source: '../../icons/share.png' + onClicked: { + AppController.doShare(dialog.text, dialog.title) } } } diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index 1702f433d..f533e1cbe 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -25,72 +25,81 @@ ElDialog { color: "#aa000000" } - Flickable { + padding: 0 + + ColumnLayout { anchors.fill: parent - contentHeight: rootLayout.height - clip:true - interactive: height < contentHeight - - ColumnLayout { - id: rootLayout - width: parent.width - spacing: constants.paddingMedium - - QRImage { - id: qr - render: dialog.enter ? false : true - qrdata: dialog.text_qr ? dialog.text_qr : dialog.text - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: constants.paddingSmall - Layout.bottomMargin: constants.paddingSmall - } + spacing: 0 + + Flickable { + Layout.fillHeight: true + Layout.fillWidth: true + + contentHeight: rootLayout.height + clip:true + interactive: height < contentHeight + + ColumnLayout { + id: rootLayout + width: parent.width + spacing: constants.paddingMedium + + QRImage { + id: qr + render: dialog.enter ? false : true + qrdata: dialog.text_qr ? dialog.text_qr : dialog.text + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingSmall + Layout.bottomMargin: constants.paddingSmall + } + + TextHighlightPane { + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.maximumWidth: qr.width + Label { + width: parent.width + text: dialog.text + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + maximumLineCount: 4 + elide: Text.ElideRight + } + } - TextHighlightPane { - Layout.fillWidth: true Label { - width: parent.width - text: dialog.text + visible: dialog.text_help + text: dialog.text_help wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - maximumLineCount: 4 - elide: Text.ElideRight + Layout.fillWidth: true } - } - Label { - visible: dialog.text_help - text: dialog.text_help - wrapMode: Text.Wrap - Layout.fillWidth: true } + } - Rectangle { - height: 1 - Layout.preferredWidth: qr.width - Layout.alignment: Qt.AlignHCenter - color: Material.accentColor - } + ButtonContainer { + Layout.fillWidth: true - ButtonContainer { - Layout.alignment: Qt.AlignHCenter + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - text: qsTr('Copy') - icon.source: '../../icons/copy_bw.png' - onClicked: { - AppController.textToClipboard(dialog.text) - toaster.show(this, qsTr('Copied!')) - } + text: qsTr('Copy') + icon.source: '../../icons/copy_bw.png' + onClicked: { + AppController.textToClipboard(dialog.text) + toaster.show(this, qsTr('Copied!')) } - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - text: qsTr('Share') - icon.source: '../../icons/share.png' - onClicked: { - AppController.doShare(dialog.text, dialog.title) - } + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + text: qsTr('Share') + icon.source: '../../icons/share.png' + onClicked: { + AppController.doShare(dialog.text, dialog.title) } } } diff --git a/electrum/gui/qml/components/LoadingWalletDialog.qml b/electrum/gui/qml/components/LoadingWalletDialog.qml index 4d0595e1e..a9f938350 100644 --- a/electrum/gui/qml/components/LoadingWalletDialog.qml +++ b/electrum/gui/qml/components/LoadingWalletDialog.qml @@ -5,6 +5,8 @@ import QtQuick.Controls.Material 2.0 import org.electrum 1.0 +import "controls" + ElDialog { id: dialog diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 31cc4c083..79f8daca7 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -73,6 +73,7 @@ ElDialog { ] Rectangle { + id: qrbg Layout.alignment: Qt.AlignHCenter Layout.topMargin: constants.paddingSmall Layout.bottomMargin: constants.paddingSmall @@ -201,14 +202,13 @@ ElDialog { Rectangle { height: 1 Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: buttons.width + Layout.preferredWidth: qrbg.width color: Material.accentColor } GridLayout { columns: 2 - // visible: request.message || !request.amount.isEmpty - Layout.maximumWidth: buttons.width + Layout.maximumWidth: qrbg.width Layout.alignment: Qt.AlignHCenter Label { @@ -241,57 +241,63 @@ ElDialog { } Rectangle { - // visible: request.message || !request.amount.isEmpty height: 1 Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: buttons.width + Layout.preferredWidth: qrbg.width color: Material.accentColor } - ButtonContainer { - id: buttons - Layout.alignment: Qt.AlignHCenter - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - icon.source: '../../icons/copy_bw.png' - icon.color: 'transparent' - text: 'Copy' - onClicked: { - if (request.isLightning && rootLayout.state == 'bolt11') - AppController.textToClipboard(_bolt11) - else if (rootLayout.state == 'bip21uri') - AppController.textToClipboard(_bip21uri) - else - AppController.textToClipboard(_address) - toaster.show(this, qsTr('Copied!')) - } - } - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - icon.source: '../../icons/share.png' - text: 'Share' - onClicked: { - enabled = false - if (request.isLightning && rootLayout.state == 'bolt11') - AppController.doShare(_bolt11, qsTr('Payment Request')) - else if (rootLayout.state == 'bip21uri') - AppController.doShare(_bip21uri, qsTr('Payment Request')) - else - AppController.doShare(_address, qsTr('Onchain address')) - - enabled = true - } - } - FlatButton { - Layout.minimumWidth: dialog.width * 1/4 - Layout.alignment: Qt.AlignHCenter - icon.source: '../../icons/pen.png' - text: qsTr('Edit') - onClicked: receiveDetailsDialog.open() - } + } + + } + + ButtonContainer { + id: buttons + Layout.fillWidth: true + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + icon.source: '../../icons/copy_bw.png' + icon.color: 'transparent' + text: 'Copy' + onClicked: { + if (request.isLightning && rootLayout.state == 'bolt11') + AppController.textToClipboard(_bolt11) + else if (rootLayout.state == 'bip21uri') + AppController.textToClipboard(_bip21uri) + else + AppController.textToClipboard(_address) + toaster.show(this, qsTr('Copied!')) } } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + icon.source: '../../icons/share.png' + text: 'Share' + onClicked: { + enabled = false + if (request.isLightning && rootLayout.state == 'bolt11') + AppController.doShare(_bolt11, qsTr('Payment Request')) + else if (rootLayout.state == 'bip21uri') + AppController.doShare(_bip21uri, qsTr('Payment Request')) + else + AppController.doShare(_address, qsTr('Onchain address')) + + enabled = true + } + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + icon.source: '../../icons/pen.png' + text: qsTr('Edit') + onClicked: receiveDetailsDialog.open() + } } } diff --git a/electrum/gui/qml/components/controls/Toaster.qml b/electrum/gui/qml/components/controls/Toaster.qml index 8255187c5..c8ec99113 100644 --- a/electrum/gui/qml/components/controls/Toaster.qml +++ b/electrum/gui/qml/components/controls/Toaster.qml @@ -17,7 +17,7 @@ Item { function show(item, text) { _text = text var r = item.mapToItem(parent, item.x, item.y) - x = r.x + x = r.x - (toaster.width - item.width)/2 y = r.y - toaster.height - constants.paddingLarge toaster._y = y - 35 ani.restart() From e589d859ae06d92aeabd98965e9ffd7256e86ca2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Feb 2023 21:26:56 +0100 Subject: [PATCH 0190/1143] qml: reset position in history to top when loading another wallet --- electrum/gui/qml/components/History.qml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index e47345666..f989480df 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -143,4 +143,11 @@ Pane { Daemon.currentWallet.historyModel.updateBlockchainHeight(height) } } + + Connections { + target: Daemon + function onWalletLoaded() { + listview.positionViewAtBeginning() + } + } } From 7fe5282f7cc4d294545c3e09f9fe35da3b8cab12 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Feb 2023 21:49:23 +0100 Subject: [PATCH 0191/1143] qml: hamburger styling/menu position --- electrum/gui/qml/components/WalletMainView.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 569e17e43..8b913af7d 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -102,6 +102,7 @@ Item { ColumnLayout { anchors.fill: parent + spacing: 0 History { id: history @@ -155,22 +156,26 @@ Item { } ButtonContainer { + id: buttonContainer Layout.fillWidth: true FlatButton { Layout.fillWidth: false Layout.preferredWidth: implicitHeight + Layout.preferredHeight: receiveButton.implicitHeight + icon.source: '../../icons/hamburger.png' icon.height: constants.iconSizeSmall icon.width: constants.iconSizeSmall onClicked: { mainView.menu.open() - mainView.menu.y = mainView.height - mainView.menu.height + mainView.menu.y = mainView.height + app.header.height - mainView.menu.height - buttonContainer.height } } FlatButton { + id: receiveButton visible: Daemon.currentWallet Layout.fillWidth: true Layout.preferredWidth: 1 From 77fe2e642158f4e4d6fdcf033f329642736be187 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 25 Feb 2023 11:07:23 +0100 Subject: [PATCH 0192/1143] Qt tx dialog: rename Save and Export actions --- electrum/gui/qt/transaction_dialog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 3d445b96d..9399ecd2f 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -398,7 +398,7 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if self.broadcast_button = b = QPushButton(_("Broadcast")) b.clicked.connect(self.do_broadcast) - self.save_button = b = QPushButton(_("Save")) + self.save_button = b = QPushButton(_("Add to History")) b.clicked.connect(self.save) self.cancel_button = b = QPushButton(_("Close")) @@ -416,7 +416,7 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if self.psbt_only_widgets.append(export_submenu) self.export_actions_button = QToolButton() - self.export_actions_button.setText(_("Export")) + self.export_actions_button.setText(_("Share")) self.export_actions_button.setMenu(export_actions_menu) self.export_actions_button.setPopupMode(QToolButton.InstantPopup) @@ -505,7 +505,7 @@ def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transac action.triggered.connect(lambda: self.show_qr(tx=gettx())) menu.addAction(action) - action = QAction(_("Export to file"), self) + action = QAction(_("Save to file"), self) action.triggered.connect(lambda: self.export_to_file(tx=gettx())) menu.addAction(action) @@ -784,9 +784,9 @@ def update(self): self.save_button.setEnabled(tx_details.can_save_as_local) if tx_details.can_save_as_local: - self.save_button.setToolTip(_("Save transaction offline")) + self.save_button.setToolTip(_("Add transaction to history, without broadcasting it")) else: - self.save_button.setToolTip(_("Transaction already saved or not yet signed.")) + self.save_button.setToolTip(_("Transaction already in history or not yet signed.")) run_hook('transaction_dialog_update', self) From e4273e5ab981875231f6971e9702929cd3ca9956 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 6 Jan 2023 17:24:30 +0100 Subject: [PATCH 0193/1143] utxo privacy analysis: - add a new event, 'adb_removed_tx' - new wallet method: get_tx_parents - number of parents is shown in coins tab - detailed list of parents is shown in dialog --- electrum/address_synchronizer.py | 3 +- electrum/gui/qt/main_window.py | 5 ++ electrum/gui/qt/utxo_dialog.py | 113 +++++++++++++++++++++++++++++++ electrum/gui/qt/utxo_list.py | 17 +++-- electrum/wallet.py | 57 ++++++++++++++-- 5 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 electrum/gui/qt/utxo_dialog.py diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index ccc9606dd..76a0255e2 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -349,7 +349,7 @@ def add_value_from_prev_output(): self.db.add_transaction(tx_hash, tx) self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs())) if is_new: - util.trigger_callback('adb_added_tx', self, tx_hash) + util.trigger_callback('adb_added_tx', self, tx_hash, tx) return True def remove_transaction(self, tx_hash: str) -> None: @@ -401,6 +401,7 @@ def remove_from_spent_outpoints(): scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey.hex()) prevout = TxOutpoint(bfh(tx_hash), idx) self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value) + util.trigger_callback('adb_removed_tx', self, tx_hash, tx) def get_depending_transactions(self, tx_hash: str) -> Set[str]: """Returns all (grand-)children of tx_hash in this wallet.""" diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index dedb4f5c0..18590f936 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1065,6 +1065,11 @@ def show_address(self, addr: str, *, parent: QWidget = None): d = address_dialog.AddressDialog(self, addr, parent=parent) d.exec_() + def show_utxo(self, utxo): + from . import utxo_dialog + d = utxo_dialog.UTXODialog(self, utxo) + d.exec_() + def show_channel_details(self, chan): from .channel_details import ChannelDetailsDialog ChannelDetailsDialog(self, chan).show() diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py new file mode 100644 index 000000000..495197459 --- /dev/null +++ b/electrum/gui/qt/utxo_dialog.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import TYPE_CHECKING + +from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtGui import QTextCharFormat, QFont +from PyQt5.QtWidgets import QVBoxLayout, QLabel, QTextBrowser + +from electrum.i18n import _ + +from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel +from .history_list import HistoryList, HistoryModel +from .qrtextedit import ShowQRTextEdit + +if TYPE_CHECKING: + from .main_window import ElectrumWindow + +# todo: +# - edit label in tx detail window + + +class UTXODialog(WindowModalDialog): + + def __init__(self, window: 'ElectrumWindow', utxo): + WindowModalDialog.__init__(self, window, _("Coin Privacy Analysis")) + self.main_window = window + self.config = window.config + self.wallet = window.wallet + self.utxo = utxo + + txid = self.utxo.prevout.txid.hex() + parents = self.wallet.get_tx_parents(txid) + out = [] + for _txid, _list in sorted(parents.items()): + tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) + label = self.wallet.get_label_for_txid(_txid) or "" + out.append((tx_height, tx_pos, _txid, label, _list)) + + self.parents_list = QTextBrowser() + self.parents_list.setOpenLinks(False) # disable automatic link opening + self.parents_list.anchorClicked.connect(self.open_tx) # send links to our handler + self.parents_list.setFont(QFont(MONOSPACE_FONT)) + self.parents_list.setReadOnly(True) + self.parents_list.setTextInteractionFlags(self.parents_list.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) + self.parents_list.setMinimumWidth(900) + self.parents_list.setMinimumHeight(400) + self.parents_list.setLineWrapMode(QTextBrowser.NoWrap) + + cursor = self.parents_list.textCursor() + ext = QTextCharFormat() + + for tx_height, tx_pos, _txid, label, _list in reversed(sorted(out)): + key = "%dx%d"%(tx_height, tx_pos) if tx_pos >= 0 else _txid[0:8] + list_str = ','.join(filter(None, _list)) + lnk = QTextCharFormat() + lnk.setToolTip(_('Click to open, right-click for menu')) + lnk.setAnchorHref(_txid) + #lnk.setAnchorNames([a_name]) + lnk.setAnchor(True) + lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) + cursor.insertText(key, lnk) + cursor.insertText("\t", ext) + cursor.insertText("%-32s\t<- "%label[0:32], ext) + cursor.insertText(list_str, ext) + cursor.insertBlock() + + vbox = QVBoxLayout() + vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) + vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) + vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(len(parents)))) + vbox.addWidget(self.parents_list) + msg = ' '.join([ + _("Note: This analysis only shows parent transactions, and does not take address reuse into consideration."), + _("If you reuse addresses, more links can be established between your transactions, that are not displayed here.") + ]) + vbox.addWidget(WWLabel(msg)) + vbox.addLayout(Buttons(CloseButton(self))) + self.setLayout(vbox) + # set cursor to top + cursor.setPosition(0) + self.parents_list.setTextCursor(cursor) + + def open_tx(self, txid): + if isinstance(txid, QUrl): + txid = txid.toString(QUrl.None_) + tx = self.wallet.adb.get_transaction(txid) + if not tx: + return + label = self.wallet.get_label_for_txid(txid) + self.main_window.show_transaction(tx, tx_desc=label) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index ba6415e17..f605f6077 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -49,10 +49,12 @@ class Columns(IntEnum): ADDRESS = 1 LABEL = 2 AMOUNT = 3 + PARENTS = 4 headers = { Columns.OUTPOINT: _('Output point'), Columns.ADDRESS: _('Address'), + Columns.PARENTS: _('Parents'), Columns.LABEL: _('Label'), Columns.AMOUNT: _('Amount'), } @@ -87,14 +89,15 @@ def update(self): name = utxo.prevout.to_str() self._utxo_dict[name] = utxo address = utxo.address - amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True) - labels = [str(utxo.short_id), address, '', amount] + amount_str = self.parent.format_amount(utxo.value_sats(), whitespaces=True) + labels = [str(utxo.short_id), address, '', amount_str, ''] utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR) utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) + utxo_item[self.Columns.PARENTS].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.OUTPOINT].setFont(QFont(MONOSPACE_FONT)) self.model().insertRow(idx, utxo_item) self.refresh_row(name, idx) @@ -117,8 +120,10 @@ def refresh_row(self, key, row): assert row is not None utxo = self._utxo_dict[key] utxo_item = [self.std_model.item(row, col) for col in self.Columns] - address = utxo.address - label = self.wallet.get_label_for_txid(utxo.prevout.txid.hex()) or self.wallet.get_label_for_address(address) + txid = utxo.prevout.txid.hex() + parents = self.wallet.get_tx_parents(txid) + utxo_item[self.Columns.PARENTS].setText('%6s'%len(parents)) + label = self.wallet.get_label_for_txid(txid) or '' utxo_item[self.Columns.LABEL].setText(label) SELECTED_TO_SPEND_TOOLTIP = _('Coin selected to be spent') if key in self._spend_set: @@ -130,7 +135,7 @@ def refresh_row(self, key, row): for col in utxo_item: col.setBackground(color) col.setToolTip(tooltip) - if self.wallet.is_frozen_address(address): + if self.wallet.is_frozen_address(utxo.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(utxo): @@ -257,7 +262,7 @@ def create_menu(self, position): tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) - menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label)) + menu.addAction(_("View parents"), lambda: self.parent.show_utxo(utxo)) # fully spend menu_spend = menu.addMenu(_("Fully spend") + '…') m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) diff --git a/electrum/wallet.py b/electrum/wallet.py index dade7ef8d..c2812d2b8 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -317,6 +317,8 @@ def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: Si self.adb.add_address(addr) self.lock = self.adb.lock self.transaction_lock = self.adb.transaction_lock + self._last_full_history = None + self._tx_parents_cache = {} self.taskgroup = OldTaskGroup() @@ -453,6 +455,16 @@ async def stop(self): def is_up_to_date(self) -> bool: return self._up_to_date + def tx_is_related(self, tx): + is_mine = any([self.is_mine(out.address) for out in tx.outputs()]) + is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]) + return is_mine + + def clear_tx_parents_cache(self): + with self.lock, self.transaction_lock: + self._tx_parents_cache.clear() + self._last_full_history = None + @event_listener async def on_event_adb_set_up_to_date(self, adb): if self.adb != adb: @@ -473,21 +485,25 @@ async def on_event_adb_set_up_to_date(self, adb): self.logger.info(f'set_up_to_date: {up_to_date}') @event_listener - def on_event_adb_added_tx(self, adb, tx_hash): + def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction): if self.adb != adb: return - tx = self.db.get_transaction(tx_hash) - if not tx: - raise Exception(tx_hash) - is_mine = any([self.is_mine(out.address) for out in tx.outputs()]) - is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]) - if not is_mine: + if not self.tx_is_related(tx): return + self.clear_tx_parents_cache() if self.lnworker: self.lnworker.maybe_add_backup_from_tx(tx) self._update_invoices_and_reqs_touched_by_tx(tx_hash) util.trigger_callback('new_transaction', self, tx) + @event_listener + def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction): + if self.adb != adb: + return + if not self.tx_is_related(tx): + return + self.clear_tx_parents_cache() + @event_listener def on_event_adb_added_verified_tx(self, adb, tx_hash): if adb != self.adb: @@ -845,6 +861,33 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: is_lightning_funding_tx=is_lightning_funding_tx, ) + def get_tx_parents(self, txid) -> Dict: + """ + recursively calls itself and returns a flat dict: + txid -> input_index -> prevout + note: this does not take into account address reuse + """ + if not self.is_up_to_date(): + return {} + if self._last_full_history is None: + self._last_full_history = self.get_full_history(None) + + with self.lock, self.transaction_lock: + result = self._tx_parents_cache.get(txid, None) + if result is not None: + return result + result = {} + parents = [] + tx = self.adb.get_transaction(txid) + for i, txin in enumerate(tx.inputs()): + parents.append(str(txin.short_id)) + _txid = txin.prevout.txid.hex() + if _txid in self._last_full_history.keys(): + result.update(self.get_tx_parents(_txid)) + result[txid] = parents + self._tx_parents_cache[txid] = result + return result + def get_balance(self, **kwargs): domain = self.get_addresses() return self.adb.get_balance(domain, **kwargs) From 72fb43f950815ef7a655c39f47610c835ac155d0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 25 Feb 2023 12:23:34 +0100 Subject: [PATCH 0194/1143] lnworker: do not assume MPP in num_sats_can_receive --- electrum/lnworker.py | 23 +++++------------------ electrum/submarine_swaps.py | 7 +------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index f83166df3..6dc9b32c6 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2161,11 +2161,9 @@ def get_channels_for_receiving(self, amount_msat=None) -> Sequence[Channel]: return channels def num_sats_can_receive(self, deltas=None) -> Decimal: - """Return a conservative estimate of max sat value we can realistically receive - in a single payment. (MPP is allowed) - - The theoretical max would be `sum(chan.available_to_spend(REMOTE) for chan in self.channels)`, - but that would require a sender using MPP to magically guess all our channel liquidities. + """ + We no longer assume the sender to send MPP on different channels, + because channel liquidities are hard to guess """ if deltas is None: deltas = {} @@ -2182,12 +2180,10 @@ def recv_capacity(chan): recv_chan_msats = [recv_capacity(chan) for chan in recv_channels] if not recv_chan_msats: return Decimal(0) - can_receive_msat = max( - max(recv_chan_msats), # single-part payment baseline - sum(recv_chan_msats) // 2, # heuristic for MPP - ) + can_receive_msat = max(recv_chan_msats) return Decimal(can_receive_msat) / 1000 + def _suggest_channels_for_rebalance(self, direction, amount_sat) -> Sequence[Tuple[Channel, int]]: """ Suggest a channel and amount to send/receive with that channel, so that we will be able to receive/send amount_sat @@ -2297,15 +2293,6 @@ async def rebalance_channels(self, chan1, chan2, amount_msat): return await self.pay_invoice( invoice, channels=[chan1]) - def num_sats_can_receive_no_mpp(self) -> Decimal: - with self.lock: - channels = [ - c for c in self.channels.values() - if c.is_active() and not c.is_frozen_for_receiving() - ] - can_receive = max([c.available_to_spend(REMOTE) for c in channels]) if channels else 0 - return Decimal(can_receive) / 1000 - def can_receive_invoice(self, invoice: Invoice) -> bool: assert invoice.is_lightning() return (invoice.get_amount_sat() or 0) <= self.num_sats_can_receive() diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index cd62f98d8..55defd265 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -241,11 +241,6 @@ def add_lnwatcher_callback(self, swap: SwapData) -> None: callback = lambda: self._claim_swap(swap) self.lnwatcher.add_callback(swap.lockup_address, callback) - def num_sats_can_receive(self): - # finding how to do MPP is too hard for sender, - # might result in our coins being locked - return self.lnworker.num_sats_can_receive_no_mpp() - async def normal_swap( self, *, @@ -678,7 +673,7 @@ def _create_and_sign_claim_tx( def max_amount_forward_swap(self) -> Optional[int]: """ returns None if we cannot swap """ max_swap_amt_ln = self.get_max_amount() - max_recv_amt_ln = int(self.num_sats_can_receive()) + max_recv_amt_ln = int(self.lnworker.num_sats_can_receive()) max_amt_ln = int(min(max_swap_amt_ln, max_recv_amt_ln)) max_amt_oc = self.get_send_amount(max_amt_ln, is_reverse=False) or 0 min_amt_oc = self.get_send_amount(self.get_min_amount(), is_reverse=False) or 0 From 5ee91594d3ac7baef959fd97ed90f6652a6335b4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 24 Feb 2023 11:15:12 +0100 Subject: [PATCH 0195/1143] qml: replace Enter manually option in SendDialog with Invoices, which is removed from main menu --- electrum/gui/qml/components/SendDialog.qml | 56 ++----------------- .../gui/qml/components/WalletMainView.qml | 9 --- 2 files changed, 5 insertions(+), 60 deletions(-) diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index 56e4457a0..11df6b8f6 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -55,14 +55,12 @@ ElDialog { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 - icon.source: '../../icons/pen.png' - text: qsTr('Manual input') + icon.source: '../../icons/tab_receive.png' + text: qsTr('Invoices') + enabled: Daemon.currentWallet.invoiceModel.rowCount() // TODO: only count non-expired onClicked: { - var _mid = manualInputDialog.createObject(mainView) - _mid.accepted.connect(function() { - dialog.dispatch(_mid.recipient) - }) - _mid.open() + dialog.close() + app.stack.push(Qt.resolvedUrl('Invoices.qml')) } } @@ -77,50 +75,6 @@ ElDialog { } - Component { - id: manualInputDialog - ElDialog { - property alias recipient: recipientTextEdit.text - - iconSource: Qt.resolvedUrl('../../icons/pen.png') - - anchors.centerIn: parent - implicitWidth: parent.width * 0.9 - - parent: Overlay.overlay - modal: true - - Overlay.modal: Rectangle { - color: "#aa000000" - } - - title: qsTr('Manual Input') - - ColumnLayout { - width: parent.width - - Label { - text: 'Enter a bitcoin address or a Lightning invoice' - wrapMode: Text.Wrap - Layout.maximumWidth: parent.width - } - - TextField { - id: recipientTextEdit - topPadding: constants.paddingXXLarge - bottomPadding: constants.paddingXXLarge - Layout.preferredWidth: parent.width - font.family: FixedFont - - wrapMode: TextInput.WrapAnywhere - placeholderText: qsTr('Enter the payment request here') - } - } - - onClosed: destroy() - } - } - Bitcoin { id: bitcoin } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 8b913af7d..72a4df14b 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -48,15 +48,6 @@ Item { } id: menu - MenuItem { - icon.color: 'transparent' - action: Action { - text: qsTr('Invoices'); - onTriggered: menu.openPage(Qt.resolvedUrl('Invoices.qml')) - enabled: Daemon.currentWallet - icon.source: '../../icons/tab_receive.png' - } - } MenuItem { icon.color: 'transparent' action: Action { From 3a90f3588893c0592ee7a5c45b9c9283fad2adc0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 24 Feb 2023 12:11:54 +0100 Subject: [PATCH 0196/1143] qml: reintroduce receive requests list page --- electrum/gui/qml/components/ReceiveDialog.qml | 11 +++ .../gui/qml/components/ReceiveRequests.qml | 69 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 electrum/gui/qml/components/ReceiveRequests.qml diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 79f8daca7..ac666c58a 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -255,6 +255,17 @@ ElDialog { id: buttons Layout.fillWidth: true + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + icon.source: '../../icons/tab_receive.png' + text: qsTr('Requests') + onClicked: { + dialog.close() + app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) + } + } FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml new file mode 100644 index 000000000..b4d285b4f --- /dev/null +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -0,0 +1,69 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 +import QtQml.Models 2.1 +import QtQml 2.6 + +import "controls" + +Pane { + id: root + + ColumnLayout { + anchors.fill: parent + + Heading { + text: qsTr('Receive requests') + } + + Frame { + background: PaneInsetBackground {} + + verticalPadding: 0 + horizontalPadding: 0 + Layout.fillHeight: true + Layout.fillWidth: true + + ListView { + id: listview + anchors.fill: parent + clip: true + + model: DelegateModel { + id: delegateModel + model: Daemon.currentWallet.requestModel + delegate: InvoiceDelegate { + onClicked: { + //var dialog = app.stack.getRoot().openInvoice(model.key) + // dialog.invoiceAmountChanged.connect(function () { + // Daemon.currentWallet.invoiceModel.init_model() + // }) + } + } + } + + add: Transition { + NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 } + NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 } + } + addDisplaced: Transition { + SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 } + } + + remove: Transition { + NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } + NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: 200 } + SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } + } + } + + ScrollIndicator.vertical: ScrollIndicator { } + } + } + } +} From f12fe4af4d75befa74cf3d3b8b42fe5500a95cd7 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 24 Feb 2023 14:47:28 +0100 Subject: [PATCH 0197/1143] qml: add option to open ReceiveDialog with existing request key --- electrum/gui/qml/components/ReceiveDialog.qml | 10 ++++++++-- electrum/gui/qml/components/ReceiveRequests.qml | 10 ++++++---- electrum/gui/qml/components/WalletMainView.qml | 6 ++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index ac666c58a..52cc2a87b 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -13,6 +13,8 @@ ElDialog { title: qsTr('Receive Payment') + property string key + property string _bolt11: request.bolt11 property string _bip21uri: request.bip21 property string _address: request.address @@ -441,8 +443,12 @@ ElDialog { } Component.onCompleted: { - // callLater to make sure any popups are on top of the dialog stacking order - Qt.callLater(createDefaultRequest) + if (dialog.key) { + request.key = dialog.key + } else { + // callLater to make sure any popups are on top of the dialog stacking order + Qt.callLater(createDefaultRequest) + } } // hack. delay qr rendering until dialog is shown diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index b4d285b4f..f0171ab91 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -5,6 +5,8 @@ import QtQuick.Controls.Material 2.0 import QtQml.Models 2.1 import QtQml 2.6 +import org.electrum 1.0 + import "controls" Pane { @@ -35,10 +37,10 @@ Pane { model: Daemon.currentWallet.requestModel delegate: InvoiceDelegate { onClicked: { - //var dialog = app.stack.getRoot().openInvoice(model.key) - // dialog.invoiceAmountChanged.connect(function () { - // Daemon.currentWallet.invoiceModel.init_model() - // }) + // TODO: only open unpaid? + if (model.status == Invoice.Unpaid) { + app.stack.getRoot().openRequest(model.key) + } } } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 72a4df14b..d629cf4b7 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -21,6 +21,12 @@ Item { return dialog } + function openRequest(key) { + var dialog = receiveDialog.createObject(app, { key: key }) + dialog.open() + return dialog + } + function openSendDialog() { _sendDialog = sendDialog.createObject(mainView, {invoiceParser: invoiceParser}) _sendDialog.open() From adf23f602dc71ec58123ad33739c300933766258 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 24 Feb 2023 20:24:51 +0100 Subject: [PATCH 0198/1143] qml: don't show option Never for expiry combobox when in preferences --- electrum/gui/qml/components/Preferences.qml | 1 + .../gui/qml/components/controls/RequestExpiryComboBox.qml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 805e2be96..b30fc8692 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -247,6 +247,7 @@ Pane { } RequestExpiryComboBox { + includeNever: false onCurrentValueChanged: { if (activeFocus) Config.requestExpiry = currentValue diff --git a/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml b/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml index 3a0e161b4..30d24feea 100644 --- a/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml +++ b/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml @@ -6,6 +6,8 @@ import org.electrum 1.0 ElComboBox { id: expires + property bool includeNever: true + textRole: 'text' valueRole: 'value' @@ -18,7 +20,8 @@ ElComboBox { expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60}) expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60}) expiresmodel.append({'text': qsTr('1 month'), 'value': 31*24*60*60}) - expiresmodel.append({'text': qsTr('Never'), 'value': 0}) + if (includeNever) + expiresmodel.append({'text': qsTr('Never'), 'value': 0}) expires.currentIndex = 0 for (let i=0; i < expiresmodel.count; i++) { if (expiresmodel.get(i).value == Config.requestExpiry) { From d85ee1b639d848210bdf710ae95dbec2939b0016 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 24 Feb 2023 22:09:44 +0100 Subject: [PATCH 0199/1143] qml: move max ln receive to ReceiveDetailsDialog, always show invoice fields on ReceiveDialog --- .../qml/components/ReceiveDetailsDialog.qml | 26 +++++++++++++++++++ electrum/gui/qml/components/ReceiveDialog.qml | 24 +---------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index eb8ad4fcc..0af10e72d 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -41,6 +41,32 @@ ElDialog { columnSpacing: constants.paddingSmall columns: 4 + TextHighlightPane { + Layout.columnSpan: 4 + Layout.fillWidth: true + + visible: Daemon.currentWallet.lightningCanReceive + + RowLayout { + width: parent.width + spacing: constants.paddingXSmall + Label { + text: qsTr('Max amount over Lightning') + font.pixelSize: constants.fontSizeSmall + color: Material.accentColor + wrapMode: Text.Wrap + } + Image { + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + source: '../../icons/lightning.png' + } + FormattedAmount { + amount: Daemon.currentWallet.lightningCanReceive + } + } + } + Label { text: qsTr('Message') } diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 52cc2a87b..f220c6bb3 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -182,25 +182,6 @@ ElDialog { } } - RowLayout { - Layout.alignment: Qt.AlignHCenter - visible: Daemon.currentWallet.isLightning - spacing: constants.paddingXSmall - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - source: '../../icons/lightning.png' - } - Label { - text: qsTr('can receive:') - font.pixelSize: constants.fontSizeSmall - color: Material.accentColor - } - FormattedAmount { - amount: Daemon.currentWallet.lightningCanReceive - } - } - Rectangle { height: 1 Layout.alignment: Qt.AlignHCenter @@ -221,23 +202,20 @@ ElDialog { text: request.status_str } Label { - visible: request.message text: qsTr('Message') color: Material.accentColor } Label { - visible: request.message Layout.fillWidth: true text: request.message wrapMode: Text.Wrap } Label { - visible: !request.amount.isEmpty text: qsTr('Amount') color: Material.accentColor } FormattedAmount { - visible: !request.amount.isEmpty + valid: !request.amount.isEmpty amount: request.amount } } From 4cb3d411ea2f894404b27fcfea5ad4971eab6e66 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 24 Feb 2023 22:11:20 +0100 Subject: [PATCH 0200/1143] qml: override finish button text in ServerConnectWizard --- electrum/gui/qml/components/ServerConnectWizard.qml | 1 + electrum/gui/qml/components/wizard/Wizard.qml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/electrum/gui/qml/components/ServerConnectWizard.qml b/electrum/gui/qml/components/ServerConnectWizard.qml index ad4755761..3f08c7642 100644 --- a/electrum/gui/qml/components/ServerConnectWizard.qml +++ b/electrum/gui/qml/components/ServerConnectWizard.qml @@ -12,6 +12,7 @@ Wizard { enter: null // disable transition wiz: Daemon.serverConnectWizard + finishButtonText: qsTr('Next') onAccepted: { var proxy = wizard_data['proxy'] diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index 82de9fb2b..d34678c01 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -24,6 +24,7 @@ ElDialog { property var wizard_data property alias pages: pages property QtObject wiz + property alias finishButtonText: finishButton.text function doClose() { if (pages.currentIndex == 0) @@ -171,6 +172,7 @@ ElDialog { } Button { + id: finishButton text: qsTr("Finish") visible: pages.lastpage enabled: pages.pagevalid From 2b216ef6b967841ee838364ea37322e34edf965c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 25 Feb 2023 13:44:49 +0100 Subject: [PATCH 0201/1143] qml: avoid stacking ReceiveRequests pages --- electrum/gui/qml/components/ReceiveDialog.qml | 3 ++- electrum/gui/qml/components/ReceiveRequests.qml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index f220c6bb3..3d84dac6e 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -243,7 +243,8 @@ ElDialog { text: qsTr('Requests') onClicked: { dialog.close() - app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) + if (app.stack.currentItem.objectName != 'ReceiveRequests') + app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) } } FlatButton { diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index f0171ab91..0885d4375 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -11,6 +11,7 @@ import "controls" Pane { id: root + objectName: 'ReceiveRequests' ColumnLayout { anchors.fill: parent From 5426411f996e83ec4389098f177e09e4833d1405 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Feb 2023 10:57:12 +0100 Subject: [PATCH 0202/1143] android: include p4a cherry-pick 70fa6ddd040dc14f3cb28ebc2cfc5779c5cc5342, avoid sh>=2 --- contrib/android/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 97bb20cb0..0c1edb37a 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,7 +179,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "d33e07ba4c7931da46122a32f3807709a73cb7f6^{commit}" \ + && git checkout "254033e750ac844426de18085a4262066c8a34c7^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars From 40a2591351aa9e6e0335a524f8789a92ac843180 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Feb 2023 11:07:00 +0100 Subject: [PATCH 0203/1143] qml: wording/styling NetworkOverview --- .../gui/qml/components/NetworkOverview.qml | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 2ce5da262..361ac659e 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -31,47 +31,6 @@ Pane { width: parent.width columns: 2 - Heading { - Layout.columnSpan: 2 - text: qsTr('Network') - } - - Label { - text: qsTr('Proxy:'); - color: Material.accentColor - } - Label { - text: 'mode' in Network.proxy ? qsTr('enabled') : qsTr('disabled') - } - - Label { - visible: 'mode' in Network.proxy - text: qsTr('Proxy server:'); - color: Material.accentColor - } - Label { - visible: 'mode' in Network.proxy - text: Network.proxy['host'] ? Network.proxy['host'] + ':' + Network.proxy['port'] : '' - } - - Label { - visible: 'mode' in Network.proxy - text: qsTr('Proxy type:'); - color: Material.accentColor - } - RowLayout { - Image { - visible: Network.isProxyTor - Layout.preferredWidth: constants.iconSizeMedium - Layout.preferredHeight: constants.iconSizeMedium - source: '../../icons/tor_logo.png' - } - Label { - visible: 'mode' in Network.proxy - text: Network.isProxyTor ? 'TOR' : Network.proxy['mode'] - } - } - Heading { Layout.columnSpan: 2 text: qsTr('On-chain') @@ -128,7 +87,7 @@ Pane { } Label { - text: qsTr('Gossip:'); + text: Config.useGossip ? qsTr('Gossip:') : qsTr('Trampoline:') color: Material.accentColor } ColumnLayout { @@ -144,7 +103,7 @@ Pane { } } Label { - text: qsTr('disabled'); + text: qsTr('enabled'); visible: !Config.useGossip } @@ -157,6 +116,48 @@ Pane { visible: Daemon.currentWallet.isLightning text: Daemon.currentWallet.lightningNumPeers } + + Heading { + Layout.columnSpan: 2 + text: qsTr('Network') + } + + Label { + text: qsTr('Proxy:'); + color: Material.accentColor + } + Label { + text: 'mode' in Network.proxy ? qsTr('enabled') : qsTr('none') + } + + Label { + visible: 'mode' in Network.proxy + text: qsTr('Proxy server:'); + color: Material.accentColor + } + Label { + visible: 'mode' in Network.proxy + text: Network.proxy['host'] ? Network.proxy['host'] + ':' + Network.proxy['port'] : '' + } + + Label { + visible: 'mode' in Network.proxy + text: qsTr('Proxy type:'); + color: Material.accentColor + } + RowLayout { + Image { + visible: Network.isProxyTor + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium + source: '../../icons/tor_logo.png' + } + Label { + visible: 'mode' in Network.proxy + text: Network.isProxyTor ? 'TOR' : Network.proxy['mode'] + } + } + } } From 65abb9004987be585a82782606d4ca13a1eda996 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Feb 2023 11:16:04 +0100 Subject: [PATCH 0204/1143] build: update build container base image versions, update apt sources to current --- contrib/android/Dockerfile | 2 +- contrib/android/apt.sources.list | 4 ++-- contrib/build-linux/appimage/Dockerfile | 2 +- contrib/build-linux/appimage/apt.sources.list | 4 ++-- contrib/build-linux/sdist/Dockerfile | 2 +- contrib/build-wine/Dockerfile | 2 +- contrib/build-wine/apt.sources.list | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 0c1edb37a..04d2e74eb 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -1,6 +1,6 @@ # based on https://github.com/kivy/python-for-android/blob/master/Dockerfile -FROM debian:bullseye@sha256:bfe6615d017d1eebe19f349669de58cda36c668ef916e618be78071513c690e5 +FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 ENV DEBIAN_FRONTEND=noninteractive diff --git a/contrib/android/apt.sources.list b/contrib/android/apt.sources.list index b5b939218..fe4030017 100644 --- a/contrib/android/apt.sources.list +++ b/contrib/android/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20221106T031548Z/ bullseye main non-free contrib -deb-src https://snapshot.debian.org/archive/debian/20221106T031548Z/ bullseye main non-free contrib +deb https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib +deb-src https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib diff --git a/contrib/build-linux/appimage/Dockerfile b/contrib/build-linux/appimage/Dockerfile index df80315e9..e92b6eb0d 100644 --- a/contrib/build-linux/appimage/Dockerfile +++ b/contrib/build-linux/appimage/Dockerfile @@ -2,7 +2,7 @@ # from https://docs.appimage.org/introduction/concepts.html : # "[AppImages] should be built on the oldest possible system, allowing them to run on newer system[s]" -FROM debian:buster@sha256:e83854c9fb469daa7273d73c43a3fe5ae2da77cb40d3d34282a9af09a9db49f9 +FROM debian:buster@sha256:233c3bbc892229c82da7231980d50adceba4db56a08c0b7053a4852782703459 ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive diff --git a/contrib/build-linux/appimage/apt.sources.list b/contrib/build-linux/appimage/apt.sources.list index e202948aa..e19552491 100644 --- a/contrib/build-linux/appimage/apt.sources.list +++ b/contrib/build-linux/appimage/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20221106T031548Z/ buster main non-free contrib -deb-src https://snapshot.debian.org/archive/debian/20221106T031548Z/ buster main non-free contrib +deb https://snapshot.debian.org/archive/debian/20230226T090712Z/ buster main non-free contrib +deb-src https://snapshot.debian.org/archive/debian/20230226T090712Z/ buster main non-free contrib diff --git a/contrib/build-linux/sdist/Dockerfile b/contrib/build-linux/sdist/Dockerfile index f1df96e42..2caf62cf7 100644 --- a/contrib/build-linux/sdist/Dockerfile +++ b/contrib/build-linux/sdist/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye@sha256:bfe6615d017d1eebe19f349669de58cda36c668ef916e618be78071513c690e5 +FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive diff --git a/contrib/build-wine/Dockerfile b/contrib/build-wine/Dockerfile index fd7441435..deb28e114 100644 --- a/contrib/build-wine/Dockerfile +++ b/contrib/build-wine/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye@sha256:bfe6615d017d1eebe19f349669de58cda36c668ef916e618be78071513c690e5 +FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 # need ca-certificates before using snapshot packages RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ diff --git a/contrib/build-wine/apt.sources.list b/contrib/build-wine/apt.sources.list index b5b939218..fe4030017 100644 --- a/contrib/build-wine/apt.sources.list +++ b/contrib/build-wine/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20221106T031548Z/ bullseye main non-free contrib -deb-src https://snapshot.debian.org/archive/debian/20221106T031548Z/ bullseye main non-free contrib +deb https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib +deb-src https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib From 6a6982cdae016fa435973551d7e82883b94f7754 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Feb 2023 12:20:51 +0100 Subject: [PATCH 0205/1143] qml: defer intent handling at startup, otherwise it gets lost as the app is not handling the signal yet. Also defer intent handling until a wallet is opened. --- .../gui/qml/components/WalletMainView.qml | 15 +++++++++++++++ electrum/gui/qml/qeapp.py | 19 +++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index d629cf4b7..622632150 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -14,6 +14,7 @@ Item { property string title: Daemon.currentWallet ? Daemon.currentWallet.name : '' property var _sendDialog + property string _intentUri function openInvoice(key) { var dialog = invoiceDialog.createObject(app, { invoice: invoiceParser, invoice_key: key }) @@ -229,10 +230,24 @@ Item { Connections { target: AppController function onUriReceived(uri) { + console.log('uri received: ' + uri) + if (!Daemon.currentWallet) { + console.log('No wallet open, deferring') + _intentUri = uri + return + } invoiceParser.recipient = uri } } + Connections { + target: Daemon + function onWalletLoaded() { + if (_intentUri) + invoiceParser.recipient = _intentUri + } + } + Component { id: invoiceDialog InvoiceDialog { diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 43ddd78db..622d262fb 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -56,8 +56,6 @@ class QEAppController(BaseCrashReporter, QObject): sendingBugreportSuccess = pyqtSignal(str) sendingBugreportFailure = pyqtSignal(str) - _crash_user_text = '' - def __init__(self, qedaemon, plugins): BaseCrashReporter.__init__(self, None, None, None) QObject.__init__(self) @@ -65,6 +63,10 @@ def __init__(self, qedaemon, plugins): self._qedaemon = qedaemon self._plugins = plugins + self._crash_user_text = '' + self._app_started = False + self._intent = '' + # set up notification queue and notification_timer self.user_notification_queue = queue.Queue() self.user_notification_last_time = 0 @@ -142,15 +144,23 @@ def bindIntent(self): self.logger.error(f'unable to bind intent: {repr(e)}') def on_new_intent(self, intent): + if not self._app_started: + self._intent = intent + return + data = str(intent.getDataString()) + self.logger.debug(f'received intent: {repr(data)}') scheme = str(intent.getScheme()).lower() if scheme == BITCOIN_BIP21_URI_SCHEME or scheme == LIGHTNING_URI_SCHEME: self.uriReceived.emit(data) + def startupFinished(self): + self._app_started = True + if self._intent: + self.on_new_intent(self._intent) + @pyqtSlot(str, str) def doShare(self, data, title): - #if platform != 'android': - #return try: from jnius import autoclass, cast except ImportError: @@ -352,6 +362,7 @@ def objectCreated(self, object, url): if object is None: self._valid = False self.engine.objectCreated.disconnect(self.objectCreated) + self.appController.startupFinished() def message_handler(self, line, funct, file): # filter out common harmless messages From 8faf8f4a31ae83333047532a2d7cf0e731528f61 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Feb 2023 12:45:44 +0100 Subject: [PATCH 0206/1143] wine: add --allow-downgrades to second apt-get command --- contrib/build-wine/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/Dockerfile b/contrib/build-wine/Dockerfile index deb28e114..9b4dfbbdb 100644 --- a/contrib/build-wine/Dockerfile +++ b/contrib/build-wine/Dockerfile @@ -46,7 +46,7 @@ RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \ rm winehq.key && \ apt-add-repository https://dl.winehq.org/wine-builds/debian/ && \ apt-get update -q && \ - apt-get install -qy \ + apt-get install -qy --allow-downgrades \ wine-stable-amd64:amd64=7.0.0.0~bullseye-1 \ wine-stable-i386:i386=7.0.0.0~bullseye-1 \ wine-stable:amd64=7.0.0.0~bullseye-1 \ From 68a3364c33a798bf8faca1523c49fb38558aae49 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Feb 2023 14:42:26 +0100 Subject: [PATCH 0207/1143] qml: clear deferred intent after processing --- electrum/gui/qml/components/WalletMainView.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 622632150..8660b5e9c 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -243,8 +243,10 @@ Item { Connections { target: Daemon function onWalletLoaded() { - if (_intentUri) + if (_intentUri) { invoiceParser.recipient = _intentUri + _intentUri = '' + } } } From cb8cc76e1f9ba9f27c8f0d4bb57c9fca750860ae Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 27 Feb 2023 21:40:30 +0100 Subject: [PATCH 0208/1143] requests list: remove hidden column LN_INVOICE --- electrum/gui/qt/request_list.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 8d03ce713..6db3ed4cd 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -56,8 +56,7 @@ class Columns(IntEnum): AMOUNT = 2 STATUS = 3 ADDRESS = 4 - LN_INVOICE = 5 - LN_RHASH = 6 + LN_RHASH = 5 headers = { Columns.DATE: _('Date'), @@ -65,12 +64,11 @@ class Columns(IntEnum): Columns.AMOUNT: _('Amount'), Columns.STATUS: _('Status'), Columns.ADDRESS: _('Address'), - Columns.LN_INVOICE: 'LN Request', Columns.LN_RHASH: 'LN RHASH', } filter_columns = [ Columns.DATE, Columns.DESCRIPTION, Columns.AMOUNT, - Columns.ADDRESS, Columns.LN_INVOICE, Columns.LN_RHASH, + Columns.ADDRESS, Columns.LN_RHASH, ] def __init__(self, receive_tab: 'ReceiveTab'): @@ -150,7 +148,6 @@ def update(self): labels[self.Columns.AMOUNT] = amount_str labels[self.Columns.STATUS] = status_str labels[self.Columns.ADDRESS] = req.get_address() or "" - labels[self.Columns.LN_INVOICE] = req.lightning_invoice or "" labels[self.Columns.LN_RHASH] = req.rhash if req.is_lightning() else "" items = [QStandardItem(e) for e in labels] self.set_editability(items) @@ -218,5 +215,4 @@ def set_visibility_of_columns(self): def set_visible(col: int, b: bool): self.showColumn(col) if b else self.hideColumn(col) set_visible(self.Columns.ADDRESS, False) - set_visible(self.Columns.LN_INVOICE, False) set_visible(self.Columns.LN_RHASH, False) From da402973cd02744065cdb9a9e3d5a73070b941c9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 28 Feb 2023 09:34:03 +0100 Subject: [PATCH 0209/1143] follow-up 72fb43f950815ef7a655c39f47610c835ac155d0 --- electrum/gui/qml/qeswaphelper.py | 2 +- electrum/gui/qt/swap_dialog.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 66e8e3b6e..511835108 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -202,7 +202,7 @@ def init_swap_slider_range(self): max_onchain_spend = 0 reverse = int(min(lnworker.num_sats_can_send(), swap_manager.get_max_amount())) - max_recv_amt_ln = int(swap_manager.num_sats_can_receive()) + max_recv_amt_ln = int(lnworker.num_sats_can_receive()) max_recv_amt_oc = swap_manager.get_send_amount(max_recv_amt_ln, is_reverse=False) or 0 forward = int(min(max_recv_amt_oc, # maximally supported swap amount by provider diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 259ab95ec..1e298a4a1 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -168,7 +168,7 @@ def on_send_edited(self): if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send(): # cannot send this much on lightning recv_amount = None - if (not self.is_reverse) and recv_amount and recv_amount > self.swap_manager.num_sats_can_receive(): + if (not self.is_reverse) and recv_amount and recv_amount > self.lnworker.num_sats_can_receive(): # cannot receive this much on lightning recv_amount = None self.recv_amount_e.follows = True @@ -244,7 +244,7 @@ def run(self): onchain_amount = self.send_amount_e.get_amount() if lightning_amount is None or onchain_amount is None: return - if lightning_amount > self.swap_manager.num_sats_can_receive(): + if lightning_amount > self.lnworker.num_sats_can_receive(): if not self.window.question(CANNOT_RECEIVE_WARNING): return self.window.protect(self.do_normal_swap, (lightning_amount, onchain_amount)) From 0928c0190a16c1679ca162d8b66027a0c5b5237e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 11:58:15 +0100 Subject: [PATCH 0210/1143] qml: fix toaster quirkyness --- electrum/gui/qml/components/controls/Toaster.qml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/controls/Toaster.qml b/electrum/gui/qml/components/controls/Toaster.qml index c8ec99113..cad38cb3e 100644 --- a/electrum/gui/qml/components/controls/Toaster.qml +++ b/electrum/gui/qml/components/controls/Toaster.qml @@ -7,7 +7,7 @@ import ".." Item { id: toaster - width: rect.width + width: contentItem.implicitWidth height: rect.height visible: false @@ -17,9 +17,9 @@ Item { function show(item, text) { _text = text var r = item.mapToItem(parent, item.x, item.y) - x = r.x - (toaster.width - item.width)/2 + x = r.x - item.width + 0.5*(item.width - toaster.width) y = r.y - toaster.height - constants.paddingLarge - toaster._y = y - 35 + toaster._y = y - toaster.height ani.restart() } @@ -40,17 +40,17 @@ Item { id: rect width: contentItem.width height: contentItem.height - color: constants.colorAlpha(Material.dialogColor, 0.90) - border { - color: Material.accentColor - width: 1 - } + color: constants.colorAlpha(Material.background, 0.90) RowLayout { id: contentItem Label { Layout.margins: 10 text: toaster._text + onTextChanged: { + // hack. ref implicitWidth so it gets recalculated + var _ = contentItem.implicitWidth + } } } } From fe540200a9873fd29f860fada537a290c9b26cae Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 14:04:20 +0100 Subject: [PATCH 0211/1143] qml: report wallet open problems when walletdb cannot open a wallet (WalletFileException) --- electrum/gui/qml/components/Wallets.qml | 2 +- electrum/gui/qml/qedaemon.py | 5 +++ electrum/gui/qml/qewalletdb.py | 45 ++++++++++++++----------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index e9d384265..9d18b3f61 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -56,7 +56,7 @@ Pane { height: row.height onClicked: { - if (Daemon.currentWallet.name != model.name) + if (!Daemon.currentWallet || Daemon.currentWallet.name != model.name) Daemon.load_wallet(model.path) else app.stack.pop() diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index f8c0a88e1..97d524dfc 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -142,12 +142,17 @@ def __init__(self, daemon, parent=None): self._walletdb = QEWalletDB() self._walletdb.validPasswordChanged.connect(self.passwordValidityCheck) + self._walletdb.walletOpenProblem.connect(self.on_wallet_open_problem) @pyqtSlot() def passwordValidityCheck(self): if not self._walletdb._validPassword: self.walletRequiresPassword.emit(self._name, self._path) + @pyqtSlot(str) + def on_wallet_open_problem(self, error): + self.walletOpenError.emit(error) + @pyqtSlot() @pyqtSlot(str) @pyqtSlot(str, str) diff --git a/electrum/gui/qml/qewalletdb.py b/electrum/gui/qml/qewalletdb.py index 346e6c0d9..ebc88dc44 100644 --- a/electrum/gui/qml/qewalletdb.py +++ b/electrum/gui/qml/qewalletdb.py @@ -6,14 +6,15 @@ from electrum.storage import WalletStorage, StorageEncryptionVersion from electrum.wallet_db import WalletDB from electrum.bip32 import normalize_bip32_derivation, xpub_type -from electrum.util import InvalidPassword +from electrum.util import InvalidPassword, WalletFileException from electrum import keystore class QEWalletDB(QObject): _logger = get_logger(__name__) fileNotFound = pyqtSignal() - pathChanged = pyqtSignal([bool], arguments=["ready"]) + walletOpenProblem = pyqtSignal([str], arguments=['error']) + pathChanged = pyqtSignal([bool], arguments=['ready']) needsPasswordChanged = pyqtSignal() needsHWDeviceChanged = pyqtSignal() passwordChanged = pyqtSignal() @@ -149,21 +150,25 @@ def load_storage(self): def load_db(self): # needs storage accessible - self._db = WalletDB(self._storage.read(), manual_upgrades=True) - if self._db.requires_split(): - self._logger.warning('wallet requires split') - self._requiresSplit = True - self.requiresSplitChanged.emit() - return - if self._db.get_action(): - self._logger.warning('action pending. QML version doesn\'t support continuation of wizard') - return - - if self._db.requires_upgrade(): - self._logger.warning('wallet requires upgrade, upgrading') - self._db.upgrade() - self._db.write(self._storage) - - self._ready = True - self.readyChanged.emit() - + try: + self._db = WalletDB(self._storage.read(), manual_upgrades=True) + if self._db.requires_split(): + self._logger.warning('wallet requires split') + self._requiresSplit = True + self.requiresSplitChanged.emit() + return + if self._db.get_action(): + self._logger.warning('action pending. QML version doesn\'t support continuation of wizard') + return + + if self._db.requires_upgrade(): + self._logger.warning('wallet requires upgrade, upgrading') + self._db.upgrade() + self._db.write(self._storage) + + self._ready = True + self.readyChanged.emit() + except WalletFileException as e: + self._logger.error(f'{repr(e)}') + self._storage = None + self.walletOpenProblem.emit(str(e)) From d59e687cdb7cbbf125a04b938492bc10c449264f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 14:11:52 +0100 Subject: [PATCH 0212/1143] qml: follow-up fe540200a9873fd29f860fada537a290c9b26cae --- electrum/gui/qml/components/OpenWalletDialog.qml | 4 ++++ electrum/gui/qml/qedaemon.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index 0d58565d0..9fcd75f84 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -134,6 +134,10 @@ ElDialog { onNeedsPasswordChanged: { notice.visible = needsPassword } + onWalletOpenProblem: { + openwalletdialog.close() + Daemon.onWalletOpenProblem(error) + } } Component.onCompleted: { diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 97d524dfc..c3c3f578f 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -142,7 +142,7 @@ def __init__(self, daemon, parent=None): self._walletdb = QEWalletDB() self._walletdb.validPasswordChanged.connect(self.passwordValidityCheck) - self._walletdb.walletOpenProblem.connect(self.on_wallet_open_problem) + self._walletdb.walletOpenProblem.connect(self.onWalletOpenProblem) @pyqtSlot() def passwordValidityCheck(self): @@ -150,7 +150,7 @@ def passwordValidityCheck(self): self.walletRequiresPassword.emit(self._name, self._path) @pyqtSlot(str) - def on_wallet_open_problem(self, error): + def onWalletOpenProblem(self, error): self.walletOpenError.emit(error) @pyqtSlot() From a88c2ced25e283939acdb52408978e218a0a431c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 14:14:57 +0100 Subject: [PATCH 0213/1143] qml: qerequestdetails check lnworker before deref --- electrum/gui/qml/qerequestdetails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 2d2e05fe5..7a1c07d29 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -116,7 +116,7 @@ def expiration(self): @pyqtProperty(str, notify=detailsChanged) def bolt11(self): - can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() + can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 if self._req and can_receive > 0 and self._req.amount_msat/1000 <= can_receive: return self._req.lightning_invoice else: From 7e84aed9c22dc4117f371c7c4c953408988c1dbb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 14:32:26 +0100 Subject: [PATCH 0214/1143] qml: log and reraise any exceptions in the constructor of QEAbstractInvoiceListModel, so we at least see the root cause of the confusing AttributeError: 'QEWallet' object has no attribute 'requestModel' --- electrum/gui/qml/qeinvoicelistmodel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 16b918f77..f5f53a8db 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -29,7 +29,11 @@ def __init__(self, wallet, parent=None): self._timer.setSingleShot(True) self._timer.timeout.connect(self.updateStatusStrings) - self.init_model() + try: + self.init_model() + except Exception as e: + self._logger.error(f'{repr(e)}') + raise e def rowCount(self, index): return len(self.invoices) From c7cb2fb9e68ecb7b2dfd9fcf87f4d3b5feec278c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 14:49:08 +0100 Subject: [PATCH 0215/1143] qml: explicitly use None when empty string is used as password backend requires None, Qt5 passes empty string --- electrum/gui/qml/qedaemon.py | 9 ++++++++- electrum/gui/qml/qewallet.py | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index c3c3f578f..6c8fb0801 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -171,6 +171,10 @@ def load_wallet(self, path=None, password=None): self._logger.debug('load wallet ' + str(self._path)) + # map empty string password to None + if password == '': + password = None + if not password: password = self._password @@ -225,7 +229,7 @@ def _on_backend_wallet_loaded(self, password = None): wallet = self.daemon._wallets[self._path] self._current_wallet = QEWallet.getInstanceFor(wallet) self.availableWallets.updateWallet(self._path) - self._current_wallet.password = password + self._current_wallet.password = password if password else None self.walletLoaded.emit(self._name, self._path) @@ -312,6 +316,9 @@ def startChangePassword(self): @pyqtSlot(str) def setPassword(self, password): assert self._use_single_password + # map empty string password to None + if password == '': + password = None self._logger.debug('about to set password for ALL wallets') self.daemon.update_password_for_directory(old_password=self._password, new_password=password) self._password = password diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index e6f2f1b64..8132f270f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -710,14 +710,20 @@ def verify_password(self, password): @pyqtSlot(str) def set_password(self, password): + if password == '': + password = None + storage = self.wallet.storage # HW wallet not supported yet if storage.is_encrypted_with_hw_device(): return + current_password = self.password if self.password != '' else None + try: - self.wallet.update_password(self.password, password, encrypt_storage=True) + self._logger.info(f'PW change from {current_password} to {password}') + self.wallet.update_password(current_password, password, encrypt_storage=True) self.password = password except InvalidPassword as e: self._logger.exception(repr(e)) From 719b468eee8b3e13680f6e7b90194d618181fe0c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 27 Feb 2023 10:31:21 +0100 Subject: [PATCH 0216/1143] Refresh bolt11 routing hints when channel liquidity changes: - wallet_db update: separate Invoices and Requests. - do not store bolt11 invoice in Request --- electrum/gui/qml/qeinvoicelistmodel.py | 3 +- electrum/gui/qml/qerequestdetails.py | 2 +- electrum/gui/qt/receive_tab.py | 2 +- electrum/gui/qt/request_list.py | 2 +- electrum/invoices.py | 107 +++++++++++++++---------- electrum/lnworker.py | 62 +++++++------- electrum/submarine_swaps.py | 8 +- electrum/tests/test_invoices.py | 14 ++-- electrum/tests/test_lnpeer.py | 3 + electrum/wallet.py | 41 +++++----- electrum/wallet_db.py | 22 ++++- 11 files changed, 154 insertions(+), 112 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index f5f53a8db..1bd39dd4d 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -124,8 +124,7 @@ def invoice_to_model(self, invoice: Invoice): item['address'] = '' item['date'] = format_time(item['timestamp']) item['amount'] = QEAmount(from_invoice=invoice) - item['onchain_fallback'] = invoice.is_lightning() and invoice._lnaddr.get_fallback_address() - item['type'] = 'invoice' + item['onchain_fallback'] = invoice.is_lightning() and invoice.get_address() return item diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 7a1c07d29..1956cdfa4 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -118,7 +118,7 @@ def expiration(self): def bolt11(self): can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 if self._req and can_receive > 0 and self._req.amount_msat/1000 <= can_receive: - return self._req.lightning_invoice + return self._wallet.wallet.get_bolt11_invoice(self._req) else: return '' diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index dfe3da7fb..fa012c0d4 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -224,7 +224,7 @@ def update_current_request(self): help_texts = self.wallet.get_help_texts_for_receive_request(req) addr = (req.get_address() or '') if not help_texts.address_is_error else '' URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else '' - lnaddr = (req.lightning_invoice or '') if not help_texts.ln_is_error else '' + lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else '' address_help = help_texts.address_help URI_help = help_texts.URI_help ln_help = help_texts.ln_help diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 6db3ed4cd..49cead064 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -197,7 +197,7 @@ def create_menu(self, position): if URI := self.wallet.get_request_URI(req): copy_menu.addAction(_("Bitcoin URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) if req.is_lightning(): - copy_menu.addAction(_("Lightning Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request')) + copy_menu.addAction(_("Lightning Request"), lambda: self.parent.do_copy(self.wallet.get_bolt11_invoice(req), title='Lightning Request')) #if 'view_url' in req: # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) menu.addAction(_("Delete"), lambda: self.delete_requests([key])) diff --git a/electrum/invoices.py b/electrum/invoices.py index 0933cba6c..2791258bc 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -7,6 +7,7 @@ from .json_db import StoredObject from .i18n import _ from .util import age, InvoiceError +from .lnutil import hex_to_bytes from .lnaddr import lndecode, LnAddr from . import constants from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC @@ -83,7 +84,11 @@ def _decode_outputs(outputs) -> Optional[List[PartialTxOutput]]: @attr.s -class Invoice(StoredObject): +class BaseInvoice(StoredObject): + """ + Base class for Invoice and Request + In the code, we use 'invoice' for outgoing payments, and 'request' for incoming payments. + """ # mandatory fields amount_msat = attr.ib(kw_only=True) # type: Optional[Union[int, str]] # can be '!' or None @@ -101,10 +106,6 @@ class Invoice(StoredObject): bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str] #bip70_requestor = attr.ib(type=str, kw_only=True) # type: Optional[str] - # lightning only - lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] - - __lnaddr = None def is_lightning(self): return self.lightning_invoice is not None @@ -117,15 +118,6 @@ def get_status_str(self, status): status_str = _('Expires') + ' ' + age(expiration, include_seconds=True) return status_str - def get_address(self) -> Optional[str]: - """returns the first address, to be displayed in GUI""" - address = None - if self.outputs: - address = self.outputs[0].address if len(self.outputs) > 0 else None - if not address and self.is_lightning(): - address = self._lnaddr.get_fallback_address() or None - return address - def get_outputs(self) -> Sequence[PartialTxOutput]: outputs = self.outputs or [] if not outputs: @@ -135,12 +127,6 @@ def get_outputs(self) -> Sequence[PartialTxOutput]: outputs = [PartialTxOutput.from_address_and_value(address, int(amount))] return outputs - def can_be_paid_onchain(self) -> bool: - if self.is_lightning(): - return bool(self._lnaddr.get_fallback_address()) - else: - return True - def get_expiration_date(self): # 0 means never return self.exp + self.time if self.exp else 0 @@ -193,12 +179,6 @@ def get_bip21_URI(self, *, include_lightning: bool = False) -> Optional[str]: uri = create_bip21_uri(addr, amount, message, extra_query_params=extra) return str(uri) - @lightning_invoice.validator - def _validate_invoice_str(self, attribute, value): - if value is not None: - lnaddr = lndecode(value) # this checks the str can be decoded - self.__lnaddr = lnaddr # save it, just to avoid having to recompute later - @amount_msat.validator def _validate_amount(self, attribute, value): if value is None: @@ -212,16 +192,6 @@ def _validate_amount(self, attribute, value): else: raise InvoiceError(f"unexpected amount: {value!r}") - @property - def _lnaddr(self) -> LnAddr: - if self.__lnaddr is None: - self.__lnaddr = lndecode(self.lightning_invoice) - return self.__lnaddr - - @property - def rhash(self) -> str: - return self._lnaddr.paymenthash.hex() - @classmethod def from_bech32(cls, invoice: str) -> 'Invoice': """Constructs Invoice object from BOLT-11 string. @@ -259,6 +229,48 @@ def from_bip70_payreq(cls, pr: 'PaymentRequest', *, height: int = 0) -> 'Invoice lightning_invoice=None, ) + def get_id(self) -> str: + if self.is_lightning(): + return self.rhash + else: # on-chain + return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time) + +@attr.s +class Invoice(BaseInvoice): + lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] + __lnaddr = None + + def get_address(self) -> Optional[str]: + """returns the first address, to be displayed in GUI""" + address = None + if self.outputs: + address = self.outputs[0].address if len(self.outputs) > 0 else None + if not address and self.is_lightning(): + address = self._lnaddr.get_fallback_address() or None + return address + + @property + def _lnaddr(self) -> LnAddr: + if self.__lnaddr is None: + self.__lnaddr = lndecode(self.lightning_invoice) + return self.__lnaddr + + @property + def rhash(self) -> str: + return self._lnaddr.paymenthash.hex() + + @lightning_invoice.validator + def _validate_invoice_str(self, attribute, value): + if value is not None: + lnaddr = lndecode(value) # this checks the str can be decoded + self.__lnaddr = lnaddr # save it, just to avoid having to recompute later + + def can_be_paid_onchain(self) -> bool: + if self.is_lightning(): + return bool(self._lnaddr.get_fallback_address()) + else: + return True + def to_debug_json(self) -> Dict[str, Any]: d = self.to_json() d.update({ @@ -274,11 +286,24 @@ def to_debug_json(self) -> Dict[str, Any]: d['r_tags'] = [str((a.hex(),b.hex(),c,d,e)) for a,b,c,d,e in ln_routing_info[-1]] return d - def get_id(self) -> str: - if self.is_lightning(): - return self.rhash - else: # on-chain - return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time) + +@attr.s +class Request(BaseInvoice): + payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes) # type: Optional[bytes] + + def is_lightning(self): + return self.payment_hash is not None + + def get_address(self) -> Optional[str]: + """returns the first address, to be displayed in GUI""" + address = None + if self.outputs: + address = self.outputs[0].address if len(self.outputs) > 0 else None + return address + + @property + def rhash(self) -> str: + return self.payment_hash.hex() def get_id_from_onchain_outputs(outputs: List[PartialTxOutput], *, timestamp: int) -> str: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 6dc9b32c6..5f6a579c2 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -633,6 +633,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.lnrater: LNRater = None self.payment_info = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage + self._bolt11_cache = {} # note: this sweep_address is only used as fallback; as it might result in address-reuse self.logs = defaultdict(list) # type: Dict[str, List[HtlcLog]] # key is RHASH # (not persisted) # used in tests @@ -980,6 +981,7 @@ def channels_for_peer(self, node_id): def channel_state_changed(self, chan: Channel): if type(chan) is Channel: self.save_channel(chan) + self.clear_invoices_cache() util.trigger_callback('channel', self.wallet, chan) def save_channel(self, chan: Channel): @@ -1781,27 +1783,31 @@ def create_route_for_payment( route[-1].node_features |= invoice_features return route - def create_invoice( + def clear_invoices_cache(self): + self._bolt11_cache.clear() + + def get_bolt11_invoice( self, *, + payment_hash: bytes, amount_msat: Optional[int], message: str, expiry: int, fallback_address: Optional[str], - write_to_disk: bool = True, channels: Optional[Sequence[Channel]] = None, ) -> Tuple[LnAddr, str]: + pair = self._bolt11_cache.get(payment_hash) + if pair: + lnaddr, invoice = pair + assert lnaddr.get_amount_msat() == amount_msat + return pair + assert amount_msat is None or amount_msat > 0 timestamp = int(time.time()) routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels) - if not routing_hints: - self.logger.info( - "Warning. No routing hints added to invoice. " - "Other clients will likely not be able to send to us.") + self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}") invoice_features = self.features.for_invoice() - payment_preimage = os.urandom(32) - payment_hash = sha256(payment_preimage) - info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) + payment_preimage = self.get_preimage(payment_hash) amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None if expiry == 0: expiry = LN_EXPIRY_NEVER @@ -1820,30 +1826,20 @@ def create_invoice( date=timestamp, payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage)) invoice = lnencode(lnaddr, self.node_keypair.privkey) + pair = lnaddr, invoice + self._bolt11_cache[payment_hash] = pair + return pair + + def create_payment_info(self, amount_sat: Optional[int], write_to_disk=True) -> bytes: + amount_msat = amount_sat * 1000 if amount_sat else None + payment_preimage = os.urandom(32) + payment_hash = sha256(payment_preimage) + info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) self.save_preimage(payment_hash, payment_preimage, write_to_disk=False) self.save_payment_info(info, write_to_disk=False) if write_to_disk: self.wallet.save_db() - return lnaddr, invoice - - def add_request( - self, - *, - amount_sat: Optional[int], - message: str, - expiry: int, - fallback_address: Optional[str], - ) -> str: - # passed expiry is relative, it is absolute in the lightning invoice - amount_msat = amount_sat * 1000 if amount_sat else None - lnaddr, invoice = self.create_invoice( - amount_msat=amount_msat, - message=message, - expiry=expiry, - fallback_address=fallback_address, - write_to_disk=False, - ) - return invoice + return payment_hash def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True): assert sha256(preimage) == payment_hash @@ -1853,7 +1849,7 @@ def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: def get_preimage(self, payment_hash: bytes) -> Optional[bytes]: r = self.preimages.get(payment_hash.hex()) - return bfh(r) if r else None + return bytes.fromhex(r) if r else None def get_payment_info(self, payment_hash: bytes) -> Optional[PaymentInfo]: """returns None if payment_hash is a payment we are forwarding""" @@ -1922,6 +1918,8 @@ def set_invoice_status(self, key: str, status: int) -> None: self.set_payment_status(bfh(key), status) util.trigger_callback('invoice_status', self.wallet, key, status) self.logger.info(f"invoice status triggered (2) for key {key} and status {status}") + # liquidity changed + self.clear_invoices_cache() def set_request_status(self, payment_hash: bytes, status: int) -> None: if self.get_payment_status(payment_hash) == status: @@ -2138,7 +2136,7 @@ def get_channels_for_receiving(self, amount_msat=None) -> Sequence[Channel]: channels = list(self.channels.values()) # we exclude channels that cannot *right now* receive (e.g. peer offline) channels = [chan for chan in channels - if (chan.is_active() and not chan.is_frozen_for_receiving())] + if (chan.is_open() and not chan.is_frozen_for_receiving())] # Filter out nodes that have low receive capacity compared to invoice amt. # Even with MPP, below a certain threshold, including these channels probably # hurts more than help, as they lead to many failed attempts for the sender. @@ -2283,7 +2281,7 @@ async def rebalance_channels(self, chan1, chan2, amount_msat): raise Exception('Rebalance requires two different channels') if self.uses_trampoline() and chan1.node_id == chan2.node_id: raise Exception('Rebalance requires channels from different trampolines') - lnaddr, invoice = self.create_invoice( + lnaddr, invoice = self.add_reqest( amount_msat=amount_msat, message='rebalance', expiry=3600, diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 55defd265..9532bc8a6 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -261,14 +261,16 @@ async def normal_swap( assert self.lnwatcher privkey = os.urandom(32) pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) - lnaddr, invoice = self.lnworker.create_invoice( - amount_msat=lightning_amount_sat * 1000, + amount_msat = lightning_amount_sat * 1000 + payment_hash = self.lnworker.create_payment_info(lightning_amount_sat) + lnaddr, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=payment_hash, + amount_msat=amount_msat, message='swap', expiry=3600 * 24, fallback_address=None, channels=channels, ) - payment_hash = lnaddr.paymenthash preimage = self.lnworker.get_preimage(payment_hash) request_data = { "type": "submarine", diff --git a/electrum/tests/test_invoices.py b/electrum/tests/test_invoices.py index 3bdf9d847..06dafd494 100644 --- a/electrum/tests/test_invoices.py +++ b/electrum/tests/test_invoices.py @@ -5,7 +5,7 @@ from electrum.simple_config import SimpleConfig from electrum.wallet import restore_wallet_from_text, Standard_Wallet, Abstract_Wallet -from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, Invoice +from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED from electrum.transaction import Transaction, PartialTxOutput from electrum.util import TxMinedInfo @@ -20,11 +20,11 @@ def setUp(self): self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.wallet1_path = os.path.join(self.electrum_path, "somewallet1") self.wallet2_path = os.path.join(self.electrum_path, "somewallet2") - self._orig_get_cur_time = Invoice._get_cur_time + self._orig_get_cur_time = BaseInvoice._get_cur_time def tearDown(self): super().tearDown() - Invoice._get_cur_time = staticmethod(self._orig_get_cur_time) + BaseInvoice._get_cur_time = staticmethod(self._orig_get_cur_time) def create_wallet2(self) -> Standard_Wallet: text = 'cross end slow expose giraffe fuel track awake turtle capital ranch pulp' @@ -156,8 +156,6 @@ async def test_wallet_reuse_unused_fallback_onchain_addr_when_getting_paid_with_ self.assertTrue(pr1.is_lightning()) self.assertEqual(PR_UNPAID, wallet1.get_invoice_status(pr1)) self.assertEqual(addr1, pr1.get_address()) - self.assertEqual(addr1, pr1._lnaddr.get_fallback_address()) - self.assertTrue(pr1.can_be_paid_onchain()) self.assertFalse(pr1.has_expired()) # create payreq2 @@ -213,7 +211,7 @@ async def test_wallet_reuse_addr_of_expired_request(self): self.assertEqual(addr1, pr1.get_address()) self.assertFalse(pr1.has_expired()) - Invoice._get_cur_time = lambda *args: time.time() + 100_000 + BaseInvoice._get_cur_time = lambda *args: time.time() + 100_000 self.assertTrue(pr1.has_expired()) # create payreq2 @@ -240,7 +238,7 @@ async def test_wallet_get_request_by_addr(self): self.assertFalse(pr1.has_expired()) self.assertEqual(pr1, wallet1.get_request_by_addr(addr1)) - Invoice._get_cur_time = lambda *args: time.time() + 100_000 + BaseInvoice._get_cur_time = lambda *args: time.time() + 100_000 self.assertTrue(pr1.has_expired()) self.assertEqual(None, wallet1.get_request_by_addr(addr1)) @@ -265,6 +263,6 @@ async def test_wallet_get_request_by_addr(self): self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr1)) # now make both invoices be past their expiration date. pr2 should be unaffected. - Invoice._get_cur_time = lambda *args: time.time() + 200_000 + BaseInvoice._get_cur_time = lambda *args: time.time() + 200_000 self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2)) self.assertEqual(pr2, wallet1.get_request_by_addr(addr1)) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 187a5139c..095165469 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -175,6 +175,9 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}") + def clear_invoices_cache(self): + pass + def pay_scheduled_invoices(self): pass diff --git a/electrum/wallet.py b/electrum/wallet.py index c2812d2b8..630f40beb 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -74,7 +74,7 @@ from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) -from .invoices import Invoice +from .invoices import Invoice, Request from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED from .contacts import Contacts from .interface import NetworkException @@ -2444,7 +2444,7 @@ def export_request(self, x: Invoice) -> Dict[str, Any]: } if is_lightning: d['rhash'] = x.rhash - d['lightning_invoice'] = x.lightning_invoice + d['lightning_invoice'] = self.get_bolt11_invoice(x) d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_receive'] = self.lnworker.can_receive_invoice(x) @@ -2478,7 +2478,7 @@ def export_invoice(self, x: Invoice) -> Dict[str, Any]: 'invoice_id': key, } if is_lightning: - d['lightning_invoice'] = x.lightning_invoice + d['lightning_invoice'] = self.get_bolt11_invoice(x) d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_pay'] = self.lnworker.can_pay_invoice(x) @@ -2509,6 +2509,18 @@ def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None: relevant_invoice_keys.add(invoice_key) self._update_onchain_invoice_paid_detection(relevant_invoice_keys) + def get_bolt11_invoice(self, req: Request) -> str: + if not self.lnworker: + return '' + amount_msat = req.amount_msat if req.amount_msat > 0 else None + lnaddr, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=req.payment_hash, + amount_msat=amount_msat, + message=req.message, + expiry=req.exp, + fallback_address=req.get_address() if self.config.get('bolt11_fallback', True) else None) + return invoice + def create_request(self, amount_sat: int, message: str, exp_delay: int, address: Optional[str]): # for receiving amount_sat = amount_sat or 0 @@ -2516,21 +2528,11 @@ def create_request(self, amount_sat: int, message: str, exp_delay: int, address: message = message or '' address = address or None # converts "" to None exp_delay = exp_delay or 0 - timestamp = int(Invoice._get_cur_time()) - fallback_address = address if self.config.get('bolt11_fallback', True) else None - lightning = self.has_lightning() - if lightning: - lightning_invoice = self.lnworker.add_request( - amount_sat=amount_sat, - message=message, - expiry=exp_delay, - fallback_address=fallback_address, - ) - else: - lightning_invoice = None + timestamp = int(Request._get_cur_time()) + payment_hash = self.lnworker.create_payment_info(amount_sat, write_to_disk=False) if self.has_lightning() else None outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] height = self.adb.get_local_height() - req = Invoice( + req = Request( outputs=outputs, message=message, time=timestamp, @@ -2538,7 +2540,7 @@ def create_request(self, amount_sat: int, message: str, exp_delay: int, address: exp=exp_delay, height=height, bip70=None, - lightning_invoice=lightning_invoice, + payment_hash=payment_hash, ) key = self.add_payment_request(req) return key @@ -2859,7 +2861,6 @@ def get_help_texts_for_receive_request(self, req: Invoice) -> ReceiveRequestHelp ln_is_error = False ln_swap_suggestion = None ln_rebalance_suggestion = None - lnaddr = req.lightning_invoice or '' URI = self.get_request_URI(req) or '' lightning_online = self.lnworker and self.lnworker.num_peers() > 0 can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive() @@ -2879,7 +2880,7 @@ def get_help_texts_for_receive_request(self, req: Invoice) -> ReceiveRequestHelp URI_help = _('This request cannot be paid on-chain') if is_amt_too_small_for_onchain: URI_help = _('Amount too small to be received onchain') - if not lnaddr: + if not req.is_lightning(): ln_is_error = True ln_help = _('This request does not have a Lightning invoice.') @@ -2887,7 +2888,7 @@ def get_help_texts_for_receive_request(self, req: Invoice) -> ReceiveRequestHelp if self.adb.is_used(addr): address_help = URI_help = (_("This address has already been used. " "For better privacy, do not reuse it for new payments.")) - if lnaddr: + if req.is_lightning(): if not lightning_online: ln_is_error = True ln_help = _('You must be online to receive Lightning payments.') diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index a7a87fa19..157b5a477 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -33,7 +33,7 @@ from . import util, bitcoin from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh -from .invoices import Invoice +from .invoices import Invoice, Request from .keystore import bip44_derivation from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .logging import Logger @@ -52,7 +52,7 @@ OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 50 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 51 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -199,6 +199,7 @@ def upgrade(self): self._convert_version_48() self._convert_version_49() self._convert_version_50() + self._convert_version_51() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() @@ -980,6 +981,21 @@ def _convert_version_50(self): self._convert_invoices_keys(requests) self.data['seed_version'] = 50 + def _convert_version_51(self): + from .lnaddr import lndecode + if not self._is_upgrade_method_needed(50, 50): + return + requests = self.data.get('payment_requests', {}) + for key, item in list(requests.items()): + lightning_invoice = item.pop('lightning_invoice') + if lightning_invoice is None: + payment_hash = None + else: + lnaddr = lndecode(lightning_invoice) + payment_hash = lnaddr.paymenthash.hex() + item['payment_hash'] = payment_hash + self.data['seed_version'] = 51 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return @@ -1460,7 +1476,7 @@ def _convert_dict(self, path, key, v): if key == 'invoices': v = dict((k, Invoice(**x)) for k, x in v.items()) if key == 'payment_requests': - v = dict((k, Invoice(**x)) for k, x in v.items()) + v = dict((k, Request(**x)) for k, x in v.items()) elif key == 'adds': v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items()) elif key == 'fee_updates': From 5912c9226058d007782bc9a6794fb3f2d5cb4956 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 16:19:47 +0100 Subject: [PATCH 0217/1143] qml: TxDetails buttons refactor all buttons have icons now bump fee and cancel tx now below status line in highlightbox --- electrum/gui/qml/components/TxDetails.qml | 93 ++++++++++++++--------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index bd2537ed9..088409b9c 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -109,27 +109,6 @@ Pane { } } - Item { - visible: feebumpButton.visible - Layout.preferredWidth: 1 ; Layout.preferredHeight: 1 - } - FlatButton { - id: feebumpButton - visible: txdetails.canBump || txdetails.canCpfp - textUnderIcon: false - icon.source: '../../icons/warning.png' - icon.color: 'transparent' - text: qsTr('Bump fee') - onClicked: { - if (txdetails.canBump) { - var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) - } else { - var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) - } - dialog.open() - } - } - Label { Layout.fillWidth: true text: qsTr('Status') @@ -141,6 +120,53 @@ Pane { text: txdetails.status } + TextHighlightPane { + Layout.fillWidth: true + Layout.columnSpan: 2 + borderColor: constants.colorWarning + visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel + + RowLayout { + width: parent.width + Label { + Layout.fillWidth: true + text: qsTr('This transaction is still unconfirmed.') + '\n' + + qsTr('You can increase fees to speed up the transaction, or cancel this transaction') + wrapMode: Text.Wrap + } + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + FlatButton { + id: feebumpButton + visible: txdetails.canBump || txdetails.canCpfp + textUnderIcon: false + icon.source: '../../icons/add.png' + text: qsTr('Bump fee') + onClicked: { + if (txdetails.canBump) { + var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) + } else { + var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + } + dialog.open() + } + } + FlatButton { + id: cancelButton + visible: txdetails.canCancel + textUnderIcon: false + icon.source: '../../icons/closebutton.png' + text: qsTr('Cancel Tx') + onClicked: { + var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) + dialog.open() + } + } + } + } + + } + Label { text: qsTr('Mempool depth') color: Material.accentColor @@ -317,30 +343,29 @@ Pane { ButtonContainer { Layout.fillWidth: true - visible: txdetails.canSign || txdetails.canBroadcast FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 + icon.source: '../../icons/key.png' text: qsTr('Sign') - enabled: txdetails.canSign + visible: txdetails.canSign onClicked: txdetails.sign() } + FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 + icon.source: '../../icons/microphone.png' text: qsTr('Broadcast') - enabled: txdetails.canBroadcast + visible: txdetails.canBroadcast onClicked: txdetails.broadcast() } - } - - ButtonContainer { - Layout.fillWidth: true FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 + icon.source: '../../icons/qrcode_white.png' text: qsTr('Export') onClicked: { var dialog = exportTxDialog.createObject(root, { txdetails: txdetails }) @@ -351,6 +376,7 @@ Pane { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 + icon.source: '../../icons/save.png' text: qsTr('Save') visible: txdetails.canSaveAsLocal onClicked: txdetails.save() @@ -359,21 +385,12 @@ Pane { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 + icon.source: '../../icons/delete.png' text: qsTr('Remove') visible: txdetails.canRemove onClicked: txdetails.removeLocalTx() } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Cancel Tx') - visible: txdetails.canCancel - onClicked: { - var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) - dialog.open() - } - } } } From e91c45e61154451101f5dab55fdc03c658703fa9 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 16:20:18 +0100 Subject: [PATCH 0218/1143] qml: text change 'Change' to 'Modify' --- electrum/gui/qml/components/Preferences.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index b30fc8692..2047b4b80 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -203,7 +203,7 @@ Pane { } } Button { - text: qsTr('Change') + text: qsTr('Modify') visible: Config.pinCode != '' onClicked: { var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode}) From 9a3e533096d235de50d04b4809e53f3b8903e7c8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 28 Feb 2023 16:31:21 +0100 Subject: [PATCH 0219/1143] qml: remove requests button again --- electrum/gui/qml/components/ReceiveDialog.qml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 3d84dac6e..290f89533 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -235,18 +235,6 @@ ElDialog { id: buttons Layout.fillWidth: true - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - icon.source: '../../icons/tab_receive.png' - text: qsTr('Requests') - onClicked: { - dialog.close() - if (app.stack.currentItem.objectName != 'ReceiveRequests') - app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) - } - } FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 From 33c7ecbaf8775d41115986eab4890635e0e67710 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 1 Mar 2023 11:07:12 +0100 Subject: [PATCH 0220/1143] utxo details: show list of parents as a tree --- electrum/gui/qt/utxo_dialog.py | 45 +++++++++++++++++++++++++--------- electrum/wallet.py | 5 ++-- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 495197459..3979d075c 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -24,6 +24,7 @@ # SOFTWARE. from typing import TYPE_CHECKING +import copy from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QTextCharFormat, QFont @@ -53,11 +54,8 @@ def __init__(self, window: 'ElectrumWindow', utxo): txid = self.utxo.prevout.txid.hex() parents = self.wallet.get_tx_parents(txid) - out = [] - for _txid, _list in sorted(parents.items()): - tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) - label = self.wallet.get_label_for_txid(_txid) or "" - out.append((tx_height, tx_pos, _txid, label, _list)) + num_parents = len(parents) + parents_copy = copy.deepcopy(parents) self.parents_list = QTextBrowser() self.parents_list.setOpenLinks(False) # disable automatic link opening @@ -72,9 +70,27 @@ def __init__(self, window: 'ElectrumWindow', utxo): cursor = self.parents_list.textCursor() ext = QTextCharFormat() - for tx_height, tx_pos, _txid, label, _list in reversed(sorted(out)): + if num_parents < 200: + ASCII_EDGE = '└─' + ASCII_BRANCH = '├─' + ASCII_PIPE = '| ' + ASCII_SPACE = ' ' + else: + ASCII_EDGE = '└' + ASCII_BRANCH = '├' + ASCII_PIPE = '|' + ASCII_SPACE = ' ' + + def print_ascii_tree(_txid, prefix, is_last): + if _txid not in parents: + return + tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) key = "%dx%d"%(tx_height, tx_pos) if tx_pos >= 0 else _txid[0:8] - list_str = ','.join(filter(None, _list)) + label = self.wallet.get_label_for_txid(_txid) or "" + if _txid not in parents_copy: + label = '[duplicate]' + c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH) + cursor.insertText(prefix + c, ext) lnk = QTextCharFormat() lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setAnchorHref(_txid) @@ -82,15 +98,20 @@ def __init__(self, window: 'ElectrumWindow', utxo): lnk.setAnchor(True) lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) cursor.insertText(key, lnk) - cursor.insertText("\t", ext) - cursor.insertText("%-32s\t<- "%label[0:32], ext) - cursor.insertText(list_str, ext) + cursor.insertText(" ", ext) + cursor.insertText(label, ext) cursor.insertBlock() - + next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE) + parents_list = parents_copy.pop(_txid, []) + for i, p in enumerate(parents_list): + is_last = i == len(parents_list) - 1 + print_ascii_tree(p, next_prefix, is_last) + # recursively build the tree + print_ascii_tree(txid, '', False) vbox = QVBoxLayout() vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) - vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(len(parents)))) + vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(num_parents))) vbox.addWidget(self.parents_list) msg = ' '.join([ _("Note: This analysis only shows parent transactions, and does not take address reuse into consideration."), diff --git a/electrum/wallet.py b/electrum/wallet.py index c2812d2b8..fa24fe89b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -864,8 +864,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: def get_tx_parents(self, txid) -> Dict: """ recursively calls itself and returns a flat dict: - txid -> input_index -> prevout - note: this does not take into account address reuse + txid -> list of parent txids """ if not self.is_up_to_date(): return {} @@ -880,8 +879,8 @@ def get_tx_parents(self, txid) -> Dict: parents = [] tx = self.adb.get_transaction(txid) for i, txin in enumerate(tx.inputs()): - parents.append(str(txin.short_id)) _txid = txin.prevout.txid.hex() + parents.append(_txid) if _txid in self._last_full_history.keys(): result.update(self.get_tx_parents(_txid)) result[txid] = parents From b29a63a1f8bef782596f06ee476fc70dcba87c47 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 1 Mar 2023 11:23:46 +0100 Subject: [PATCH 0221/1143] TxEditor: always show preview button --- electrum/gui/qt/confirm_tx_dialog.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 80e6cfe7d..b928cc70a 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -103,7 +103,6 @@ def __init__(self, *, title='', self.set_io_visible(self.config.get('show_tx_io', False)) self.set_fee_edit_visible(self.config.get('show_tx_fee_details', False)) self.set_locktime_visible(self.config.get('show_tx_locktime', False)) - self.set_preview_visible(self.config.get('show_tx_preview_button', False)) self.update_fee_target() self.resize(self.layout().sizeHint()) @@ -366,6 +365,7 @@ def update_fee_fields(self): def create_buttons_bar(self): self.preview_button = QPushButton(_('Preview')) self.preview_button.clicked.connect(self.on_preview) + self.preview_button.setVisible(self.allow_preview) self.ok_button = QPushButton(_('OK')) self.ok_button.clicked.connect(self.on_send) self.ok_button.setDefault(True) @@ -380,9 +380,6 @@ def create_top_bar(self, text): self.m2.setCheckable(True) self.m3 = self.pref_menu.addAction('Edit Locktime', self.toggle_locktime) self.m3.setCheckable(True) - self.m4 = self.pref_menu.addAction('Show Preview Button', self.toggle_preview_button) - self.m4.setCheckable(True) - self.m4.setEnabled(self.allow_preview) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) @@ -418,16 +415,6 @@ def toggle_locktime(self): self.set_locktime_visible(b) self.resize_to_fit_content() - def toggle_preview_button(self): - b = not self.config.get('show_tx_preview_button', False) - self.config.set_key('show_tx_preview_button', b) - self.set_preview_visible(b) - - def set_preview_visible(self, b): - b = b and self.allow_preview - self.preview_button.setVisible(b) - self.m4.setChecked(b) - def set_io_visible(self, b): self.io_widget.setVisible(b) self.m1.setChecked(b) From 3c5774a189fd2922e4e1388f9b32db02d4bcf0dc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 1 Mar 2023 14:45:25 +0000 Subject: [PATCH 0222/1143] qt tx dialog: fix for ln-related txs when --offline Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 255, in _open_internal_link self.main_window.do_process_from_txid(txid=target, parent=self) File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 2212, in do_process_from_txid self.show_transaction(tx) File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1079, in show_transaction show_transaction(tx, parent=self, desc=tx_desc) File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 351, in show_transaction d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved) File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 449, in __init__ self.update() File "/home/user/wspace/electrum/electrum/gui/qt/transaction_dialog.py", line 667, in update tx_mined_status = self.wallet.lnworker.lnwatcher.adb.get_tx_height(txid) AttributeError: 'NoneType' object has no attribute 'adb' --- electrum/gui/qt/transaction_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 9399ecd2f..11303fb9b 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -664,7 +664,7 @@ def update(self): item = lnworker_history[txid] ln_amount = item['amount_msat'] / 1000 if amount is None: - tx_mined_status = self.wallet.lnworker.lnwatcher.adb.get_tx_height(txid) + tx_mined_status = self.wallet.adb.get_tx_height(txid) else: ln_amount = None self.broadcast_button.setEnabled(tx_details.can_broadcast) From f6dc72899a853a7f7e5692e44a8e58e249b02f68 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 1 Mar 2023 15:27:13 +0000 Subject: [PATCH 0223/1143] lnsweep: use chan.logger instead of module _logger to log the chan id for free --- electrum/lnchannel.py | 4 ++-- electrum/lnsweep.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index a7db8d5fa..5458a2b0c 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -268,10 +268,10 @@ def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]: their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx) if our_sweep_info is not None: self._sweep_info[txid] = our_sweep_info - self.logger.info(f'we force closed') + self.logger.info(f'we (local) force closed') elif their_sweep_info is not None: self._sweep_info[txid] = their_sweep_info - self.logger.info(f'they force closed.') + self.logger.info(f'they (remote) force closed.') else: self._sweep_info[txid] = {} self.logger.info(f'not sure who closed.') diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index b896863b0..3190ea9df 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -26,6 +26,7 @@ _logger = get_logger(__name__) +# note: better to use chan.logger instead, when applicable class SweepInfo(NamedTuple): @@ -215,7 +216,7 @@ def create_sweeptxs_for_our_ctx( found_to_remote = False if not found_to_local and not found_to_remote: return - _logger.debug(f'found our ctx: {to_local_address} {to_remote_address}') + chan.logger.debug(f'(lnsweep) found our ctx: {to_local_address} {to_remote_address}') # other outputs are htlcs # if they are spent, we need to generate the script # so, second-stage htlc sweep should not be returned here @@ -241,7 +242,7 @@ def create_sweeptxs_for_our_ctx( gen_tx=sweep_tx) we_breached = ctn < chan.get_oldest_unrevoked_ctn(LOCAL) if we_breached: - _logger.info("we breached.") + chan.logger.info(f"(lnsweep) we breached. txid: {ctx.txid()}") # return only our_ctx_to_local, because we don't keep htlc_signatures for old states return txs @@ -328,7 +329,7 @@ def analyze_ctx(chan: 'Channel', ctx: Transaction): return their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) is_revocation = True - #_logger.info(f'tx for revoked: {list(txs.keys())}') + #chan.logger.debug(f'(lnsweep) tx for revoked: {list(txs.keys())}') elif chan.get_data_loss_protect_remote_pcp(ctn): their_pcp = chan.get_data_loss_protect_remote_pcp(ctn) is_revocation = False @@ -368,7 +369,7 @@ def create_sweeptxs_for_their_ctx( found_to_remote = False if not found_to_local and not found_to_remote: return - _logger.debug(f'found their ctx: {to_local_address} {to_remote_address}') + chan.logger.debug(f'(lnsweep) found their ctx: {to_local_address} {to_remote_address}') if is_revocation: our_revocation_privkey = derive_blinded_privkey(our_conf.revocation_basepoint.privkey, per_commitment_secret) gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address) From d11237d6a1da3cd60e6bfc9189f1305f1f9de984 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 1 Mar 2023 16:20:42 +0000 Subject: [PATCH 0224/1143] lnworker: start watching already redeemed chans if txs are missing This fixes a bug where if one runs `wallet.clear_history()` they would see exceptions later: ``` Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 866, in timer_actions self.update_wallet() File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1021, in update_wallet self.update_tabs() File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1033, in update_tabs self.utxo_list.update() File "/home/user/wspace/electrum/electrum/gui/qt/utxo_list.py", line 103, in update self.refresh_row(name, idx) File "/home/user/wspace/electrum/electrum/gui/qt/utxo_list.py", line 124, in refresh_row parents = self.wallet.get_tx_parents(txid) File "/home/user/wspace/electrum/electrum/wallet.py", line 885, in get_tx_parents result.update(self.get_tx_parents(_txid)) File "/home/user/wspace/electrum/electrum/wallet.py", line 881, in get_tx_parents for i, txin in enumerate(tx.inputs()): AttributeError: 'NoneType' object has no attribute 'inputs' ``` This is related to the privacy analysis, which assumes that for each tx item in the history list we should have the raw tx in the db. This is no longer true after wallet.clear_history(), if the wallet has certain LN channels. E.g. an already redeemed channel that was local-force-closed, as that closing tx is not related to the wallet directly. In commit 3541ecb5765ae4971a7bb8109e4600e58d7f603b, we decided not to watch already redeemed channels. This is potentially good for e.g. privacy, as the server would otherwise see us subscribe to that chan. However it means that after running wallet.clear_history() txs related to the channel but not to the wallet won't be re-downloaded. Instead, now if there are missing txs for a redeemed channel, we start watching it, hence the synchronizer will re-downloaded the txs. --- electrum/lnchannel.py | 23 +++++++++++++++++++++++ electrum/lnworker.py | 4 ++-- electrum/wallet.py | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 5458a2b0c..5329dd271 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -230,6 +230,29 @@ def is_closed(self): def is_redeemed(self): return self.get_state() == ChannelState.REDEEMED + def need_to_subscribe(self) -> bool: + """Whether lnwatcher/synchronizer need to be watching this channel.""" + if not self.is_redeemed(): + return True + # Chan already deeply closed. Still, if some txs are missing, we should sub. + # check we have funding tx + # note: tx might not be directly related to the wallet, e.g. chan opened by remote + if (funding_item := self.get_funding_height()) is None: + return True + if self.lnworker: + funding_txid, funding_height, funding_timestamp = funding_item + if self.lnworker.wallet.adb.get_transaction(funding_txid) is None: + return True + # check we have closing tx + # note: tx might not be directly related to the wallet, e.g. local-fclose + if (closing_item := self.get_closing_height()) is None: + return True + if self.lnworker: + closing_txid, closing_height, closing_timestamp = closing_item + if self.lnworker.wallet.adb.get_transaction(closing_txid) is None: + return True + return False + @abstractmethod def get_close_options(self) -> Sequence[ChanCloseOption]: pass diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 5f6a579c2..5ecaaabb7 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -762,10 +762,10 @@ def start_network(self, network: 'Network'): self.lnrater = LNRater(self, network) for chan in self.channels.values(): - if not chan.is_redeemed(): + if chan.need_to_subscribe(): self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address()) for cb in self.channel_backups.values(): - if not cb.is_redeemed(): + if cb.need_to_subscribe(): self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) for coro in [ diff --git a/electrum/wallet.py b/electrum/wallet.py index 0c112f551..a82d43375 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -878,6 +878,7 @@ def get_tx_parents(self, txid) -> Dict: result = {} parents = [] tx = self.adb.get_transaction(txid) + assert tx, f"cannot find {txid} in db" for i, txin in enumerate(tx.inputs()): _txid = txin.prevout.txid.hex() parents.append(_txid) From 958191013b926f72a6e7d92c38801bdb64d6e69f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 1 Mar 2023 17:13:21 +0000 Subject: [PATCH 0225/1143] qt tx dialog: remove "desc" field, just use wallet.get_label_for_txid --- electrum/gui/qt/channel_details.py | 2 +- electrum/gui/qt/history_list.py | 9 ++------- electrum/gui/qt/main_window.py | 5 ++--- electrum/gui/qt/transaction_dialog.py | 10 ++++++---- electrum/gui/qt/utxo_dialog.py | 3 +-- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py index 65d8a3f0f..970ce9ed9 100644 --- a/electrum/gui/qt/channel_details.py +++ b/electrum/gui/qt/channel_details.py @@ -172,7 +172,7 @@ def show_tx(self, link_text: str): if not tx: self.show_error(_("Transaction not found.")) return - self.window.show_transaction(tx, tx_desc=_('Transaction')) + self.window.show_transaction(tx) def get_common_form(self, chan): form = QtWidgets.QFormLayout(None) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 66476723f..59c7dc3ef 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -676,12 +676,7 @@ def mouseDoubleClickEvent(self, event: QMouseEvent): tx = self.wallet.adb.get_transaction(tx_hash) if not tx: return - self.show_transaction(tx_item, tx) - - def show_transaction(self, tx_item, tx): - tx_hash = tx_item['txid'] - label = self.wallet.get_label_for_txid(tx_hash) or None # prefer 'None' if not defined (force tx dialog to hide Description field if missing) - self.parent.show_transaction(tx, tx_desc=label) + self.parent.show_transaction(tx) def add_copy_menu(self, menu, idx): cc = menu.addMenu(_("Copy")) @@ -735,7 +730,7 @@ def create_menu(self, position: QPoint): # TODO use siblingAtColumn when min Qt version is >=5.11 persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) menu_edit.addAction(_("{}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) - menu.addAction(_("View Transaction"), lambda: self.show_transaction(tx_item, tx)) + menu.addAction(_("View Transaction"), lambda: self.parent.show_transaction(tx)) channel_id = tx_item.get('channel_id') if channel_id and self.wallet.lnworker and (chan := self.wallet.lnworker.get_channel_by_id(bytes.fromhex(channel_id))): menu.addAction(_("View Channel"), lambda: self.parent.show_channel_details(chan)) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 18590f936..f71d09f82 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1074,9 +1074,8 @@ def show_channel_details(self, chan): from .channel_details import ChannelDetailsDialog ChannelDetailsDialog(self, chan).show() - def show_transaction(self, tx, *, tx_desc=None): - '''tx_desc is set only for txs created in the Send tab''' - show_transaction(tx, parent=self, desc=tx_desc) + def show_transaction(self, tx: Transaction): + show_transaction(tx, parent=self) def show_lightning_transaction(self, tx_item): from .lightning_tx_dialog import LightningTxDialog diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 11303fb9b..0c6ee566b 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -346,9 +346,9 @@ def on_context_menu_for_outputs(self, pos: QPoint): -def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, prompt_if_unsaved=False): +def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved=False): try: - d = TxDialog(tx, parent=parent, desc=desc, prompt_if_unsaved=prompt_if_unsaved) + d = TxDialog(tx, parent=parent, prompt_if_unsaved=prompt_if_unsaved) except SerializationError as e: _logger.exception('unable to deserialize the transaction') parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) @@ -360,7 +360,7 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, pr class TxDialog(QDialog, MessageBoxMixin): - def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, external_keypairs=None): + def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved, external_keypairs=None): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' @@ -373,7 +373,9 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved self.saved = False - self.desc = desc + self.desc = None + if txid := tx.txid(): + self.desc = self.wallet.get_label_for_txid(txid) or None self.setMinimumWidth(640) self.psbt_only_widgets = [] # type: List[QWidget] diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 3979d075c..8a37d2cd5 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -130,5 +130,4 @@ def open_tx(self, txid): tx = self.wallet.adb.get_transaction(txid) if not tx: return - label = self.wallet.get_label_for_txid(txid) - self.main_window.show_transaction(tx, tx_desc=label) + self.main_window.show_transaction(tx) From 9d3f53932b97a053e8060ed6300549edd5ebdf98 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 11:34:32 +0000 Subject: [PATCH 0226/1143] add descriptor.py from bitcoin-core/HWI --- electrum/bip32.py | 83 ++++++ electrum/descriptor.py | 637 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 720 insertions(+) create mode 100644 electrum/descriptor.py diff --git a/electrum/bip32.py b/electrum/bip32.py index 796777081..f27952e0d 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -2,7 +2,9 @@ # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php +import binascii import hashlib +import struct from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional from .util import bfh, BitcoinException @@ -426,3 +428,84 @@ def is_xkey_consistent_with_key_origin_info(xkey: str, *, if bfh(root_fingerprint) != bip32node.fingerprint: return False return True + + +class KeyOriginInfo: + """ + Object representing the origin of a key. + + from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/key.py + # Copyright (c) 2020 The HWI developers + # Distributed under the MIT software license. + """ + def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None: + """ + :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from + :param path: The derivation path to reach this key from the key at ``fingerprint`` + """ + self.fingerprint: bytes = fingerprint + self.path: Sequence[int] = path + + @classmethod + def deserialize(cls, s: bytes) -> 'KeyOriginInfo': + """ + Deserialize a serialized KeyOriginInfo. + They will be serialized in the same way that PSBTs serialize derivation paths + """ + fingerprint = s[0:4] + s = s[4:] + path = list(struct.unpack("<" + "I" * (len(s) // 4), s)) + return cls(fingerprint, path) + + def serialize(self) -> bytes: + """ + Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs + """ + r = self.fingerprint + r += struct.pack("<" + "I" * len(self.path), *self.path) + return r + + def _path_string(self) -> str: + strpath = self.get_derivation_path() + if len(strpath) >= 2: + assert strpath.startswith("m/") + return strpath[1:] # cut leading "m" + + def to_string(self) -> str: + """ + Return the KeyOriginInfo as a string in the form ///... + This is the same way that KeyOriginInfo is shown in descriptors + """ + s = binascii.hexlify(self.fingerprint).decode() + s += self._path_string() + return s + + @classmethod + def from_string(cls, s: str) -> 'KeyOriginInfo': + """ + Create a KeyOriginInfo from the string + :param s: The string to parse + """ + s = s.lower() + entries = s.split("/") + fingerprint = binascii.unhexlify(s[0:8]) + path: Sequence[int] = [] + if len(entries) > 1: + path = convert_bip32_path_to_list_of_uint32(s[9:]) + return cls(fingerprint, path) + + def get_derivation_path(self) -> str: + """ + Return the string for just the path + """ + return convert_bip32_intpath_to_strpath(self.path) + + def get_full_int_list(self) -> List[int]: + """ + Return a list of ints representing this KeyOriginInfo. + The first int is the fingerprint, followed by the path + """ + xfp = [struct.unpack(" int: + """ + :meta private: + Function to compute modulo over the polynomial used for descriptor checksums + From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp + """ + c0 = c >> 35 + c = ((c & 0x7ffffffff) << 5) ^ val + if (c0 & 1): + c ^= 0xf5dee51989 + if (c0 & 2): + c ^= 0xa9fdca3312 + if (c0 & 4): + c ^= 0x1bab10e32d + if (c0 & 8): + c ^= 0x3706b1677a + if (c0 & 16): + c ^= 0x644d626ffd + return c + +def DescriptorChecksum(desc: str) -> str: + """ + Compute the checksum for a descriptor + + :param desc: The descriptor string to compute a checksum for + :return: A checksum + """ + INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " + CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + c = 1 + cls = 0 + clscount = 0 + for ch in desc: + pos = INPUT_CHARSET.find(ch) + if pos == -1: + return "" + c = PolyMod(c, pos & 31) + cls = cls * 3 + (pos >> 5) + clscount += 1 + if clscount == 3: + c = PolyMod(c, cls) + cls = 0 + clscount = 0 + if clscount > 0: + c = PolyMod(c, cls) + for j in range(0, 8): + c = PolyMod(c, 0) + c ^= 1 + + ret = [''] * 8 + for j in range(0, 8): + ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] + return ''.join(ret) + +def AddChecksum(desc: str) -> str: + """ + Compute and attach the checksum for a descriptor + + :param desc: The descriptor string to add a checksum to + :return: Descriptor with checksum + """ + return desc + "#" + DescriptorChecksum(desc) + + +class PubkeyProvider(object): + """ + A public key expression in a descriptor. + Can contain the key origin info, the pubkey itself, and subsequent derivation paths for derivation from the pubkey + The pubkey can be a typical pubkey or an extended pubkey. + """ + def __init__( + self, + origin: Optional['KeyOriginInfo'], + pubkey: str, + deriv_path: Optional[str] + ) -> None: + """ + :param origin: The key origin if one is available + :param pubkey: The public key. Either a hex string or a serialized extended pubkey + :param deriv_path: Additional derivation path (suffix) if the pubkey is an extended pubkey + """ + self.origin = origin + self.pubkey = pubkey + self.deriv_path = deriv_path + + # Make ExtendedKey from pubkey if it isn't hex + self.extkey = None + try: + unhexlify(self.pubkey) + # Is hex, normal pubkey + except Exception: + # Not hex, maybe xpub + self.extkey = BIP32Node.from_xkey(pubkey) + + @classmethod + def parse(cls, s: str) -> 'PubkeyProvider': + """ + Deserialize a key expression from the string into a ``PubkeyProvider``. + + :param s: String containing the key expression + :return: A new ``PubkeyProvider`` containing the details given by ``s`` + """ + origin = None + deriv_path = None + + if s[0] == "[": + end = s.index("]") + origin = KeyOriginInfo.from_string(s[1:end]) + s = s[end + 1:] + + pubkey = s + slash_idx = s.find("/") + if slash_idx != -1: + pubkey = s[:slash_idx] + deriv_path = s[slash_idx:] + + return cls(origin, pubkey, deriv_path) + + def to_string(self) -> str: + """ + Serialize the pubkey expression to a string to be used in a descriptor + + :return: The pubkey expression as a string + """ + s = "" + if self.origin: + s += "[{}]".format(self.origin.to_string()) + s += self.pubkey + if self.deriv_path: + s += self.deriv_path + return s + + def get_pubkey_bytes(self, pos: int) -> bytes: + if self.extkey is not None: + if self.deriv_path is not None: + path_str = self.deriv_path[1:] + if path_str[-1] == "*": + path_str = path_str[:-1] + str(pos) + path = convert_bip32_path_to_list_of_uint32(path_str) + child_key = self.extkey.subkey_at_public_derivation(path) + return child_key.eckey.get_public_key_bytes() + else: + return self.extkey.eckey.get_public_key_bytes() + return unhexlify(self.pubkey) + + def get_full_derivation_path(self, pos: int) -> str: + """ + Returns the full derivation path at the given position, including the origin + """ + path = self.origin.get_derivation_path() if self.origin is not None else "m/" + path += self.deriv_path if self.deriv_path is not None else "" + if path[-1] == "*": + path = path[:-1] + str(pos) + return path + + def get_full_derivation_int_list(self, pos: int) -> List[int]: + """ + Returns the full derivation path as an integer list at the given position. + Includes the origin and master key fingerprint as an int + """ + path: List[int] = self.origin.get_full_int_list() if self.origin is not None else [] + if self.deriv_path is not None: + der_split = self.deriv_path.split("/") + for p in der_split: + if not p: + continue + if p == "*": + i = pos + elif p[-1] in "'phHP": + assert len(p) >= 2 + i = int(p[:-1]) | 0x80000000 + else: + i = int(p) + path.append(i) + return path + + def __lt__(self, other: 'PubkeyProvider') -> bool: + return self.pubkey < other.pubkey + + +class Descriptor(object): + r""" + An abstract class for Descriptors themselves. + Descriptors can contain multiple :class:`PubkeyProvider`\ s and multiple ``Descriptor`` as subdescriptors. + """ + def __init__( + self, + pubkeys: List['PubkeyProvider'], + subdescriptors: List['Descriptor'], + name: str + ) -> None: + r""" + :param pubkeys: The :class:`PubkeyProvider`\ s that are part of this descriptor + :param subdescriptor: The ``Descriptor``\ s that are part of this descriptor + :param name: The name of the function for this descriptor + """ + self.pubkeys = pubkeys + self.subdescriptors = subdescriptors + self.name = name + + def to_string_no_checksum(self) -> str: + """ + Serializes the descriptor as a string without the descriptor checksum + + :return: The descriptor string + """ + return "{}({}{})".format( + self.name, + ",".join([p.to_string() for p in self.pubkeys]), + self.subdescriptors[0].to_string_no_checksum() if len(self.subdescriptors) > 0 else "" + ) + + def to_string(self) -> str: + """ + Serializes the descriptor as a string with the checksum + + :return: The descriptor with a checksum + """ + return AddChecksum(self.to_string_no_checksum()) + + def expand(self, pos: int) -> "ExpandedScripts": + """ + Returns the scripts for a descriptor at the given `pos` for ranged descriptors. + """ + raise NotImplementedError("The Descriptor base class does not implement this method") + + +class PKDescriptor(Descriptor): + """ + A descriptor for ``pk()`` descriptors + """ + def __init__( + self, + pubkey: 'PubkeyProvider' + ) -> None: + """ + :param pubkey: The :class:`PubkeyProvider` for this descriptor + """ + super().__init__([pubkey], [], "pk") + + +class PKHDescriptor(Descriptor): + """ + A descriptor for ``pkh()`` descriptors + """ + def __init__( + self, + pubkey: 'PubkeyProvider' + ) -> None: + """ + :param pubkey: The :class:`PubkeyProvider` for this descriptor + """ + super().__init__([pubkey], [], "pkh") + + def expand(self, pos: int) -> "ExpandedScripts": + script = b"\x76\xa9\x14" + hash_160(self.pubkeys[0].get_pubkey_bytes(pos)) + b"\x88\xac" + return ExpandedScripts(script, None, None) + + +class WPKHDescriptor(Descriptor): + """ + A descriptor for ``wpkh()`` descriptors + """ + def __init__( + self, + pubkey: 'PubkeyProvider' + ) -> None: + """ + :param pubkey: The :class:`PubkeyProvider` for this descriptor + """ + super().__init__([pubkey], [], "wpkh") + + def expand(self, pos: int) -> "ExpandedScripts": + script = b"\x00\x14" + hash_160(self.pubkeys[0].get_pubkey_bytes(pos)) + return ExpandedScripts(script, None, None) + + +class MultisigDescriptor(Descriptor): + """ + A descriptor for ``multi()`` and ``sortedmulti()`` descriptors + """ + def __init__( + self, + pubkeys: List['PubkeyProvider'], + thresh: int, + is_sorted: bool + ) -> None: + r""" + :param pubkeys: The :class:`PubkeyProvider`\ s for this descriptor + :param thresh: The number of keys required to sign this multisig + :param is_sorted: Whether this is a ``sortedmulti()`` descriptor + """ + super().__init__(pubkeys, [], "sortedmulti" if is_sorted else "multi") + self.thresh = thresh + self.is_sorted = is_sorted + if self.is_sorted: + self.pubkeys.sort() + + def to_string_no_checksum(self) -> str: + return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) + + def expand(self, pos: int) -> "ExpandedScripts": + if self.thresh > 16: + m = b"\x01" + self.thresh.to_bytes(1, "big") + else: + m = (self.thresh + 0x50).to_bytes(1, "big") if self.thresh > 0 else b"\x00" + n = (len(self.pubkeys) + 0x50).to_bytes(1, "big") if len(self.pubkeys) > 0 else b"\x00" + script: bytes = m + der_pks = [p.get_pubkey_bytes(pos) for p in self.pubkeys] + if self.is_sorted: + der_pks.sort() + for pk in der_pks: + script += len(pk).to_bytes(1, "big") + pk + script += n + b"\xae" + + return ExpandedScripts(script, None, None) + + +class SHDescriptor(Descriptor): + """ + A descriptor for ``sh()`` descriptors + """ + def __init__( + self, + subdescriptor: 'Descriptor' + ) -> None: + """ + :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor + """ + super().__init__([], [subdescriptor], "sh") + + def expand(self, pos: int) -> "ExpandedScripts": + assert len(self.subdescriptors) == 1 + redeem_script, _, witness_script = self.subdescriptors[0].expand(pos) + script = b"\xa9\x14" + hash_160(redeem_script) + b"\x87" + return ExpandedScripts(script, redeem_script, witness_script) + + +class WSHDescriptor(Descriptor): + """ + A descriptor for ``wsh()`` descriptors + """ + def __init__( + self, + subdescriptor: 'Descriptor' + ) -> None: + """ + :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor + """ + super().__init__([], [subdescriptor], "wsh") + + def expand(self, pos: int) -> "ExpandedScripts": + assert len(self.subdescriptors) == 1 + witness_script, _, _ = self.subdescriptors[0].expand(pos) + script = b"\x00\x20" + sha256(witness_script) + return ExpandedScripts(script, None, witness_script) + + +class TRDescriptor(Descriptor): + """ + A descriptor for ``tr()`` descriptors + """ + def __init__( + self, + internal_key: 'PubkeyProvider', + subdescriptors: List['Descriptor'] = None, + depths: List[int] = None, + ) -> None: + r""" + :param internal_key: The :class:`PubkeyProvider` that is the internal key for this descriptor + :param subdescriptors: The :class:`Descriptor`\ s that are the leaf scripts for this descriptor + :param depths: The depths of the leaf scripts in the same order as `subdescriptors` + """ + if subdescriptors is None: + subdescriptors = [] + if depths is None: + depths = [] + super().__init__([internal_key], subdescriptors, "tr") + self.depths = depths + + def to_string_no_checksum(self) -> str: + r = f"{self.name}({self.pubkeys[0].to_string()}" + path: List[bool] = [] # Track left or right for each depth + for p, depth in enumerate(self.depths): + r += "," + while len(path) <= depth: + if len(path) > 0: + r += "{" + path.append(False) + r += self.subdescriptors[p].to_string_no_checksum() + while len(path) > 0 and path[-1]: + if len(path) > 0: + r += "}" + path.pop() + if len(path) > 0: + path[-1] = True + r += ")" + return r + +def _get_func_expr(s: str) -> Tuple[str, str]: + """ + Get the function name and then the expression inside + + :param s: The string that begins with a function name + :return: The function name as the first element of the tuple, and the expression contained within the function as the second element + :raises: ValueError: if a matching pair of parentheses cannot be found + """ + start = s.index("(") + end = s.rindex(")") + return s[0:start], s[start + 1:end] + + +def _get_const(s: str, const: str) -> str: + """ + Get the first character of the string, make sure it is the expected character, + and return the rest of the string + + :param s: The string that begins with a constant character + :param const: The constant character + :return: The remainder of the string without the constant character + :raises: ValueError: if the first character is not the constant character + """ + if s[0] != const: + raise ValueError(f"Expected '{const}' but got '{s[0]}'") + return s[1:] + + +def _get_expr(s: str) -> Tuple[str, str]: + """ + Extract the expression that ``s`` begins with. + + This will return the initial part of ``s``, up to the first comma or closing brace, + skipping ones that are surrounded by braces. + + :param s: The string to extract the expression from + :return: A pair with the first item being the extracted expression and the second the rest of the string + """ + level: int = 0 + for i, c in enumerate(s): + if c in ["(", "{"]: + level += 1 + elif level > 0 and c in [")", "}"]: + level -= 1 + elif level == 0 and c in [")", "}", ","]: + break + return s[0:i], s[i:] + +def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]: + """ + Parses an individual pubkey expression from a string that may contain more than one pubkey expression. + + :param expr: The expression to parse a pubkey expression from + :return: The :class:`PubkeyProvider` that is parsed as the first item of a tuple, and the remainder of the expression as the second item. + """ + end = len(expr) + comma_idx = expr.find(",") + next_expr = "" + if comma_idx != -1: + end = comma_idx + next_expr = expr[end + 1:] + return PubkeyProvider.parse(expr[:end]), next_expr + + +class _ParseDescriptorContext(Enum): + """ + :meta private: + + Enum representing the level that we are in when parsing a descriptor. + Some expressions aren't allowed at certain levels, this helps us track those. + """ + + TOP = 1 + """The top level, not within any descriptor""" + + P2SH = 2 + """Within a ``sh()`` descriptor""" + + P2WSH = 3 + """Within a ``wsh()`` descriptor""" + + P2TR = 4 + """Within a ``tr()`` descriptor""" + + +def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor': + """ + :meta private: + + Parse a descriptor given the context level we are in. + Used recursively to parse subdescriptors + + :param desc: The descriptor string to parse + :param ctx: The :class:`_ParseDescriptorContext` indicating the level we are in + :return: The parsed descriptor + :raises: ValueError: if the descriptor is malformed + """ + func, expr = _get_func_expr(desc) + if func == "pk": + pubkey, expr = parse_pubkey(expr) + if expr: + raise ValueError("more than one pubkey in pk descriptor") + return PKDescriptor(pubkey) + if func == "pkh": + if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH): + raise ValueError("Can only have pkh at top level, in sh(), or in wsh()") + pubkey, expr = parse_pubkey(expr) + if expr: + raise ValueError("More than one pubkey in pkh descriptor") + return PKHDescriptor(pubkey) + if func == "sortedmulti" or func == "multi": + if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH): + raise ValueError("Can only have multi/sortedmulti at top level, in sh(), or in wsh()") + is_sorted = func == "sortedmulti" + comma_idx = expr.index(",") + thresh = int(expr[:comma_idx]) + expr = expr[comma_idx + 1:] + pubkeys = [] + while expr: + pubkey, expr = parse_pubkey(expr) + pubkeys.append(pubkey) + if len(pubkeys) == 0 or len(pubkeys) > 16: + raise ValueError("Cannot have {} keys in a multisig; must have between 1 and 16 keys, inclusive".format(len(pubkeys))) + elif thresh < 1: + raise ValueError("Multisig threshold cannot be {}, must be at least 1".format(thresh)) + elif thresh > len(pubkeys): + raise ValueError("Multisig threshold cannot be larger than the number of keys; threshold is {} but only {} keys specified".format(thresh, len(pubkeys))) + if ctx == _ParseDescriptorContext.TOP and len(pubkeys) > 3: + raise ValueError("Cannot have {} pubkeys in bare multisig: only at most 3 pubkeys") + return MultisigDescriptor(pubkeys, thresh, is_sorted) + if func == "wpkh": + if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH): + raise ValueError("Can only have wpkh() at top level or inside sh()") + pubkey, expr = parse_pubkey(expr) + if expr: + raise ValueError("More than one pubkey in pkh descriptor") + return WPKHDescriptor(pubkey) + if func == "sh": + if ctx != _ParseDescriptorContext.TOP: + raise ValueError("Can only have sh() at top level") + subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2SH) + return SHDescriptor(subdesc) + if func == "wsh": + if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH): + raise ValueError("Can only have wsh() at top level or inside sh()") + subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2WSH) + return WSHDescriptor(subdesc) + if func == "tr": + if ctx != _ParseDescriptorContext.TOP: + raise ValueError("Can only have tr at top level") + internal_key, expr = parse_pubkey(expr) + subscripts = [] + depths = [] + if expr: + # Path from top of the tree to what we're currently processing. + # branches[i] == False: left branch in the i'th step from the top + # branches[i] == true: right branch + branches = [] + while True: + # Process open braces + while True: + try: + expr = _get_const(expr, "{") + branches.append(False) + except ValueError: + break + if len(branches) > MAX_TAPROOT_NODES: + raise ValueError(f"tr() supports at most {MAX_TAPROOT_NODES} nesting levels") # TODO xxxx fixed upstream bug here + # Process script expression + sarg, expr = _get_expr(expr) + subscripts.append(_parse_descriptor(sarg, _ParseDescriptorContext.P2TR)) + depths.append(len(branches)) + # Process closing braces + while len(branches) > 0 and branches[-1]: + expr = _get_const(expr, "}") + branches.pop() + # If we're at the end of a left branch, expect a comma + if len(branches) > 0 and not branches[-1]: + expr = _get_const(expr, ",") + branches[-1] = True + + if len(branches) == 0: + break + return TRDescriptor(internal_key, subscripts, depths) + if ctx == _ParseDescriptorContext.P2SH: + raise ValueError("A function is needed within P2SH") + elif ctx == _ParseDescriptorContext.P2WSH: + raise ValueError("A function is needed within P2WSH") + raise ValueError("{} is not a valid descriptor function".format(func)) + + +def parse_descriptor(desc: str) -> 'Descriptor': + """ + Parse a descriptor string into a :class:`Descriptor`. + Validates the checksum if one is provided in the string + + :param desc: The descriptor string + :return: The parsed :class:`Descriptor` + :raises: ValueError: if the descriptor string is malformed + """ + i = desc.find("#") + if i != -1: + checksum = desc[i + 1:] + desc = desc[:i] + computed = DescriptorChecksum(desc) + if computed != checksum: + raise ValueError("The checksum does not match; Got {}, expected {}".format(checksum, computed)) + return _parse_descriptor(desc, _ParseDescriptorContext.TOP) + From 001ca775a98e58747cf13b945cc42309c2849127 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 11:36:47 +0000 Subject: [PATCH 0227/1143] descriptor.py: speed-up DescriptorChecksum a bit --- electrum/descriptor.py | 15 +++++++++------ electrum/segwit_addr.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index ca3ba9118..fa7aa1354 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -46,6 +46,11 @@ def PolyMod(c: int, val: int) -> int: c ^= 0x644d626ffd return c + +_INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " +_INPUT_CHARSET_INV = {c: i for (i, c) in enumerate(_INPUT_CHARSET)} +_CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + def DescriptorChecksum(desc: str) -> str: """ Compute the checksum for a descriptor @@ -53,15 +58,13 @@ def DescriptorChecksum(desc: str) -> str: :param desc: The descriptor string to compute a checksum for :return: A checksum """ - INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " - CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" - c = 1 cls = 0 clscount = 0 for ch in desc: - pos = INPUT_CHARSET.find(ch) - if pos == -1: + try: + pos = _INPUT_CHARSET_INV[ch] + except KeyError: return "" c = PolyMod(c, pos & 31) cls = cls * 3 + (pos >> 5) @@ -78,7 +81,7 @@ def DescriptorChecksum(desc: str) -> str: ret = [''] * 8 for j in range(0, 8): - ret[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] + ret[j] = _CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] return ''.join(ret) def AddChecksum(desc: str) -> str: diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py index b13a9175e..9d121c3b9 100644 --- a/electrum/segwit_addr.py +++ b/electrum/segwit_addr.py @@ -25,7 +25,7 @@ from typing import Tuple, Optional, Sequence, NamedTuple, List CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" -_CHARSET_INVERSE = {x: CHARSET.find(x) for x in CHARSET} +_CHARSET_INVERSE = {c: i for (i, c) in enumerate(CHARSET)} BECH32_CONST = 1 BECH32M_CONST = 0x2bc830a3 From d2f75b7da5b4d2c300ae52ecfae7cefe86204b6c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 11:40:42 +0000 Subject: [PATCH 0228/1143] descriptor.py: don't allow ypub/zpub inside descriptors --- electrum/bip32.py | 10 +++++++++- electrum/descriptor.py | 4 ++-- electrum/tests/test_bitcoin.py | 22 ++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index f27952e0d..74ee1aeb8 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -124,7 +124,13 @@ class BIP32Node(NamedTuple): child_number: bytes = b'\x00'*4 @classmethod - def from_xkey(cls, xkey: str, *, net=None) -> 'BIP32Node': + def from_xkey( + cls, + xkey: str, + *, + net=None, + allow_custom_headers: bool = True, # to also accept ypub/zpub + ) -> 'BIP32Node': if net is None: net = constants.net xkey = DecodeBase58Check(xkey) @@ -145,6 +151,8 @@ def from_xkey(cls, xkey: str, *, net=None) -> 'BIP32Node': else: raise InvalidMasterKeyVersionBytes(f'Invalid extended key format: {hex(header)}') xtype = headers_inv[header] + if not allow_custom_headers and xtype != "standard": + raise ValueError(f"only standard xpub/xprv allowed. found custom xtype={xtype}") if is_private: eckey = ecc.ECPrivkey(xkey[13 + 33:]) else: diff --git a/electrum/descriptor.py b/electrum/descriptor.py index fa7aa1354..bacfa71cc 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -121,8 +121,8 @@ def __init__( unhexlify(self.pubkey) # Is hex, normal pubkey except Exception: - # Not hex, maybe xpub - self.extkey = BIP32Node.from_xkey(pubkey) + # Not hex, maybe xpub (but don't allow ypub/zpub) + self.extkey = BIP32Node.from_xkey(pubkey, allow_custom_headers=False) @classmethod def parse(cls, s: str) -> 'PubkeyProvider': diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 08e881ecd..c3026a0d2 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -815,6 +815,28 @@ def test_is_xprv(self): self.assertFalse(is_xprv('xprv1nval1d')) self.assertFalse(is_xprv('xprv661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG')) + def test_bip32_from_xkey(self): + bip32node1 = BIP32Node.from_xkey("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy") + self.assertEqual( + BIP32Node( + xtype='standard', + eckey=ecc.ECPubkey(bytes.fromhex("022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011")), + chaincode=bytes.fromhex("c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e"), + depth=5, + fingerprint=bytes.fromhex("d880d7d8"), + child_number=bytes.fromhex("3b9aca00"), + ), + bip32node1) + with self.assertRaises(ValueError): + BIP32Node.from_xkey( + "zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8", + allow_custom_headers=False) + bip32node2 = BIP32Node.from_xkey( + "zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8", + allow_custom_headers=True) + self.assertEqual(bytes.fromhex("03f18e53f3386a5f9a9d2c369ad3b84b429eb397b4bc69ce600f2d833b54ba32f4"), + bip32node2.eckey.get_public_key_bytes(compressed=True)) + def test_is_bip32_derivation(self): self.assertTrue(is_bip32_derivation("m/0'/1")) self.assertTrue(is_bip32_derivation("m/0'/0'")) From 8f8dd1506ed128130db963b37b7e298e611c4cc6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 11:45:38 +0000 Subject: [PATCH 0229/1143] descriptor.py: add a dozen TODOs --- electrum/descriptor.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index bacfa71cc..f323a2ec5 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -7,15 +7,28 @@ # Output Script Descriptors # See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md # +# TODO allow xprv +# TODO allow WIF privkeys +# TODO impl ADDR descriptors +# TODO impl RAW descriptors +# TODO disable descs we cannot solve: TRDescriptor +# +# TODO tests +# - port https://github.com/bitcoin-core/HWI/blob/master/test/test_descriptor.py +# - ranged descriptors (that have a "*") +# +# TODO solver? integrate with transaction.py... +# Transaction.input_script/get_preimage_script/serialize_witness + from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo from .crypto import hash_160, sha256 from binascii import unhexlify -from collections import namedtuple from enum import Enum from typing import ( List, + NamedTuple, Optional, Tuple, ) @@ -24,7 +37,11 @@ MAX_TAPROOT_NODES = 128 -ExpandedScripts = namedtuple("ExpandedScripts", ["output_script", "redeem_script", "witness_script"]) +class ExpandedScripts(NamedTuple): + output_script: Optional[bytes] = None + redeem_script: Optional[bytes] = None + witness_script: Optional[bytes] = None + def PolyMod(c: int, val: int) -> int: """ @@ -114,6 +131,7 @@ def __init__( self.origin = origin self.pubkey = pubkey self.deriv_path = deriv_path + # TODO check that deriv_path only has a single "*" (and that it is in the last pos. but can end with e.g. "*h") # Make ExtendedKey from pubkey if it isn't hex self.extkey = None @@ -270,6 +288,9 @@ def __init__( """ super().__init__([pubkey], [], "pk") + # TODO + # def expand(self, pos: int) -> "ExpandedScripts": + class PKHDescriptor(Descriptor): """ @@ -286,7 +307,7 @@ def __init__( def expand(self, pos: int) -> "ExpandedScripts": script = b"\x76\xa9\x14" + hash_160(self.pubkeys[0].get_pubkey_bytes(pos)) + b"\x88\xac" - return ExpandedScripts(script, None, None) + return ExpandedScripts(output_script=script) class WPKHDescriptor(Descriptor): @@ -304,7 +325,7 @@ def __init__( def expand(self, pos: int) -> "ExpandedScripts": script = b"\x00\x14" + hash_160(self.pubkeys[0].get_pubkey_bytes(pos)) - return ExpandedScripts(script, None, None) + return ExpandedScripts(output_script=script) class MultisigDescriptor(Descriptor): @@ -345,7 +366,7 @@ def expand(self, pos: int) -> "ExpandedScripts": script += len(pk).to_bytes(1, "big") + pk script += n + b"\xae" - return ExpandedScripts(script, None, None) + return ExpandedScripts(output_script=script) class SHDescriptor(Descriptor): @@ -365,7 +386,11 @@ def expand(self, pos: int) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 redeem_script, _, witness_script = self.subdescriptors[0].expand(pos) script = b"\xa9\x14" + hash_160(redeem_script) + b"\x87" - return ExpandedScripts(script, redeem_script, witness_script) + return ExpandedScripts( + output_script=script, + redeem_script=redeem_script, + witness_script=witness_script, + ) class WSHDescriptor(Descriptor): @@ -385,7 +410,10 @@ def expand(self, pos: int) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 witness_script, _, _ = self.subdescriptors[0].expand(pos) script = b"\x00\x20" + sha256(witness_script) - return ExpandedScripts(script, None, witness_script) + return ExpandedScripts( + output_script=script, + witness_script=witness_script, + ) class TRDescriptor(Descriptor): From f1f39f0e82f74f5f625150f82d0cf801b5d38bc1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 12:14:25 +0000 Subject: [PATCH 0230/1143] descriptors: wallet/transaction: construct intermediate osd --- electrum/bitcoin.py | 12 +-- electrum/commands.py | 7 +- electrum/descriptor.py | 146 ++++++++++++++++++++++++----- electrum/lnsweep.py | 3 + electrum/lnutil.py | 6 ++ electrum/tests/test_transaction.py | 11 ++- electrum/transaction.py | 39 ++++---- electrum/wallet.py | 110 ++++++++++++---------- 8 files changed, 223 insertions(+), 111 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index 525fb5a5a..d5ca43727 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -421,15 +421,9 @@ def p2wsh_nested_script(witness_script: str) -> str: return construct_script([0, wsh]) def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: - if txin_type == 'p2pkh': - return public_key_to_p2pkh(bfh(pubkey), net=net) - elif txin_type == 'p2wpkh': - return public_key_to_p2wpkh(bfh(pubkey), net=net) - elif txin_type == 'p2wpkh-p2sh': - scriptSig = p2wpkh_nested_script(pubkey) - return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net) - else: - raise NotImplementedError(txin_type) + from . import descriptor + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) + return desc.expand().address(net=net) # TODO this method is confusingly named diff --git a/electrum/commands.py b/electrum/commands.py index 960bae252..6d8fc7bf7 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -66,6 +66,7 @@ from . import GuiImportError from . import crypto from . import constants +from . import descriptor if TYPE_CHECKING: from .network import Network @@ -394,6 +395,8 @@ async def serialize(self, jsontx): txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) keypairs[pubkey] = privkey, compressed + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) + txin.script_descriptor = desc txin.script_type = txin_type txin.pubkeys = [bfh(pubkey)] txin.num_sig = 1 @@ -420,9 +423,11 @@ async def signtransaction_with_privkey(self, tx, privkey): for priv in privkey: txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv) pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed) - address = bitcoin.pubkey_to_address(txin_type, pubkey.hex()) + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type) + address = desc.expand().address() if address in txins_dict.keys(): for txin in txins_dict[address]: + txin.script_descriptor = desc txin.pubkeys = [pubkey] txin.script_type = txin_type tx.sign({pubkey.hex(): (priv2, compressed)}) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index f323a2ec5..59abd49be 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -1,4 +1,5 @@ # Copyright (c) 2017 Andrew Chow +# Copyright (c) 2023 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php # @@ -22,6 +23,8 @@ from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo +from . import bitcoin +from .bitcoin import construct_script, opcodes from .crypto import hash_160, sha256 from binascii import unhexlify @@ -31,16 +34,41 @@ NamedTuple, Optional, Tuple, + Sequence, ) MAX_TAPROOT_NODES = 128 -class ExpandedScripts(NamedTuple): - output_script: Optional[bytes] = None - redeem_script: Optional[bytes] = None - witness_script: Optional[bytes] = None +class ExpandedScripts: + + def __init__( + self, + *, + output_script: Optional[bytes] = None, + redeem_script: Optional[bytes] = None, + witness_script: Optional[bytes] = None, + scriptcode_for_sighash: Optional[bytes] = None + ): + self.output_script = output_script + self.redeem_script = redeem_script + self.witness_script = witness_script + self.scriptcode_for_sighash = scriptcode_for_sighash + + @property + def scriptcode_for_sighash(self) -> Optional[bytes]: + if self._scriptcode_for_sighash: + return self._scriptcode_for_sighash + return self.witness_script or self.redeem_script or self.output_script + + @scriptcode_for_sighash.setter + def scriptcode_for_sighash(self, value: Optional[bytes]): + self._scriptcode_for_sighash = value + + def address(self, *, net=None) -> Optional[str]: + if spk := self.output_script: + return bitcoin.script_to_address(spk.hex(), net=net) def PolyMod(c: int, val: int) -> int: @@ -180,18 +208,25 @@ def to_string(self) -> str: s += self.deriv_path return s - def get_pubkey_bytes(self, pos: int) -> bytes: + def get_pubkey_bytes(self, *, pos: Optional[int] = None) -> bytes: + if self.is_range() and pos is None: + raise ValueError("pos must be set for ranged descriptor") if self.extkey is not None: - if self.deriv_path is not None: + compressed = True # bip32 implies compressed pubkeys + if self.deriv_path is None: + assert not self.is_range() + return self.extkey.eckey.get_public_key_bytes(compressed=compressed) + else: path_str = self.deriv_path[1:] - if path_str[-1] == "*": + if self.is_range(): + assert path_str[-1] == "*" path_str = path_str[:-1] + str(pos) path = convert_bip32_path_to_list_of_uint32(path_str) child_key = self.extkey.subkey_at_public_derivation(path) - return child_key.eckey.get_public_key_bytes() - else: - return self.extkey.eckey.get_public_key_bytes() - return unhexlify(self.pubkey) + return child_key.eckey.get_public_key_bytes(compressed=compressed) + else: + assert not self.is_range() + return unhexlify(self.pubkey) def get_full_derivation_path(self, pos: int) -> str: """ @@ -227,6 +262,13 @@ def get_full_derivation_int_list(self, pos: int) -> List[int]: def __lt__(self, other: 'PubkeyProvider') -> bool: return self.pubkey < other.pubkey + def is_range(self) -> bool: + if not self.deriv_path: + return False + if self.deriv_path[-1] == "*": # TODO hardened + return True + return False + class Descriptor(object): r""" @@ -268,12 +310,24 @@ def to_string(self) -> str: """ return AddChecksum(self.to_string_no_checksum()) - def expand(self, pos: int) -> "ExpandedScripts": + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": """ Returns the scripts for a descriptor at the given `pos` for ranged descriptors. """ raise NotImplementedError("The Descriptor base class does not implement this method") + def is_range(self) -> bool: + for pubkey in self.pubkeys: + if pubkey.is_range(): + return True + for desc in self.subdescriptors: + if desc.is_range(): + return True + return False + + def is_segwit(self) -> bool: + return any([desc.is_segwit() for desc in self.subdescriptors]) + class PKDescriptor(Descriptor): """ @@ -288,8 +342,10 @@ def __init__( """ super().__init__([pubkey], [], "pk") - # TODO - # def expand(self, pos: int) -> "ExpandedScripts": + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": + pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos) + script = construct_script([pubkey, opcodes.OP_CHECKSIG]) + return ExpandedScripts(output_script=bytes.fromhex(script)) class PKHDescriptor(Descriptor): @@ -305,9 +361,11 @@ def __init__( """ super().__init__([pubkey], [], "pkh") - def expand(self, pos: int) -> "ExpandedScripts": - script = b"\x76\xa9\x14" + hash_160(self.pubkeys[0].get_pubkey_bytes(pos)) + b"\x88\xac" - return ExpandedScripts(output_script=script) + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": + pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos) + pkh = hash_160(pubkey).hex() + script = bitcoin.pubkeyhash_to_p2pkh_script(pkh) + return ExpandedScripts(output_script=bytes.fromhex(script)) class WPKHDescriptor(Descriptor): @@ -323,9 +381,17 @@ def __init__( """ super().__init__([pubkey], [], "wpkh") - def expand(self, pos: int) -> "ExpandedScripts": - script = b"\x00\x14" + hash_160(self.pubkeys[0].get_pubkey_bytes(pos)) - return ExpandedScripts(output_script=script) + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": + pkh = hash_160(self.pubkeys[0].get_pubkey_bytes(pos=pos)) + output_script = construct_script([0, pkh]) + scriptcode = bitcoin.pubkeyhash_to_p2pkh_script(pkh.hex()) + return ExpandedScripts( + output_script=bytes.fromhex(output_script), + scriptcode_for_sighash=bytes.fromhex(scriptcode), + ) + + def is_segwit(self) -> bool: + return True class MultisigDescriptor(Descriptor): @@ -352,14 +418,14 @@ def __init__( def to_string_no_checksum(self) -> str: return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) - def expand(self, pos: int) -> "ExpandedScripts": + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": if self.thresh > 16: m = b"\x01" + self.thresh.to_bytes(1, "big") else: m = (self.thresh + 0x50).to_bytes(1, "big") if self.thresh > 0 else b"\x00" n = (len(self.pubkeys) + 0x50).to_bytes(1, "big") if len(self.pubkeys) > 0 else b"\x00" script: bytes = m - der_pks = [p.get_pubkey_bytes(pos) for p in self.pubkeys] + der_pks = [p.get_pubkey_bytes(pos=pos) for p in self.pubkeys] if self.is_sorted: der_pks.sort() for pk in der_pks: @@ -382,14 +448,17 @@ def __init__( """ super().__init__([], [subdescriptor], "sh") - def expand(self, pos: int) -> "ExpandedScripts": + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 - redeem_script, _, witness_script = self.subdescriptors[0].expand(pos) + sub_scripts = self.subdescriptors[0].expand(pos=pos) + redeem_script = sub_scripts.output_script + witness_script = sub_scripts.witness_script script = b"\xa9\x14" + hash_160(redeem_script) + b"\x87" return ExpandedScripts( output_script=script, redeem_script=redeem_script, witness_script=witness_script, + scriptcode_for_sighash=sub_scripts.scriptcode_for_sighash, ) @@ -406,15 +475,19 @@ def __init__( """ super().__init__([], [subdescriptor], "wsh") - def expand(self, pos: int) -> "ExpandedScripts": + def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 - witness_script, _, _ = self.subdescriptors[0].expand(pos) + sub_scripts = self.subdescriptors[0].expand(pos=pos) + witness_script = sub_scripts.output_script script = b"\x00\x20" + sha256(witness_script) return ExpandedScripts( output_script=script, witness_script=witness_script, ) + def is_segwit(self) -> bool: + return True + class TRDescriptor(Descriptor): """ @@ -457,6 +530,10 @@ def to_string_no_checksum(self) -> str: r += ")" return r + def is_segwit(self) -> bool: + return True + + def _get_func_expr(s: str) -> Tuple[str, str]: """ Get the function name and then the expression inside @@ -666,3 +743,20 @@ def parse_descriptor(desc: str) -> 'Descriptor': raise ValueError("The checksum does not match; Got {}, expected {}".format(checksum, computed)) return _parse_descriptor(desc, _ParseDescriptorContext.TOP) + +##### + + +def get_singlesig_descriptor_from_legacy_leaf(*, pubkey: str, script_type: str) -> Optional[Descriptor]: + pubkey = PubkeyProvider.parse(pubkey) + if script_type == 'p2pk': + return PKDescriptor(pubkey=pubkey) + elif script_type == 'p2pkh': + return PKHDescriptor(pubkey=pubkey) + elif script_type == 'p2wpkh': + return WPKHDescriptor(pubkey=pubkey) + elif script_type == 'p2wpkh-p2sh': + wpkh = WPKHDescriptor(pubkey=pubkey) + return SHDescriptor(subdescriptor=wpkh) + else: + raise NotImplementedError(f"unexpected {script_type=}") diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 3190ea9df..d9a989242 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -8,6 +8,7 @@ from .util import bfh from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness from .invoices import PR_PAID +from . import descriptor from . import ecc from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey, @@ -513,6 +514,8 @@ def create_sweeptx_their_ctx_to_remote( prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) txin = PartialTxInput(prevout=prevout) txin._trusted_value_sats = val + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey, script_type='p2wpkh') + txin.script_descriptor = desc txin.script_type = 'p2wpkh' txin.pubkeys = [bfh(our_payment_pubkey)] txin.num_sig = 1 diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 025f772c7..8fab2a86f 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -21,6 +21,7 @@ PartialTxOutput, opcodes, TxOutput) from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number from . import ecc, bitcoin, crypto, transaction +from . import descriptor from .bitcoin import (push_script, redeem_script_to_address, address_to_script, construct_witness, construct_script) from . import segwit_addr @@ -818,6 +819,11 @@ def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes # commitment tx input prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos) c_input = PartialTxInput(prevout=prevout) + + ppubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] + multi = descriptor.MultisigDescriptor(pubkeys=ppubkeys, thresh=2, is_sorted=True) + c_input.script_descriptor = descriptor.WSHDescriptor(subdescriptor=multi) + c_input.script_type = 'p2wsh' c_input.pubkeys = [bfh(pk) for pk in pubkeys] c_input.num_sig = 2 diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py index c1bb1bb6f..1e7264aad 100644 --- a/electrum/tests/test_transaction.py +++ b/electrum/tests/test_transaction.py @@ -9,8 +9,9 @@ from electrum.bitcoin import (deserialize_privkey, opcodes, construct_script, construct_witness) from electrum.ecc import ECPrivkey -from .test_bitcoin import disable_ecdsa_r_value_grinding +from electrum import descriptor +from .test_bitcoin import disable_ecdsa_r_value_grinding from . import ElectrumTestCase signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' @@ -89,8 +90,12 @@ def test_match_against_script_template(self): def test_tx_update_signatures(self): tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA") - tx.inputs()[0].script_type = 'p2pkh' - tx.inputs()[0].pubkeys = [bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')] + pubkey = bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6') + script_type = 'p2pkh' + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=script_type) + tx.inputs()[0].script_descriptor = desc + tx.inputs()[0].script_type = script_type + tx.inputs()[0].pubkeys = [pubkey] tx.inputs()[0].num_sig = 1 tx.update_signatures(signed_blob_signatures) self.assertEqual(tx.serialize(), signed_blob) diff --git a/electrum/transaction.py b/electrum/transaction.py index 92c18c321..6cb02cefa 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -52,6 +52,7 @@ from .crypto import sha256d from .logging import get_logger from .util import ShortID +from .descriptor import Descriptor if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -821,6 +822,12 @@ def input_script(self, txin: TxInput, *, estimate_size=False) -> str: if txin.is_native_segwit(): return '' + if desc := txin.script_descriptor: + if desc.is_segwit(): + if redeem_script := desc.expand().redeem_script: + return construct_script([redeem_script]) + return "" + _type = txin.script_type pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) if _type in ('address', 'unknown') and estimate_size: @@ -833,18 +840,10 @@ def input_script(self, txin: TxInput, *, estimate_size=False) -> str: return construct_script([0, *sig_list, redeem_script]) elif _type == 'p2pkh': return construct_script([sig_list[0], pubkeys[0]]) - elif _type in ['p2wpkh', 'p2wsh']: - return '' elif _type == 'p2wpkh-p2sh': + assert estimate_size # otherwise script_descriptor should handle it redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0]) return construct_script([redeem_script]) - elif _type == 'p2wsh-p2sh': - if estimate_size: - witness_script = '' - else: - witness_script = self.get_preimage_script(txin) - redeem_script = bitcoin.p2wsh_nested_script(witness_script) - return construct_script([redeem_script]) raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}') @classmethod @@ -858,18 +857,12 @@ def get_preimage_script(cls, txin: 'PartialTxInput') -> str: raise Exception('OP_CODESEPARATOR black magic is not supported') return txin.redeem_script.hex() - pubkeys = [pk.hex() for pk in txin.pubkeys] - if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - return multisig_script(pubkeys, txin.num_sig) - elif txin.script_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - pubkey = pubkeys[0] - pkh = hash_160(bfh(pubkey)).hex() - return bitcoin.pubkeyhash_to_p2pkh_script(pkh) - elif txin.script_type == 'p2pk': - pubkey = pubkeys[0] - return bitcoin.public_key_to_p2pk_script(pubkey) - else: - raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}') + if desc := txin.script_descriptor: + sc = desc.expand() + if script := sc.scriptcode_for_sighash: + return script.hex() + raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}") + raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}') def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: inputs = self.inputs() @@ -1255,6 +1248,7 @@ def __init__(self, *args, **kwargs): self.witness_script = None # type: Optional[bytes] self._unknown = {} # type: Dict[bytes, bytes] + self.script_descriptor = None # type: Optional[Descriptor] self.script_type = 'unknown' self.num_sig = 0 # type: int # num req sigs for multisig self.pubkeys = [] # type: List[bytes] # note: order matters @@ -1296,6 +1290,7 @@ def to_json(self): 'height': self.block_height, 'value_sats': self.value_sats(), 'address': self.address, + 'desc': self.script_descriptor.to_string() if self.script_descriptor else None, 'utxo': str(self.utxo) if self.utxo else None, 'witness_utxo': self.witness_utxo.serialize_to_network().hex() if self.witness_utxo else None, 'sighash': self.sighash, @@ -1614,6 +1609,7 @@ def __init__(self, *args, **kwargs): self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self._unknown = {} # type: Dict[bytes, bytes] + self.script_descriptor = None # type: Optional[Descriptor] self.script_type = 'unknown' self.num_sig = 0 # num req sigs for multisig self.pubkeys = [] # type: List[bytes] # note: order matters @@ -1623,6 +1619,7 @@ def __init__(self, *args, **kwargs): def to_json(self): d = super().to_json() d.update({ + 'desc': self.script_descriptor.to_string() if self.script_descriptor else None, 'redeem_script': self.redeem_script.hex() if self.redeem_script else None, 'witness_script': self.witness_script.hex() if self.witness_script else None, 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) diff --git a/electrum/wallet.py b/electrum/wallet.py index a82d43375..21bdda164 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -84,6 +84,8 @@ from .paymentrequest import PaymentRequest from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed from .util import EventListener, event_listener +from . import descriptor +from .descriptor import Descriptor if TYPE_CHECKING: from .network import Network @@ -100,16 +102,17 @@ ] -async def _append_utxos_to_inputs(*, inputs: List[PartialTxInput], network: 'Network', - pubkey: str, txin_type: str, imax: int) -> None: - if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): - address = bitcoin.pubkey_to_address(txin_type, pubkey) - scripthash = bitcoin.address_to_scripthash(address) - elif txin_type == 'p2pk': - script = bitcoin.public_key_to_p2pk_script(pubkey) - scripthash = bitcoin.script_to_scripthash(script) - else: - raise Exception(f'unexpected txin_type to sweep: {txin_type}') +async def _append_utxos_to_inputs( + *, + inputs: List[PartialTxInput], + network: 'Network', + script_descriptor: 'descriptor.Descriptor', + pubkey: str, + txin_type: str, + imax: int, +) -> None: + script = script_descriptor.expand().output_script.hex() + scripthash = bitcoin.script_to_scripthash(script) async def append_single_utxo(item): prev_tx_raw = await network.get_transaction(item['tx_hash']) @@ -122,6 +125,9 @@ async def append_single_utxo(item): txin = PartialTxInput(prevout=prevout) txin.utxo = prev_tx txin.block_height = int(item['height']) + txin.script_descriptor = script_descriptor + # TODO rm as much of below (.num_sig / .pubkeys) as possible + # TODO need unit tests for other scripts (only have p2pk atm) txin.script_type = txin_type txin.pubkeys = [bfh(pubkey)] txin.num_sig = 1 @@ -141,9 +147,11 @@ async def sweep_preparations(privkeys, network: 'Network', imax=100): async def find_utxos_for_privkey(txin_type, privkey, compressed): pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) await _append_utxos_to_inputs( inputs=inputs, network=network, + script_descriptor=desc, pubkey=pubkey, txin_type=txin_type, imax=imax) @@ -684,13 +692,19 @@ def get_address_path_str(self, address: str) -> Optional[str]: """ pass - @abstractmethod def get_redeem_script(self, address: str) -> Optional[str]: - pass + desc = self._get_script_descriptor_for_address(address) + if desc is None: return None + redeem_script = desc.expand().redeem_script + if redeem_script: + return redeem_script.hex() - @abstractmethod def get_witness_script(self, address: str) -> Optional[str]: - pass + desc = self._get_script_descriptor_for_address(address) + if desc is None: return None + witness_script = desc.expand().witness_script + if witness_script: + return witness_script.hex() @abstractmethod def get_txin_type(self, address: str) -> str: @@ -2193,7 +2207,8 @@ def add_input_info( if self.lnworker: self.lnworker.swap_manager.add_txin_info(txin) return - # set script_type first, as later checks might rely on it: + txin.script_descriptor = self._get_script_descriptor_for_address(address) + # set script_type first, as later checks might rely on it: # TODO rm most of below in favour of osd txin.script_type = self.get_txin_type(address) txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1 if txin.redeem_script is None: @@ -2211,6 +2226,34 @@ def add_input_info( self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix) txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height + def _get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]: + if not self.is_mine(address): + return None + script_type = self.get_txin_type(address) + if script_type in ('address', 'unknown'): + return None + if script_type in ('p2pk', 'p2pkh', 'p2wpkh-p2sh', 'p2wpkh'): + pubkey = self.get_public_keys(address)[0] + return descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type) + elif script_type == 'p2sh': + pubkeys = self.get_public_keys(address) + pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] + multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) + return descriptor.SHDescriptor(subdescriptor=multi) + elif script_type == 'p2wsh': + pubkeys = self.get_public_keys(address) + pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] + multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) + return descriptor.WSHDescriptor(subdescriptor=multi) + elif script_type == 'p2wsh-p2sh': + pubkeys = self.get_public_keys(address) + pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] + multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) + wsh = descriptor.WSHDescriptor(subdescriptor=multi) + return descriptor.SHDescriptor(subdescriptor=wsh) + else: + raise NotImplementedError(f"unexpected {script_type=}") + def can_sign(self, tx: Transaction) -> bool: if not isinstance(tx, PartialTransaction): return False @@ -2262,6 +2305,7 @@ def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = Fal is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address) if not is_mine: return + txout.script_descriptor = self._get_script_descriptor_for_address(address) txout.script_type = self.get_txin_type(address) txout.is_mine = True txout.is_change = self.is_change(address) @@ -2938,20 +2982,6 @@ def get_public_key(self, address: str) -> Optional[str]: def get_public_keys(self, address: str) -> Sequence[str]: return [self.get_public_key(address)] - def get_redeem_script(self, address: str) -> Optional[str]: - txin_type = self.get_txin_type(address) - if txin_type in ('p2pkh', 'p2wpkh', 'p2pk'): - return None - if txin_type == 'p2wpkh-p2sh': - pubkey = self.get_public_key(address) - return bitcoin.p2wpkh_nested_script(pubkey) - if txin_type == 'address': - return None - raise UnknownTxinType(f'unexpected txin_type {txin_type}') - - def get_witness_script(self, address: str) -> Optional[str]: - return None - class Imported_Wallet(Simple_Wallet): # wallet made of imported addresses @@ -3463,28 +3493,6 @@ def pubkeys_to_address(self, pubkeys): def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: return transaction.multisig_script(sorted(pubkeys), self.m) - def get_redeem_script(self, address): - txin_type = self.get_txin_type(address) - pubkeys = self.get_public_keys(address) - scriptcode = self.pubkeys_to_scriptcode(pubkeys) - if txin_type == 'p2sh': - return scriptcode - elif txin_type == 'p2wsh-p2sh': - return bitcoin.p2wsh_nested_script(scriptcode) - elif txin_type == 'p2wsh': - return None - raise UnknownTxinType(f'unexpected txin_type {txin_type}') - - def get_witness_script(self, address): - txin_type = self.get_txin_type(address) - pubkeys = self.get_public_keys(address) - scriptcode = self.pubkeys_to_scriptcode(pubkeys) - if txin_type == 'p2sh': - return None - elif txin_type in ('p2wsh-p2sh', 'p2wsh'): - return scriptcode - raise UnknownTxinType(f'unexpected txin_type {txin_type}') - def derive_pubkeys(self, c, i): return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()] From 765d2312096d8355c96b6e9d7bbc96cd51afdea2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 2 Mar 2023 09:49:29 +0100 Subject: [PATCH 0231/1143] utxo dialog: fix pipe symbol --- electrum/gui/qt/utxo_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 8a37d2cd5..30dc80b1c 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -73,12 +73,12 @@ def __init__(self, window: 'ElectrumWindow', utxo): if num_parents < 200: ASCII_EDGE = '└─' ASCII_BRANCH = '├─' - ASCII_PIPE = '| ' + ASCII_PIPE = '│ ' ASCII_SPACE = ' ' else: ASCII_EDGE = '└' ASCII_BRANCH = '├' - ASCII_PIPE = '|' + ASCII_PIPE = '│' ASCII_SPACE = ' ' def print_ascii_tree(_txid, prefix, is_last): From f65158a23f4333e37b210863d9743a31e2a85d56 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 10:49:01 +0100 Subject: [PATCH 0232/1143] qml: move menu back to topbar, move addresses and channels to walletdetails and add walletdetails to topbar menu --- electrum/gui/qml/components/WalletDetails.qml | 15 +++++++ .../gui/qml/components/WalletMainView.qml | 45 ++++++------------- .../components/controls/BalanceSummary.qml | 8 ---- electrum/gui/qml/components/main.qml | 22 ++++++--- 4 files changed, 44 insertions(+), 46 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 7ab98b254..62dc7aeca 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -498,6 +498,21 @@ Pane { visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning icon.source: '../../icons/lightning.png' } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Channels') + onClicked: app.stack.push(Qt.resolvedUrl('Channels.qml')) + visible: Daemon.currentWallet && Daemon.currentWallet.isLightning + icon.source: '../../icons/lightning.png' + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Addresses') + onClicked: app.stack.push(Qt.resolvedUrl('Addresses.qml')) + icon.source: '../../icons/tab_addresses.png' + } } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 8660b5e9c..f9eceae2d 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -55,30 +55,24 @@ Item { } id: menu + MenuItem { icon.color: 'transparent' action: Action { - text: qsTr('Addresses'); - onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml')); + text: qsTr('Wallet details') enabled: Daemon.currentWallet - icon.source: '../../icons/tab_addresses.png' - } - } - MenuItem { - icon.color: 'transparent' - action: Action { - text: qsTr('Channels'); - enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning - onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml')) - icon.source: '../../icons/lightning.png' + onTriggered: menu.openPage(true, Qt.resolvedUrl('WalletDetails.qml')) + icon.source: '../../icons/wallet.png' } } + MenuSeparator { } + MenuItem { icon.color: 'transparent' action: Action { text: qsTr('Preferences'); - onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml')) + onTriggered: menu.openPage(false, Qt.resolvedUrl('Preferences.qml')) icon.source: '../../icons/preferences.png' } } @@ -87,13 +81,17 @@ Item { icon.color: 'transparent' action: Action { text: qsTr('About'); - onTriggered: menu.openPage(Qt.resolvedUrl('About.qml')) + onTriggered: menu.openPage(false, Qt.resolvedUrl('About.qml')) icon.source: '../../icons/electrum.png' } } - function openPage(url) { - stack.push(url) + function openPage(onroot, url) { + if (onroot) { + stack.pushOnRoot(url) + } else { + stack.push(url) + } currentIndex = -1 } } @@ -157,21 +155,6 @@ Item { id: buttonContainer Layout.fillWidth: true - FlatButton { - Layout.fillWidth: false - Layout.preferredWidth: implicitHeight - Layout.preferredHeight: receiveButton.implicitHeight - - icon.source: '../../icons/hamburger.png' - icon.height: constants.iconSizeSmall - icon.width: constants.iconSizeSmall - - onClicked: { - mainView.menu.open() - mainView.menu.y = mainView.height + app.header.height - mainView.menu.height - buttonContainer.height - } - } - FlatButton { id: receiveButton visible: Daemon.currentWallet diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index f92d458e4..ecced5d63 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -123,14 +123,6 @@ Item { font.pixelSize: constants.fontSizeLarge } - MouseArea { - anchors.fill: balancePane - onClicked: { - app.stack.pushOnRoot(Qt.resolvedUrl('../WalletDetails.qml')) - } - } - - // instead of all these explicit connections, we should expose // formatted balances directly as a property Connections { diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 484ec7209..8110eb3b1 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -62,25 +62,33 @@ ApplicationWindow Layout.rightMargin: constants.paddingMedium Layout.alignment: Qt.AlignVCenter - Item { - Layout.preferredWidth: constants.paddingXLarge - Layout.preferredHeight: 1 + ToolButton { + id: menuButton + enabled: stack.currentItem && stack.currentItem.menu + ? stack.currentItem.menu.count > 0 + : false + + text: enabled ? '≡' : '' + font.pixelSize: constants.fontSizeXLarge + onClicked: { + stack.currentItem.menu.open() + stack.currentItem.menu.y = toolbarTopLayout.height + } } Image { - visible: Daemon.currentWallet - source: '../../icons/wallet.png' Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall + visible: Daemon.currentWallet + source: '../../icons/wallet.png' } Label { + Layout.fillWidth: true Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height) text: stack.currentItem.title elide: Label.ElideRight - // horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter - Layout.fillWidth: true font.pixelSize: constants.fontSizeMedium font.bold: true MouseArea { From 27711093d249c0e9206a9097fb9c0cdc34a2a1cd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 11:20:58 +0100 Subject: [PATCH 0233/1143] qml: don't update (and by extension initialize) requestModel, as it isn't used --- electrum/gui/qml/qewallet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 8132f270f..8e7cadb0e 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -651,7 +651,8 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, is_ligh return assert key is not None - self.requestModel.add_invoice(self.wallet.get_request(key)) + # requestModel not used, so don't update + # self.requestModel.add_invoice(self.wallet.get_request(key)) self.requestCreateSuccess.emit(key) @pyqtSlot() @@ -677,7 +678,8 @@ def createDefaultRequest(self, ignore_gap: bool = False, reuse_address: bool = F return assert key is not None - self.requestModel.add_invoice(self.wallet.get_request(key)) + # requestModel not used, so don't update + # self.requestModel.add_invoice(self.wallet.get_request(key)) self.requestCreateSuccess.emit(key) @pyqtSlot(str) From 6a523b3de0ba7a711177c83c4007340d18e37669 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 11:56:52 +0100 Subject: [PATCH 0234/1143] Revert "qml: don't update (and by extension initialize) requestModel, as it isn't used" This reverts commit 27711093d249c0e9206a9097fb9c0cdc34a2a1cd. --- electrum/gui/qml/qewallet.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 8e7cadb0e..8132f270f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -651,8 +651,7 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, is_ligh return assert key is not None - # requestModel not used, so don't update - # self.requestModel.add_invoice(self.wallet.get_request(key)) + self.requestModel.add_invoice(self.wallet.get_request(key)) self.requestCreateSuccess.emit(key) @pyqtSlot() @@ -678,8 +677,7 @@ def createDefaultRequest(self, ignore_gap: bool = False, reuse_address: bool = F return assert key is not None - # requestModel not used, so don't update - # self.requestModel.add_invoice(self.wallet.get_request(key)) + self.requestModel.add_invoice(self.wallet.get_request(key)) self.requestCreateSuccess.emit(key) @pyqtSlot(str) From 67f3c1eb059c96e0609fb28afe2305a30cb45e07 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 11:58:00 +0100 Subject: [PATCH 0235/1143] qml: don't init QERequestListModel/requestModel --- electrum/gui/qml/qeinvoicelistmodel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 1bd39dd4d..bdd9b78b2 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -223,7 +223,8 @@ def invoice_to_model(self, invoice: Invoice): return item def get_invoice_list(self): - return self.wallet.get_unpaid_requests() + # disable for now, as QERequestListModel isn't used in UI + return [] #self.wallet.get_unpaid_requests() def get_invoice_for_key(self, key: str): return self.wallet.get_request(key) From 6383f83933cc473e98824b11c242f4217c7fc4ac Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 12:54:59 +0100 Subject: [PATCH 0236/1143] qml: separate app menu and wallet menu --- .../gui/qml/components/NetworkOverview.qml | 2 + electrum/gui/qml/components/WalletDetails.qml | 15 ---- .../gui/qml/components/WalletMainView.qml | 41 +++++----- electrum/gui/qml/components/main.qml | 81 +++++++++++++------ 4 files changed, 83 insertions(+), 56 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 361ac659e..91c0f79c7 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -11,6 +11,8 @@ Pane { padding: 0 + property string title: qsTr("Network") + ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 62dc7aeca..7ab98b254 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -498,21 +498,6 @@ Pane { visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning icon.source: '../../icons/lightning.png' } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Channels') - onClicked: app.stack.push(Qt.resolvedUrl('Channels.qml')) - visible: Daemon.currentWallet && Daemon.currentWallet.isLightning - icon.source: '../../icons/lightning.png' - } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Addresses') - onClicked: app.stack.push(Qt.resolvedUrl('Addresses.qml')) - icon.source: '../../icons/tab_addresses.png' - } } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index f9eceae2d..15170538b 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -11,7 +11,7 @@ import "controls" Item { id: mainView - property string title: Daemon.currentWallet ? Daemon.currentWallet.name : '' + property string title: Daemon.currentWallet ? Daemon.currentWallet.name : qsTr('no wallet loaded') property var _sendDialog property string _intentUri @@ -61,37 +61,42 @@ Item { action: Action { text: qsTr('Wallet details') enabled: Daemon.currentWallet - onTriggered: menu.openPage(true, Qt.resolvedUrl('WalletDetails.qml')) + onTriggered: menu.openPage(Qt.resolvedUrl('WalletDetails.qml')) icon.source: '../../icons/wallet.png' } } - - MenuSeparator { } - MenuItem { icon.color: 'transparent' action: Action { - text: qsTr('Preferences'); - onTriggered: menu.openPage(false, Qt.resolvedUrl('Preferences.qml')) - icon.source: '../../icons/preferences.png' + text: qsTr('Addresses'); + onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml')); + enabled: Daemon.currentWallet + icon.source: '../../icons/tab_addresses.png' } } - MenuItem { - icon.color: 'transparent' + icon.color: 'transparent' action: Action { - text: qsTr('About'); - onTriggered: menu.openPage(false, Qt.resolvedUrl('About.qml')) - icon.source: '../../icons/electrum.png' + text: qsTr('Channels'); + enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning + onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml')) + icon.source: '../../icons/lightning.png' } } - function openPage(onroot, url) { - if (onroot) { - stack.pushOnRoot(url) - } else { - stack.push(url) + MenuSeparator { } + + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Other wallets'); + onTriggered: menu.openPage(Qt.resolvedUrl('Wallets.qml')) + icon.source: '../../icons/file.png' } + } + + function openPage(url) { + stack.pushOnRoot(url) currentIndex = -1 } } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 8110eb3b1..d40a98ac4 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -36,6 +36,55 @@ ApplicationWindow property bool _wantClose: false property var _exceptionDialog + property QtObject appMenu: Menu { + parent: Overlay.overlay + dim: true + modal: true + Overlay.modal: Rectangle { + color: "#44000000" + } + + id: menu + + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Network') + onTriggered: menu.openPage(Qt.resolvedUrl('NetworkOverview.qml')) + icon.source: '../../icons/network.png' + } + } + + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('Preferences'); + onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml')) + icon.source: '../../icons/preferences.png' + } + } + + MenuItem { + icon.color: 'transparent' + action: Action { + text: qsTr('About'); + onTriggered: menu.openPage(Qt.resolvedUrl('About.qml')) + icon.source: '../../icons/electrum.png' + } + } + + function openPage(url) { + stack.pushOnRoot(url) + currentIndex = -1 + } + } + + function openAppMenu() { + appMenu.open() + appMenu.x = app.width - appMenu.width + appMenu.y = toolbar.height + } + header: ToolBar { id: toolbar @@ -62,24 +111,15 @@ ApplicationWindow Layout.rightMargin: constants.paddingMedium Layout.alignment: Qt.AlignVCenter - ToolButton { - id: menuButton - enabled: stack.currentItem && stack.currentItem.menu - ? stack.currentItem.menu.count > 0 - : false - - text: enabled ? '≡' : '' - font.pixelSize: constants.fontSizeXLarge - onClicked: { - stack.currentItem.menu.open() - stack.currentItem.menu.y = toolbarTopLayout.height - } + Item { + Layout.preferredWidth: constants.paddingXLarge + Layout.preferredHeight: 1 } Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall - visible: Daemon.currentWallet + visible: Daemon.currentWallet && stack.currentItem.title == Daemon.currentWallet.name source: '../../icons/wallet.png' } @@ -94,9 +134,10 @@ ApplicationWindow MouseArea { height: toolbarTopLayout.height anchors.fill: parent + // enabled: Daemon.currentWallet onClicked: { - if (stack.currentItem.objectName != 'Wallets') - stack.pushOnRoot(Qt.resolvedUrl('Wallets.qml')) + stack.getRoot().menu.open() + stack.getRoot().menu.y = toolbar.height } } } @@ -136,20 +177,14 @@ ApplicationWindow LightningNetworkStatusIndicator { MouseArea { anchors.fill: parent - onClicked: { - if (stack.currentItem.objectName != 'NetworkOverview') - stack.push(Qt.resolvedUrl('NetworkOverview.qml')) - } + onClicked: openAppMenu() } } OnchainNetworkStatusIndicator { MouseArea { anchors.fill: parent - onClicked: { - if (stack.currentItem.objectName != 'NetworkOverview') - stack.push(Qt.resolvedUrl('NetworkOverview.qml')) - } + onClicked: openAppMenu() } } } From 0da1be33b72bf68496e5119567a096b183a15eb1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 15:44:30 +0100 Subject: [PATCH 0237/1143] qml: topbar label show wallet icon only when wallet name is displayed --- electrum/gui/qml/components/Wallets.qml | 2 ++ electrum/gui/qml/components/main.qml | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 9d18b3f61..5b6d3b76a 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -13,6 +13,8 @@ Pane { padding: 0 + property string title: qsTr('Wallets') + function createWallet() { var dialog = app.newWalletWizard.createObject(app) dialog.open() diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index d40a98ac4..f17cdd375 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -119,7 +119,7 @@ ApplicationWindow Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall - visible: Daemon.currentWallet && stack.currentItem.title == Daemon.currentWallet.name + visible: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) source: '../../icons/wallet.png' } @@ -127,14 +127,15 @@ ApplicationWindow Layout.fillWidth: true Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height) text: stack.currentItem.title + ? stack.currentItem.title + : Daemon.currentWallet.name elide: Label.ElideRight verticalAlignment: Qt.AlignVCenter font.pixelSize: constants.fontSizeMedium font.bold: true MouseArea { - height: toolbarTopLayout.height + // height: toolbarTopLayout.height anchors.fill: parent - // enabled: Daemon.currentWallet onClicked: { stack.getRoot().menu.open() stack.getRoot().menu.y = toolbar.height From 8278689cc33188308ae2500f0d66ddbd5ed3df40 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 2 Mar 2023 16:19:30 +0100 Subject: [PATCH 0238/1143] qml: fix initial sync state, remove isUptodate property --- electrum/gui/qml/qewallet.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 8132f270f..c41627239 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -53,7 +53,6 @@ def getInstanceFor(cls, wallet): # shared signal for many static wallet properties dataChanged = pyqtSignal() - isUptodateChanged = pyqtSignal() requestStatusChanged = pyqtSignal([str,int], arguments=['key','status']) requestCreateSuccess = pyqtSignal([str], arguments=['key']) requestCreateError = pyqtSignal([str,str], arguments=['code','error']) @@ -81,7 +80,6 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self._logger = get_logger(f'{__name__}.[{wallet}]') - self._isUpToDate = False self._synchronizing = False self._synchronizing_progress = '' @@ -124,11 +122,7 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) - self.synchronizing = True # start in sync state - - @pyqtProperty(bool, notify=isUptodateChanged) - def isUptodate(self): - return self._isUpToDate + self.synchronizing = not wallet.is_up_to_date() synchronizingChanged = pyqtSignal() @pyqtProperty(bool, notify=synchronizingChanged) @@ -138,7 +132,7 @@ def synchronizing(self): @synchronizing.setter def synchronizing(self, synchronizing): if self._synchronizing != synchronizing: - self._logger.info(f'SYNC {self._synchronizing} -> {synchronizing}') + self._logger.debug(f'SYNC {self._synchronizing} -> {synchronizing}') self._synchronizing = synchronizing self.synchronizingChanged.emit() if synchronizing: @@ -160,17 +154,6 @@ def synchronizingProgress(self, progress): self._logger.info(progress) self.synchronizingProgressChanged.emit() - @qt_event_listener - def on_event_status(self): - self._logger.debug('status') - uptodate = self.wallet.is_up_to_date() - if self._isUpToDate != uptodate: - self._isUpToDate = uptodate - self.isUptodateChanged.emit() - - if uptodate: - self.historyModel.init_model() - @qt_event_listener def on_event_request_status(self, wallet, key, status): if wallet == self.wallet: From 4ee6def7eed3013f05e9209aa68cceb43513ec36 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 2 Mar 2023 16:58:42 +0000 Subject: [PATCH 0239/1143] qt swap dialog: (trivial) make "toggle swap direction" btn wider with dark theme enabled, PushButtons are only as wide by default as the text they contain --- electrum/gui/qt/swap_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 1e298a4a1..6db147234 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -46,7 +46,7 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No btn_width = 10 * char_width_in_lineedit() self.max_button.setFixedWidth(btn_width) self.max_button.setCheckable(True) - self.toggle_button = QPushButton(u'\U000021c4') + self.toggle_button = QPushButton(' \U000021c4 ') # whitespace to force larger min width self.toggle_button.setEnabled(is_reverse is None) # send_follows is used to know whether the send amount field / receive # amount field should be adjusted after the fee slider was moved From df9a58480bb4ee02d4309fc7dd8712372b3ec0c9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 12:57:04 +0000 Subject: [PATCH 0240/1143] descriptors: implement and use ".satisfy*" methods --- electrum/descriptor.py | 168 ++++++++++++++++++++++++++++++++++++---- electrum/transaction.py | 36 +++++---- 2 files changed, 177 insertions(+), 27 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 59abd49be..612ad6947 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -13,6 +13,9 @@ # TODO impl ADDR descriptors # TODO impl RAW descriptors # TODO disable descs we cannot solve: TRDescriptor +# TODO add checks to validate nestings +# https://github.com/bitcoin/bitcoin/blob/94070029fb6b783833973f9fe08a3a871994492f/doc/descriptors.md#reference +# e.g. sh is top-level only, wsh is top-level or directly inside sh # # TODO tests # - port https://github.com/bitcoin-core/HWI/blob/master/test/test_descriptor.py @@ -24,8 +27,9 @@ from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo from . import bitcoin -from .bitcoin import construct_script, opcodes +from .bitcoin import construct_script, opcodes, construct_witness from .crypto import hash_160, sha256 +from .util import bfh from binascii import unhexlify from enum import Enum @@ -35,6 +39,7 @@ Optional, Tuple, Sequence, + Mapping, ) @@ -71,6 +76,18 @@ def address(self, *, net=None) -> Optional[str]: return bitcoin.script_to_address(spk.hex(), net=net) +class ScriptSolutionInner(NamedTuple): + witness_items: Optional[Sequence] = None + + +class ScriptSolutionTop(NamedTuple): + witness: Optional[bytes] = None + script_sig: Optional[bytes] = None + + +class MissingSolutionPiece(Exception): pass + + def PolyMod(c: int, val: int) -> int: """ :meta private: @@ -316,6 +333,38 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": """ raise NotImplementedError("The Descriptor base class does not implement this method") + def _satisfy_inner( + self, + *, + sigdata: Mapping[bytes, bytes] = None, # pubkey -> sig + allow_dummy: bool = False, + ) -> ScriptSolutionInner: + raise NotImplementedError("The Descriptor base class does not implement this method") + + def satisfy( + self, + *, + sigdata: Mapping[bytes, bytes] = None, # pubkey -> sig + allow_dummy: bool = False, + ) -> ScriptSolutionTop: + """Construct a witness and/or scriptSig to be used in a txin, to satisfy the bitcoin SCRIPT. + + Raises MissingSolutionPiece if satisfaction is not yet possible due to e.g. missing a signature, + unless `allow_dummy` is set to True, in which case dummy data is used where needed (e.g. for size estimation). + """ + assert not self.is_range() + sol = self._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) + witness = None + script_sig = None + if self.is_segwit(): + witness = bfh(construct_witness(sol.witness_items)) + else: + script_sig = bfh(construct_script(sol.witness_items)) + return ScriptSolutionTop( + witness=witness, + script_sig=script_sig, + ) + def is_range(self) -> bool: for pubkey in self.pubkeys: if pubkey.is_range(): @@ -347,6 +396,20 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": script = construct_script([pubkey, opcodes.OP_CHECKSIG]) return ExpandedScripts(output_script=bytes.fromhex(script)) + def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: + if sigdata is None: sigdata = {} + assert not self.is_range() + assert not self.subdescriptors + pubkey = self.pubkeys[0].get_pubkey_bytes() + sig = sigdata.get(pubkey) + if sig is None and allow_dummy: + sig = 72 * b"\x00" + if sig is None: + raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") + return ScriptSolutionInner( + witness_items=(sig,), + ) + class PKHDescriptor(Descriptor): """ @@ -367,6 +430,20 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": script = bitcoin.pubkeyhash_to_p2pkh_script(pkh) return ExpandedScripts(output_script=bytes.fromhex(script)) + def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: + if sigdata is None: sigdata = {} + assert not self.is_range() + assert not self.subdescriptors + pubkey = self.pubkeys[0].get_pubkey_bytes() + sig = sigdata.get(pubkey) + if sig is None and allow_dummy: + sig = 72 * b"\x00" + if sig is None: + raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") + return ScriptSolutionInner( + witness_items=(sig, pubkey), + ) + class WPKHDescriptor(Descriptor): """ @@ -390,6 +467,20 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": scriptcode_for_sighash=bytes.fromhex(scriptcode), ) + def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: + if sigdata is None: sigdata = {} + assert not self.is_range() + assert not self.subdescriptors + pubkey = self.pubkeys[0].get_pubkey_bytes() + sig = sigdata.get(pubkey) + if sig is None and allow_dummy: + sig = 72 * b"\x00" + if sig is None: + raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") + return ScriptSolutionInner( + witness_items=(sig, pubkey), + ) + def is_segwit(self) -> bool: return True @@ -410,6 +501,8 @@ def __init__( :param is_sorted: Whether this is a ``sortedmulti()`` descriptor """ super().__init__(pubkeys, [], "sortedmulti" if is_sorted else "multi") + if not (1 <= thresh <= len(pubkeys) <= 15): + raise ValueError(f'{thresh=}, {len(pubkeys)=}') self.thresh = thresh self.is_sorted = is_sorted if self.is_sorted: @@ -419,21 +512,35 @@ def to_string_no_checksum(self) -> str: return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": - if self.thresh > 16: - m = b"\x01" + self.thresh.to_bytes(1, "big") - else: - m = (self.thresh + 0x50).to_bytes(1, "big") if self.thresh > 0 else b"\x00" - n = (len(self.pubkeys) + 0x50).to_bytes(1, "big") if len(self.pubkeys) > 0 else b"\x00" - script: bytes = m der_pks = [p.get_pubkey_bytes(pos=pos) for p in self.pubkeys] if self.is_sorted: der_pks.sort() - for pk in der_pks: - script += len(pk).to_bytes(1, "big") + pk - script += n + b"\xae" - + script = bfh(construct_script([self.thresh, *der_pks, len(der_pks), opcodes.OP_CHECKMULTISIG])) return ExpandedScripts(output_script=script) + def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: + if sigdata is None: sigdata = {} + assert not self.is_range() + assert not self.subdescriptors + der_pks = [p.get_pubkey_bytes() for p in self.pubkeys] + if self.is_sorted: + der_pks.sort() + signatures = [] + for pubkey in der_pks: + if sig := sigdata.get(pubkey): + signatures.append(sig) + if len(signatures) >= self.thresh: + break + if allow_dummy: + dummy_sig = 72 * b"\x00" + signatures += (self.thresh - len(signatures)) * [dummy_sig] + if len(signatures) < self.thresh: + raise MissingSolutionPiece(f"not enough sigs") + assert len(signatures) == self.thresh, f"thresh={self.thresh}, but got {len(signatures)} sigs" + return ScriptSolutionInner( + witness_items=(0, *signatures), + ) + class SHDescriptor(Descriptor): """ @@ -453,7 +560,7 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": sub_scripts = self.subdescriptors[0].expand(pos=pos) redeem_script = sub_scripts.output_script witness_script = sub_scripts.witness_script - script = b"\xa9\x14" + hash_160(redeem_script) + b"\x87" + script = bfh(construct_script([opcodes.OP_HASH160, hash_160(redeem_script), opcodes.OP_EQUAL])) return ExpandedScripts( output_script=script, redeem_script=redeem_script, @@ -461,6 +568,26 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": scriptcode_for_sighash=sub_scripts.scriptcode_for_sighash, ) + def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: + raise Exception("does not make sense for sh()") + + def satisfy(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionTop: + assert not self.is_range() + assert len(self.subdescriptors) == 1 + subdesc = self.subdescriptors[0] + redeem_script = self.expand().redeem_script + witness = None + if isinstance(subdesc, (WSHDescriptor, WPKHDescriptor)): # witness_v0 nested in p2sh + witness = subdesc.satisfy(sigdata=sigdata, allow_dummy=allow_dummy).witness + script_sig = bfh(construct_script([redeem_script])) + else: # legacy p2sh + subsol = subdesc._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) + script_sig = bfh(construct_script([*subsol.witness_items, redeem_script])) + return ScriptSolutionTop( + witness=witness, + script_sig=script_sig, + ) + class WSHDescriptor(Descriptor): """ @@ -479,12 +606,25 @@ def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 sub_scripts = self.subdescriptors[0].expand(pos=pos) witness_script = sub_scripts.output_script - script = b"\x00\x20" + sha256(witness_script) + output_script = bfh(construct_script([0, sha256(witness_script)])) return ExpandedScripts( - output_script=script, + output_script=output_script, witness_script=witness_script, ) + def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: + raise Exception("does not make sense for wsh()") + + def satisfy(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionTop: + assert not self.is_range() + assert len(self.subdescriptors) == 1 + subsol = self.subdescriptors[0]._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) + witness_script = self.expand().witness_script + witness = construct_witness([*subsol.witness_items, witness_script]) + return ScriptSolutionTop( + witness=bytes.fromhex(witness), + ) + def is_segwit(self) -> bool: return True diff --git a/electrum/transaction.py b/electrum/transaction.py index 6cb02cefa..d9047336d 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -52,7 +52,7 @@ from .crypto import sha256d from .logging import get_logger from .util import ShortID -from .descriptor import Descriptor +from .descriptor import Descriptor, MissingSolutionPiece if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -774,6 +774,14 @@ def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: if estimate_size and txin.witness_sizehint is not None: return '00' * txin.witness_sizehint + + if desc := txin.script_descriptor: + sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs) + if sol.witness is not None: + return sol.witness.hex() + return construct_witness([]) + + assert estimate_size # TODO xxxxx if _type in ('address', 'unknown') and estimate_size: _type = cls.guess_txintype_from_address(txin.address) pubkeys, sig_list = cls.get_siglist(txin, estimate_size=estimate_size) @@ -827,7 +835,12 @@ def input_script(self, txin: TxInput, *, estimate_size=False) -> str: if redeem_script := desc.expand().redeem_script: return construct_script([redeem_script]) return "" + sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs) + if sol.script_sig is not None: + return sol.script_sig.hex() + return "" + assert estimate_size # TODO xxxxx _type = txin.script_type pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) if _type in ('address', 'unknown') and estimate_size: @@ -841,7 +854,6 @@ def input_script(self, txin: TxInput, *, estimate_size=False) -> str: elif _type == 'p2pkh': return construct_script([sig_list[0], pubkeys[0]]) elif _type == 'p2wpkh-p2sh': - assert estimate_size # otherwise script_descriptor should handle it redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0]) return construct_script([redeem_script]) raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}') @@ -1486,17 +1498,13 @@ def is_complete(self) -> bool: return True if self.script_sig is not None and not self.is_segwit(): return True - signatures = list(self.part_sigs.values()) - s = len(signatures) - # note: The 'script_type' field is currently only set by the wallet, - # for its own addresses. This means we can only finalize inputs - # that are related to the wallet. - # The 'fix' would be adding extra logic that matches on templates, - # and figures out the script_type from available fields. - if self.script_type in ('p2pk', 'p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): - return s >= 1 - if self.script_type in ('p2sh', 'p2wsh', 'p2wsh-p2sh'): - return s >= self.num_sig + if desc := self.script_descriptor: + try: + desc.satisfy(allow_dummy=False, sigdata=self.part_sigs) + except MissingSolutionPiece: + pass + else: + return True return False def finalize(self) -> None: @@ -1589,6 +1597,8 @@ def is_segwit(self, *, guess_for_address=False) -> bool: return False if self.witness_script: return True + if desc := self.script_descriptor: + return desc.is_segwit() _type = self.script_type if _type == 'address' and guess_for_address: _type = Transaction.guess_txintype_from_address(self.address) From d062505cfd6263846dbb7d19cd1919536f5a188d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 13:15:24 +0000 Subject: [PATCH 0241/1143] transaction.py: delegate size estimation to descriptors --- electrum/descriptor.py | 48 ++++++++++++++++-- electrum/transaction.py | 105 ++++++---------------------------------- 2 files changed, 60 insertions(+), 93 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 612ad6947..b87f52be7 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -28,7 +28,10 @@ from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo from . import bitcoin from .bitcoin import construct_script, opcodes, construct_witness +from . import constants from .crypto import hash_160, sha256 +from . import ecc +from . import segwit_addr from .util import bfh from binascii import unhexlify @@ -45,6 +48,14 @@ MAX_TAPROOT_NODES = 128 +# we guess that signatures will be 72 bytes long +# note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice +# See https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature +# We assume low S (as that is a bitcoin standardness rule). +# We do not assume low R (even though the sigs we create conform), as external sigs, +# e.g. from a hw signer cannot be expected to have a low R. +DUMMY_DER_SIG = 72 * b"\x00" + class ExpandedScripts: @@ -403,7 +414,7 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn pubkey = self.pubkeys[0].get_pubkey_bytes() sig = sigdata.get(pubkey) if sig is None and allow_dummy: - sig = 72 * b"\x00" + sig = DUMMY_DER_SIG if sig is None: raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") return ScriptSolutionInner( @@ -437,7 +448,7 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn pubkey = self.pubkeys[0].get_pubkey_bytes() sig = sigdata.get(pubkey) if sig is None and allow_dummy: - sig = 72 * b"\x00" + sig = DUMMY_DER_SIG if sig is None: raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") return ScriptSolutionInner( @@ -474,7 +485,7 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn pubkey = self.pubkeys[0].get_pubkey_bytes() sig = sigdata.get(pubkey) if sig is None and allow_dummy: - sig = 72 * b"\x00" + sig = DUMMY_DER_SIG if sig is None: raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") return ScriptSolutionInner( @@ -532,7 +543,7 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn if len(signatures) >= self.thresh: break if allow_dummy: - dummy_sig = 72 * b"\x00" + dummy_sig = DUMMY_DER_SIG signatures += (self.thresh - len(signatures)) * [dummy_sig] if len(signatures) < self.thresh: raise MissingSolutionPiece(f"not enough sigs") @@ -900,3 +911,32 @@ def get_singlesig_descriptor_from_legacy_leaf(*, pubkey: str, script_type: str) return SHDescriptor(subdescriptor=wpkh) else: raise NotImplementedError(f"unexpected {script_type=}") + + +def create_dummy_descriptor_from_address(addr: Optional[str]) -> 'Descriptor': + # It's not possible to tell the script type in general just from an address. + # - "1" addresses are of course p2pkh + # - "3" addresses are p2sh but we don't know the redeem script... + # - "bc1" addresses (if they are 42-long) are p2wpkh + # - "bc1" addresses that are 62-long are p2wsh but we don't know the script... + # If we don't know the script, we _guess_ it is pubkeyhash. + # As this method is used e.g. for tx size estimation, + # the estimation will not be precise. + def guess_script_type(addr: Optional[str]) -> str: + if addr is None: + return 'p2wpkh' # the default guess + witver, witprog = segwit_addr.decode_segwit_address(constants.net.SEGWIT_HRP, addr) + if witprog is not None: + return 'p2wpkh' + addrtype, hash_160_ = bitcoin.b58_address_to_hash160(addr) + if addrtype == constants.net.ADDRTYPE_P2PKH: + return 'p2pkh' + elif addrtype == constants.net.ADDRTYPE_P2SH: + return 'p2wpkh-p2sh' + raise Exception(f'unrecognized address: {repr(addr)}') + + script_type = guess_script_type(addr) + # guess pubkey-len to be 33-bytes: + pubkey = ecc.GENERATOR.get_public_key_bytes(compressed=True).hex() + desc = get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type) + return desc diff --git a/electrum/transaction.py b/electrum/transaction.py index d9047336d..436a46e43 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -47,12 +47,12 @@ hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, int_to_hex, push_script, b58_address_to_hash160, - opcodes, add_number_to_script, base_decode, is_segwit_script_type, + opcodes, add_number_to_script, base_decode, base_encode, construct_witness, construct_script) from .crypto import sha256d from .logging import get_logger from .util import ShortID -from .descriptor import Descriptor, MissingSolutionPiece +from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -733,33 +733,6 @@ def deserialize(self) -> None: if vds.can_read_more(): raise SerializationError('extra junk at the end') - @classmethod - def get_siglist(self, txin: 'PartialTxInput', *, estimate_size=False): - if txin.is_coinbase_input(): - return [], [] - - if estimate_size: - try: - pubkey_size = len(txin.pubkeys[0]) - except IndexError: - pubkey_size = 33 # guess it is compressed - num_pubkeys = max(1, len(txin.pubkeys)) - pk_list = ["00" * pubkey_size] * num_pubkeys - num_sig = max(1, txin.num_sig) - # we guess that signatures will be 72 bytes long - # note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice - # See https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature - # We assume low S (as that is a bitcoin standardness rule). - # We do not assume low R (even though the sigs we create conform), as external sigs, - # e.g. from a hw signer cannot be expected to have a low R. - sig_list = ["00" * 72] * num_sig - else: - pk_list = [pubkey.hex() for pubkey in txin.pubkeys] - sig_list = [txin.part_sigs.get(pubkey, b'').hex() for pubkey in txin.pubkeys] - if txin.is_complete(): - sig_list = [sig for sig in sig_list if sig] - return pk_list, sig_list - @classmethod def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: if txin.witness is not None: @@ -775,47 +748,15 @@ def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: if estimate_size and txin.witness_sizehint is not None: return '00' * txin.witness_sizehint - if desc := txin.script_descriptor: + dummy_desc = None + if estimate_size: + dummy_desc = create_dummy_descriptor_from_address(txin.address) + if desc := (txin.script_descriptor or dummy_desc): sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs) if sol.witness is not None: return sol.witness.hex() return construct_witness([]) - - assert estimate_size # TODO xxxxx - if _type in ('address', 'unknown') and estimate_size: - _type = cls.guess_txintype_from_address(txin.address) - pubkeys, sig_list = cls.get_siglist(txin, estimate_size=estimate_size) - if _type in ['p2wpkh', 'p2wpkh-p2sh']: - return construct_witness([sig_list[0], pubkeys[0]]) - elif _type in ['p2wsh', 'p2wsh-p2sh']: - witness_script = multisig_script(pubkeys, txin.num_sig) - return construct_witness([0, *sig_list, witness_script]) - elif _type in ['p2pk', 'p2pkh', 'p2sh']: - return construct_witness([]) - raise UnknownTxinType(f'cannot construct witness for txin_type: {_type}') - - @classmethod - def guess_txintype_from_address(cls, addr: Optional[str]) -> str: - # It's not possible to tell the script type in general - # just from an address. - # - "1" addresses are of course p2pkh - # - "3" addresses are p2sh but we don't know the redeem script.. - # - "bc1" addresses (if they are 42-long) are p2wpkh - # - "bc1" addresses that are 62-long are p2wsh but we don't know the script.. - # If we don't know the script, we _guess_ it is pubkeyhash. - # As this method is used e.g. for tx size estimation, - # the estimation will not be precise. - if addr is None: - return 'p2wpkh' - witver, witprog = segwit_addr.decode_segwit_address(constants.net.SEGWIT_HRP, addr) - if witprog is not None: - return 'p2wpkh' - addrtype, hash_160_ = b58_address_to_hash160(addr) - if addrtype == constants.net.ADDRTYPE_P2PKH: - return 'p2pkh' - elif addrtype == constants.net.ADDRTYPE_P2SH: - return 'p2wpkh-p2sh' - raise Exception(f'unrecognized address: {repr(addr)}') + raise UnknownTxinType("cannot construct witness") @classmethod def input_script(self, txin: TxInput, *, estimate_size=False) -> str: @@ -830,7 +771,10 @@ def input_script(self, txin: TxInput, *, estimate_size=False) -> str: if txin.is_native_segwit(): return '' - if desc := txin.script_descriptor: + dummy_desc = None + if estimate_size: + dummy_desc = create_dummy_descriptor_from_address(txin.address) + if desc := (txin.script_descriptor or dummy_desc): if desc.is_segwit(): if redeem_script := desc.expand().redeem_script: return construct_script([redeem_script]) @@ -839,24 +783,7 @@ def input_script(self, txin: TxInput, *, estimate_size=False) -> str: if sol.script_sig is not None: return sol.script_sig.hex() return "" - - assert estimate_size # TODO xxxxx - _type = txin.script_type - pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) - if _type in ('address', 'unknown') and estimate_size: - _type = self.guess_txintype_from_address(txin.address) - if _type == 'p2pk': - return construct_script([sig_list[0]]) - elif _type == 'p2sh': - # put op_0 before script - redeem_script = multisig_script(pubkeys, txin.num_sig) - return construct_script([0, *sig_list, redeem_script]) - elif _type == 'p2pkh': - return construct_script([sig_list[0], pubkeys[0]]) - elif _type == 'p2wpkh-p2sh': - redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0]) - return construct_script([redeem_script]) - raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}') + raise UnknownTxinType("cannot construct scriptSig") @classmethod def get_preimage_script(cls, txin: 'PartialTxInput') -> str: @@ -1599,10 +1526,10 @@ def is_segwit(self, *, guess_for_address=False) -> bool: return True if desc := self.script_descriptor: return desc.is_segwit() - _type = self.script_type - if _type == 'address' and guess_for_address: - _type = Transaction.guess_txintype_from_address(self.address) - return is_segwit_script_type(_type) + if guess_for_address: + dummy_desc = create_dummy_descriptor_from_address(self.address) + return dummy_desc.is_segwit() + return False # can be false-negative def already_has_some_signatures(self) -> bool: """Returns whether progress has been made towards completing this input.""" From 9f5c5f92b34b4ec19046f3852d165eb8adde5106 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 3 Mar 2023 10:04:37 +0100 Subject: [PATCH 0242/1143] follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c --- electrum/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index a82d43375..7c041113e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2478,7 +2478,7 @@ def export_invoice(self, x: Invoice) -> Dict[str, Any]: 'invoice_id': key, } if is_lightning: - d['lightning_invoice'] = self.get_bolt11_invoice(x) + d['lightning_invoice'] = x.lightning_invoice d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_pay'] = self.lnworker.can_pay_invoice(x) From e24c4004faa0462d4aacdf1339fcb1427a359ae0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 3 Mar 2023 10:08:34 +0100 Subject: [PATCH 0243/1143] change invoice type hints following 719b468eee8b3e13680f6e7b90194d618181fe0c --- electrum/wallet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 7c041113e..773894e95 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -74,7 +74,7 @@ from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) -from .invoices import Invoice, Request +from .invoices import BaseInvoice, Invoice, Request from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED from .contacts import Contacts from .interface import NetworkException @@ -2372,7 +2372,7 @@ def check_expired_status(self, r: Invoice, status): status = PR_EXPIRED return status - def get_invoice_status(self, invoice: Invoice): + def get_invoice_status(self, invoice: BaseInvoice): """Returns status of (incoming) request or (outgoing) invoice.""" # lightning invoices can be paid onchain if invoice.is_lightning() and self.lnworker: @@ -2425,7 +2425,7 @@ def get_formatted_request(self, request_id): if x: return self.export_request(x) - def export_request(self, x: Invoice) -> Dict[str, Any]: + def export_request(self, x: Request) -> Dict[str, Any]: key = x.get_id() status = self.get_invoice_status(x) status_str = x.get_status_str(status) From 9c73a55c451bbc4f6fc5635a3d43fa42c13d2d81 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 10:08:34 +0100 Subject: [PATCH 0244/1143] qml: styling CPFP dialog, Toaster, GenericShareDialog --- electrum/gui/qml/components/CpfpBumpFeeDialog.qml | 4 ++-- electrum/gui/qml/components/GenericShareDialog.qml | 6 ++++-- electrum/gui/qml/components/controls/Toaster.qml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index 7f573f51d..675c817ff 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -16,6 +16,7 @@ ElDialog { signal txaccepted title: qsTr('Bump Fee') + iconSource: Qt.resolvedUrl('../../icons/rocket.png') width: parent.width height: parent.height @@ -182,8 +183,7 @@ ElDialog { delegate: TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - padding: 0 - leftPadding: constants.paddingSmall + RowLayout { width: parent.width Label { diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index f533e1cbe..734c597ef 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -54,9 +54,9 @@ ElDialog { } TextHighlightPane { - Layout.alignment: Qt.AlignHCenter + Layout.leftMargin: constants.paddingMedium + Layout.rightMargin: constants.paddingMedium Layout.fillWidth: true - Layout.maximumWidth: qr.width Label { width: parent.width text: dialog.text @@ -69,6 +69,8 @@ ElDialog { } Label { + Layout.leftMargin: constants.paddingMedium + Layout.rightMargin: constants.paddingMedium visible: dialog.text_help text: dialog.text_help wrapMode: Text.Wrap diff --git a/electrum/gui/qml/components/controls/Toaster.qml b/electrum/gui/qml/components/controls/Toaster.qml index cad38cb3e..c8f3a8ea7 100644 --- a/electrum/gui/qml/components/controls/Toaster.qml +++ b/electrum/gui/qml/components/controls/Toaster.qml @@ -17,7 +17,7 @@ Item { function show(item, text) { _text = text var r = item.mapToItem(parent, item.x, item.y) - x = r.x - item.width + 0.5*(item.width - toaster.width) + x = r.x + 0.5*(item.width - toaster.width) y = r.y - toaster.height - constants.paddingLarge toaster._y = y - toaster.height ani.restart() From 41f0f73bed7c31a3d0dd0197cd8a78ee5e5f42c6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 10:41:06 +0100 Subject: [PATCH 0245/1143] qml: PIN enable/disable/change more in line with other preferences items --- electrum/gui/qml/components/Preferences.qml | 79 +++++++++++---------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 2047b4b80..ac1ac2d6d 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -180,46 +180,53 @@ Pane { text: qsTr('Wallet behavior') } - Label { - text: qsTr('PIN') - } - RowLayout { - Label { - text: Config.pinCode == '' ? qsTr('Off'): qsTr('On') - color: Material.accentColor - Layout.rightMargin: constants.paddingMedium - } - Button { - text: qsTr('Enable') - visible: Config.pinCode == '' - onClicked: { - var dialog = pinSetup.createObject(preferences, {mode: 'enter'}) - dialog.accepted.connect(function() { - Config.pinCode = dialog.pincode - dialog.close() - }) - dialog.open() + Layout.fillWidth: true + Layout.leftMargin: -constants.paddingSmall + spacing: 0 + Switch { + id: usePin + checked: Config.pinCode + onCheckedChanged: { + if (activeFocus) { + console.log('PIN active ' + checked) + if (checked) { + var dialog = pinSetup.createObject(preferences, {mode: 'enter'}) + dialog.accepted.connect(function() { + Config.pinCode = dialog.pincode + dialog.close() + }) + dialog.rejected.connect(function() { + checked = false + }) + dialog.open() + } else { + focus = false + Config.pinCode = '' + // re-add binding, pincode still set if auth failed + checked = Qt.binding(function () { return Config.pinCode }) + } + } + } } - Button { - text: qsTr('Modify') - visible: Config.pinCode != '' - onClicked: { - var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode}) - dialog.accepted.connect(function() { - Config.pinCode = dialog.pincode - dialog.close() - }) - dialog.open() - } + Label { + Layout.fillWidth: true + text: qsTr('PIN') + wrapMode: Text.Wrap } - Button { - text: qsTr('Remove') - visible: Config.pinCode != '' - onClicked: { - Config.pinCode = '' - } + } + + Button { + text: qsTr('Modify') + visible: Config.pinCode != '' + onClicked: { + var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode}) + dialog.accepted.connect(function() { + Config.pinCode = dialog.pincode + dialog.close() + }) + dialog.open() } } From 72b07a3630558088e49e5affb72189a065726b85 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 10:53:03 +0100 Subject: [PATCH 0246/1143] qml: don't initialize swaphelper if a non-lightning wallet is opened Due to swaphelper's lifecycle (it is kept around longer than the swap dialog) it might get initialized with a non-lightning wallet. don't initialize in that case. proper fix is to tie the lifecycle to the swap process, or make it a child of the wallet. --- electrum/gui/qml/qeswaphelper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 511835108..34282809d 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -182,6 +182,8 @@ def isReverse(self, isReverse): def init_swap_slider_range(self): lnworker = self._wallet.wallet.lnworker + if not lnworker: + return swap_manager = lnworker.swap_manager try: asyncio.run(swap_manager.get_pairs()) From 90355a150f411672add07620e6cf03a02bc6e561 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 12:26:36 +0100 Subject: [PATCH 0247/1143] qml: fix exception handler register opened wallet --- electrum/gui/qml/qeapp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 622d262fb..d151f1a27 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -86,6 +86,10 @@ def on_wallet_loaded(self): qewallet = self._qedaemon.currentWallet if not qewallet: return + + # register wallet in Exception_Hook + Exception_Hook.maybe_setup(config=qewallet.wallet.config, wallet=qewallet.wallet) + # attach to the wallet user notification events # connect only once try: From 0f596cf2e9e6c52de9651f20db69f1a0d3e85a52 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 12:56:02 +0100 Subject: [PATCH 0248/1143] qml: report unified password change failure, impose minimum password length of 5, disallow empty passwords --- .../gui/qml/components/PasswordDialog.qml | 2 +- electrum/gui/qml/components/WalletDetails.qml | 25 +++++++++++-------- electrum/gui/qml/qedaemon.py | 10 ++++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index 276f1a4a3..743333e48 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -74,7 +74,7 @@ ElDialog { Layout.fillWidth: true text: qsTr("Ok") icon.source: '../../icons/confirmed.png' - enabled: confirmPassword ? pw_1.text == pw_2.text : true + enabled: confirmPassword ? pw_1.text.length > 4 && pw_1.text == pw_2.text : true onClicked: { password = pw_1.text passworddialog.accept() diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 7ab98b254..9bcf2b342 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -508,18 +508,23 @@ Pane { app.stack.pop() } function onRequestNewPassword() { // new unified password (all wallets) - var dialog = app.passwordDialog.createObject(app, - { - 'confirmPassword': true, - 'title': qsTr('Enter new password'), - 'infotext': qsTr('If you forget your password, you\'ll need to\ - restore from seed. Please make sure you have your seed stored safely') - } ) + var dialog = app.passwordDialog.createObject(app, { + confirmPassword: true, + title: qsTr('Enter new password'), + infotext: qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') + }) dialog.accepted.connect(function() { Daemon.setPassword(dialog.password) }) dialog.open() } + function onPasswordChangeFailed() { + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Error'), + text: qsTr('Password change failed') + }) + dialog.open() + } function onWalletDeleteError(code, message) { if (code == 'unpaid_requests') { var dialog = app.messageDialog.createObject(app, {text: message, yesno: true }) @@ -544,9 +549,9 @@ Pane { target: Daemon.currentWallet function onRequestNewPassword() { // new wallet password var dialog = app.passwordDialog.createObject(app, { - 'confirmPassword': true, - 'title': qsTr('Enter new password'), - 'infotext': qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') + confirmPassword: true, + title: qsTr('Enter new password'), + infotext: qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') }) dialog.accepted.connect(function() { Daemon.currentWallet.set_password(dialog.password) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 6c8fb0801..16e97bcea 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -127,6 +127,7 @@ class QEDaemon(AuthMixin, QObject): newWalletWizardChanged = pyqtSignal() serverConnectWizardChanged = pyqtSignal() loadingChanged = pyqtSignal() + passwordChangeFailed = pyqtSignal() walletLoaded = pyqtSignal([str,str], arguments=['name','path']) walletRequiresPassword = pyqtSignal([str,str], arguments=['name','path']) @@ -316,11 +317,10 @@ def startChangePassword(self): @pyqtSlot(str) def setPassword(self, password): assert self._use_single_password - # map empty string password to None - if password == '': - password = None - self._logger.debug('about to set password for ALL wallets') - self.daemon.update_password_for_directory(old_password=self._password, new_password=password) + assert password + if not self.daemon.update_password_for_directory(old_password=self._password, new_password=password): + self.passwordChangeFailed.emit() + return self._password = password @pyqtProperty(QENewWalletWizard, notify=newWalletWizardChanged) From 94fd0dcf1062c4a35eb15d8f14b8aae0571e9d84 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 13:40:31 +0100 Subject: [PATCH 0249/1143] qml: remove bolt11 invoice from qelnpaymentdetails/LightningPaymentDetails --- .../components/LightningPaymentDetails.qml | 36 ------------------- electrum/gui/qml/qelnpaymentdetails.py | 12 ------- 2 files changed, 48 deletions(-) diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index c1cacfa81..2fcfcc24f 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -195,42 +195,6 @@ Pane { } } - Label { - text: qsTr('Lightning invoice') - Layout.columnSpan: 2 - color: Material.accentColor - } - - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - - RowLayout { - width: parent.width - Label { - Layout.fillWidth: true - text: lnpaymentdetails.invoice - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - wrapMode: Text.Wrap - maximumLineCount: 3 - elide: Text.ElideRight - } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: enabled ? 'transparent' : constants.mutedForeground - enabled: lnpaymentdetails.invoice != '' - onClicked: { - var dialog = app.genericShareDialog.createObject(root, - { title: qsTr('Lightning Invoice'), text: lnpaymentdetails.invoice } - ) - dialog.open() - } - } - } - } - - } } diff --git a/electrum/gui/qml/qelnpaymentdetails.py b/electrum/gui/qml/qelnpaymentdetails.py index 57c2140e3..24250252c 100644 --- a/electrum/gui/qml/qelnpaymentdetails.py +++ b/electrum/gui/qml/qelnpaymentdetails.py @@ -72,10 +72,6 @@ def payment_hash(self): def preimage(self): return self._preimage - @pyqtProperty(str, notify=detailsChanged) - def invoice(self): - return self._invoice - @pyqtProperty(QEAmount, notify=detailsChanged) def amount(self): return self._amount @@ -101,12 +97,4 @@ def update(self): self._phash = tx['payment_hash'] self._preimage = tx['preimage'] - invoice = (self._wallet.wallet.get_invoice(self._key) - or self._wallet.wallet.get_request(self._key)) - self._logger.debug(str(invoice)) - if invoice: - self._invoice = invoice.lightning_invoice or '' - else: - self._invoice = '' - self.detailsChanged.emit() From b16fb5088b53686ed5d4bd2c826e76c4d094549e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 15:05:29 +0100 Subject: [PATCH 0250/1143] qml: padding around label above TextHighlightPanes --- electrum/gui/qml/components/AddressDetails.qml | 6 ++++-- electrum/gui/qml/components/InvoiceDialog.qml | 13 ++++++++++--- .../gui/qml/components/LightningPaymentDetails.qml | 9 ++++++--- electrum/gui/qml/components/TxDetails.qml | 9 ++++++--- electrum/gui/qml/components/WalletDetails.qml | 4 ++++ 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index b07d4fd99..6077225d2 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -72,8 +72,9 @@ Pane { } Label { - text: qsTr('Label') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Label') color: Material.accentColor } @@ -130,8 +131,9 @@ Pane { } Label { - text: qsTr('Public keys') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Public keys') color: Material.accentColor } diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index a414b813f..831a63435 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -94,8 +94,9 @@ ElDialog { } Label { - visible: invoice.invoiceType == Invoice.OnchainInvoice Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + visible: invoice.invoiceType == Invoice.OnchainInvoice text: qsTr('Address') color: Material.accentColor } @@ -116,6 +117,8 @@ ElDialog { } Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall visible: invoice.invoiceType == Invoice.LightningInvoice text: qsTr('Remote Pubkey') color: Material.accentColor @@ -152,6 +155,8 @@ ElDialog { } Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall visible: invoice.invoiceType == Invoice.LightningInvoice text: qsTr('Payment hash') color: Material.accentColor @@ -188,9 +193,10 @@ ElDialog { } Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall text: qsTr('Description') visible: invoice.message - Layout.columnSpan: 2 color: Material.accentColor } @@ -211,9 +217,10 @@ ElDialog { } Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall text: qsTr('Amount to send') color: Material.accentColor - Layout.columnSpan: 2 } TextHighlightPane { diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index 2fcfcc24f..097643947 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -74,8 +74,9 @@ Pane { } Label { - text: qsTr('Label') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Label') color: Material.accentColor } @@ -132,8 +133,9 @@ Pane { } Label { - text: qsTr('Payment hash') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Payment hash') color: Material.accentColor } @@ -164,8 +166,9 @@ Pane { } Label { - text: qsTr('Preimage') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Preimage') color: Material.accentColor } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 088409b9c..815b4336e 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -212,8 +212,9 @@ Pane { } Label { - text: qsTr('Label') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Label') color: Material.accentColor } @@ -270,8 +271,9 @@ Pane { } Label { - text: qsTr('Transaction ID') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Transaction ID') color: Material.accentColor } @@ -303,8 +305,9 @@ Pane { } Label { - text: qsTr('Outputs') Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + text: qsTr('Outputs') color: Material.accentColor } diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 9bcf2b342..54378d517 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -227,6 +227,7 @@ Pane { Label { Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall visible: Daemon.currentWallet.hasSeed text: qsTr('Seed') color: Material.accentColor @@ -267,6 +268,7 @@ Pane { Label { Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall visible: Daemon.currentWallet.isLightning text: qsTr('Lightning Node ID') color: Material.accentColor @@ -330,6 +332,7 @@ Pane { Label { Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall visible: _is2fa && !Daemon.currentWallet.canSignWithoutServer text: qsTr('Billing') color: Material.accentColor @@ -377,6 +380,7 @@ Pane { model: Daemon.currentWallet.keystores delegate: ColumnLayout { Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall RowLayout { Label { text: qsTr('Keystore') From ba888fc9fa28f28b0945b2e9eedd17f0a4a49ac7 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 15:50:13 +0100 Subject: [PATCH 0251/1143] qml: background flatbuttons within pages --- .../gui/qml/components/ChannelDetails.qml | 28 ++++++----- electrum/gui/qml/components/Preferences.qml | 25 ++++++---- electrum/gui/qml/components/TxDetails.qml | 46 +++++++++++-------- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index c9dc39778..fcd69b8f1 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -108,12 +108,14 @@ Pane { Layout.fillWidth: true Layout.preferredHeight: 1 } - FlatButton { - Layout.minimumWidth: implicitWidth - // icon.source: '../../icons/warning.png' - // icon.color: 'transparent' - text: channeldetails.frozenForSending ? qsTr('Unfreeze') : qsTr('Freeze') - onClicked: channeldetails.freezeForSending() + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 + FlatButton { + Layout.minimumWidth: implicitWidth + text: channeldetails.frozenForSending ? qsTr('Unfreeze') : qsTr('Freeze') + onClicked: channeldetails.freezeForSending() + } } } @@ -143,12 +145,14 @@ Pane { Layout.fillWidth: true Layout.preferredHeight: 1 } - FlatButton { - Layout.minimumWidth: implicitWidth - // icon.source: '../../icons/warning.png' - // icon.color: 'transparent' - text: channeldetails.frozenForReceiving ? qsTr('Unfreeze') : qsTr('Freeze') - onClicked: channeldetails.freezeForReceiving() + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 + FlatButton { + Layout.minimumWidth: implicitWidth + text: channeldetails.frozenForReceiving ? qsTr('Unfreeze') : qsTr('Freeze') + onClicked: channeldetails.freezeForReceiving() + } } } diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index ac1ac2d6d..656f49fa6 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -217,16 +217,23 @@ Pane { } } - Button { - text: qsTr('Modify') + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 visible: Config.pinCode != '' - onClicked: { - var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode}) - dialog.accepted.connect(function() { - Config.pinCode = dialog.pincode - dialog.close() - }) - dialog.open() + FlatButton { + text: qsTr('Modify') + onClicked: { + var dialog = pinSetup.createObject(preferences, { + mode: 'change', + pincode: Config.pinCode + }) + dialog.accepted.connect(function() { + Config.pinCode = dialog.pincode + dialog.close() + }) + dialog.open() + } } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 815b4336e..e0227437b 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -136,30 +136,38 @@ Pane { } ColumnLayout { Layout.alignment: Qt.AlignHCenter - FlatButton { - id: feebumpButton + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 visible: txdetails.canBump || txdetails.canCpfp - textUnderIcon: false - icon.source: '../../icons/add.png' - text: qsTr('Bump fee') - onClicked: { - if (txdetails.canBump) { - var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) - } else { - var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + FlatButton { + id: feebumpButton + textUnderIcon: false + icon.source: '../../icons/add.png' + text: qsTr('Bump fee') + onClicked: { + if (txdetails.canBump) { + var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) + } else { + var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + } + dialog.open() } - dialog.open() } } - FlatButton { - id: cancelButton + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 visible: txdetails.canCancel - textUnderIcon: false - icon.source: '../../icons/closebutton.png' - text: qsTr('Cancel Tx') - onClicked: { - var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) - dialog.open() + FlatButton { + id: cancelButton + textUnderIcon: false + icon.source: '../../icons/closebutton.png' + text: qsTr('Cancel Tx') + onClicked: { + var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) + dialog.open() + } } } } From 8fe181d757577fc21c2b509aca5f13a73ce6e5ed Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 3 Mar 2023 15:50:42 +0100 Subject: [PATCH 0252/1143] qml: sort languages --- electrum/gui/qml/qeconfig.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 0439bc801..af479941d 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -1,7 +1,8 @@ -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject - +import copy from decimal import Decimal +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject + from electrum.i18n import set_language, languages from electrum.logging import get_logger from electrum.util import DECIMAL_POINT_DEFAULT, format_satoshis @@ -34,7 +35,12 @@ def language(self, language): languagesChanged = pyqtSignal() @pyqtProperty('QVariantList', notify=languagesChanged) def languagesAvailable(self): - return list(map(lambda x: {'value': x[0], 'text': x[1]}, languages.items())) + # sort on translated languages, then re-add Default on top + langs = copy.deepcopy(languages) + default = langs.pop('') + langs_sorted = sorted(list(map(lambda x: {'value': x[0], 'text': x[1]}, langs.items())), key=lambda x: x['text']) + langs_sorted.insert(0, {'value': '', 'text': default}) + return langs_sorted autoConnectChanged = pyqtSignal() @pyqtProperty(bool, notify=autoConnectChanged) From 5673f08750673b31f2b3d8eadddb2e9a852793a7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 15:16:29 +0000 Subject: [PATCH 0253/1143] follow-up invoice changes: fix wallet.get_bolt11_inv if amt is None follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c Traceback (most recent call last): File "...\electrum\electrum\gui\qt\request_list.py", line 111, in item_changed self.receive_tab.update_current_request() File "...\electrum\electrum\gui\qt\receive_tab.py", line 227, in update_current_request lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else '' File "...\electrum\electrum\wallet.py", line 2515, in get_bolt11_invoice amount_msat = req.amount_msat if req.amount_msat > 0 else None TypeError: '>' not supported between instances of 'NoneType' and 'int' --- electrum/gui/qml/qerequestdetails.py | 2 +- electrum/wallet.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index 1956cdfa4..f957fad62 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -117,7 +117,7 @@ def expiration(self): @pyqtProperty(str, notify=detailsChanged) def bolt11(self): can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 - if self._req and can_receive > 0 and self._req.amount_msat/1000 <= can_receive: + if self._req and can_receive > 0 and (self._req.get_amount_sat() or 0) <= can_receive: return self._wallet.wallet.get_bolt11_invoice(self._req) else: return '' diff --git a/electrum/wallet.py b/electrum/wallet.py index 773894e95..bc6661ebf 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -329,7 +329,7 @@ def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: Si self._frozen_addresses = set(db.get('frozen_addresses', [])) self._frozen_coins = db.get_dict('frozen_coins') # type: Dict[str, bool] self.fiat_value = db.get_dict('fiat_value') - self._receive_requests = db.get_dict('payment_requests') # type: Dict[str, Invoice] + self._receive_requests = db.get_dict('payment_requests') # type: Dict[str, Request] self._invoices = db.get_dict('invoices') # type: Dict[str, Invoice] self._reserved_addresses = set(db.get('reserved_addresses', [])) @@ -2389,7 +2389,7 @@ def get_invoice_status(self, invoice: BaseInvoice): status = PR_PAID return self.check_expired_status(invoice, status) - def get_request_by_addr(self, addr: str) -> Optional[Invoice]: + def get_request_by_addr(self, addr: str) -> Optional[Request]: """Returns a relevant request for address, from an on-chain PoV. (One that has been paid on-chain or is pending) @@ -2417,7 +2417,7 @@ def get_request_by_addr(self, addr: str) -> Optional[Invoice]: reqs.sort(key=lambda req: req.get_time()) return reqs[-1] - def get_request(self, request_id: str) -> Optional[Invoice]: + def get_request(self, request_id: str) -> Optional[Request]: return self._receive_requests.get(request_id) def get_formatted_request(self, request_id): @@ -2512,7 +2512,8 @@ def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None: def get_bolt11_invoice(self, req: Request) -> str: if not self.lnworker: return '' - amount_msat = req.amount_msat if req.amount_msat > 0 else None + amount_msat = req.get_amount_msat() or None + assert (amount_msat is None or amount_msat > 0), amount_msat lnaddr, invoice = self.lnworker.get_bolt11_invoice( payment_hash=req.payment_hash, amount_msat=amount_msat, @@ -2545,7 +2546,7 @@ def create_request(self, amount_sat: int, message: str, exp_delay: int, address: key = self.add_payment_request(req) return key - def add_payment_request(self, req: Invoice, *, write_to_disk: bool = True): + def add_payment_request(self, req: Request, *, write_to_disk: bool = True): request_id = req.get_id() self._receive_requests[request_id] = req if addr:=req.get_address(): @@ -2577,7 +2578,7 @@ def delete_invoice(self, invoice_id, *, write_to_disk: bool = True): if write_to_disk: self.save_db() - def get_sorted_requests(self) -> List[Invoice]: + def get_sorted_requests(self) -> List[Request]: """ sorted by timestamp """ out = [self.get_request(x) for x in self._receive_requests.keys()] out = [x for x in out if x is not None] From b42b5c0c0fc7f715464fbed6b969c020761ce41c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 15:18:01 +0000 Subject: [PATCH 0254/1143] follow-up invoice changes: fix kivy ReceiveScreen follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c --- electrum/gui/kivy/uix/screens.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 143e44ae2..6476e2e1a 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -14,7 +14,7 @@ from electrum.invoices import (PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, - pr_expiration_values, Invoice) + pr_expiration_values, Invoice, Request) from electrum import bitcoin, constants from electrum import lnutil from electrum.transaction import tx_from_any, PartialTxOutput @@ -536,12 +536,12 @@ def new_request(self): self.update() self.app.show_request(key) - def get_card(self, req: Invoice) -> Dict[str, Any]: + def get_card(self, req: Request) -> Dict[str, Any]: is_lightning = req.is_lightning() if not is_lightning: address = req.get_address() else: - address = req.lightning_invoice + address = self.app.wallet.get_bolt11_invoice(req) key = req.get_id() amount = req.get_amount_sat() description = req.message From a1a1fae4cc897c2c229bb0dd75b14c956a26f574 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 15:21:47 +0000 Subject: [PATCH 0255/1143] invoices.py: small clean-up --- electrum/invoices.py | 21 ++++++++++++++++----- electrum/lnworker.py | 5 +++-- electrum/wallet.py | 6 +++--- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/electrum/invoices.py b/electrum/invoices.py index 2791258bc..59065162e 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -107,8 +107,16 @@ class BaseInvoice(StoredObject): #bip70_requestor = attr.ib(type=str, kw_only=True) # type: Optional[str] - def is_lightning(self): - return self.lightning_invoice is not None + def is_lightning(self) -> bool: + raise NotImplementedError() + + def get_address(self) -> Optional[str]: + """returns the first address, to be displayed in GUI""" + raise NotImplementedError() + + @property + def rhash(self) -> str: + raise NotImplementedError() def get_status_str(self, status): status_str = pr_tooltips[status] @@ -240,8 +248,10 @@ class Invoice(BaseInvoice): lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] __lnaddr = None + def is_lightning(self): + return self.lightning_invoice is not None + def get_address(self) -> Optional[str]: - """returns the first address, to be displayed in GUI""" address = None if self.outputs: address = self.outputs[0].address if len(self.outputs) > 0 else None @@ -257,6 +267,7 @@ def _lnaddr(self) -> LnAddr: @property def rhash(self) -> str: + assert self.is_lightning() return self._lnaddr.paymenthash.hex() @lightning_invoice.validator @@ -295,7 +306,6 @@ def is_lightning(self): return self.payment_hash is not None def get_address(self) -> Optional[str]: - """returns the first address, to be displayed in GUI""" address = None if self.outputs: address = self.outputs[0].address if len(self.outputs) > 0 else None @@ -303,9 +313,10 @@ def get_address(self) -> Optional[str]: @property def rhash(self) -> str: + assert self.is_lightning() return self.payment_hash.hex() -def get_id_from_onchain_outputs(outputs: List[PartialTxOutput], *, timestamp: int) -> str: +def get_id_from_onchain_outputs(outputs: Sequence[PartialTxOutput], *, timestamp: int) -> str: outputs_str = "\n".join(f"{txout.scriptpubkey.hex()}, {txout.value}" for txout in outputs) return sha256d(outputs_str + "%d" % timestamp).hex()[0:10] diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 5ecaaabb7..eae2ac43a 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -30,6 +30,7 @@ from . import keystore from .util import profiler, chunks, OldTaskGroup from .invoices import Invoice, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LN_EXPIRY_NEVER +from .invoices import BaseInvoice from .util import NetworkRetryManager, JsonRPCClient, NotEnoughFunds from .util import EventListener, event_listener from .lnutil import LN_MAX_FUNDING_SAT @@ -1899,7 +1900,7 @@ def get_payment_status(self, payment_hash: bytes) -> int: info = self.get_payment_info(payment_hash) return info.status if info else PR_UNPAID - def get_invoice_status(self, invoice: Invoice) -> int: + def get_invoice_status(self, invoice: BaseInvoice) -> int: invoice_id = invoice.rhash if invoice_id in self.inflight_payments: return PR_INFLIGHT @@ -2291,7 +2292,7 @@ async def rebalance_channels(self, chan1, chan2, amount_msat): return await self.pay_invoice( invoice, channels=[chan1]) - def can_receive_invoice(self, invoice: Invoice) -> bool: + def can_receive_invoice(self, invoice: BaseInvoice) -> bool: assert invoice.is_lightning() return (invoice.get_amount_sat() or 0) <= self.num_sats_can_receive() diff --git a/electrum/wallet.py b/electrum/wallet.py index bc6661ebf..a1d607518 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2361,11 +2361,11 @@ def import_addresses(self, addresses: List[str], *, def delete_address(self, address: str) -> None: raise Exception("this wallet cannot delete addresses") - def get_request_URI(self, req: Invoice) -> Optional[str]: + def get_request_URI(self, req: Request) -> Optional[str]: include_lightning = bool(self.config.get('bip21_lightning', False)) return req.get_bip21_URI(include_lightning=include_lightning) - def check_expired_status(self, r: Invoice, status): + def check_expired_status(self, r: BaseInvoice, status): #if r.is_lightning() and r.exp == 0: # status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds if status == PR_UNPAID and r.has_expired(): @@ -2850,7 +2850,7 @@ def get_tx_fee_warning( else: return allow_send, long_warning, short_warning - def get_help_texts_for_receive_request(self, req: Invoice) -> ReceiveRequestHelp: + def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp: key = req.get_id() addr = req.get_address() or '' amount_sat = req.get_amount_sat() or 0 From 26cc1b83084e38c07b07a5623584e790ebe6b21b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 15:34:36 +0000 Subject: [PATCH 0256/1143] follow-up invoice changes: fix qt lightning_tx_dialog follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c Traceback (most recent call last): File "...\electrum\electrum\gui\qt\history_list.py", line 673, in mouseDoubleClickEvent self.parent.show_lightning_transaction(tx_item) File "...\electrum\electrum\gui\qt\main_window.py", line 1082, in show_lightning_transaction d = LightningTxDialog(self, tx_item) File "...\electrum\electrum\gui\qt\lightning_tx_dialog.py", line 60, in __init__ self.invoice = invoice.lightning_invoice AttributeError: 'Request' object has no attribute 'lightning_invoice' --- electrum/gui/qt/lightning_tx_dialog.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/lightning_tx_dialog.py b/electrum/gui/qt/lightning_tx_dialog.py index e51411796..34a9ffacf 100644 --- a/electrum/gui/qt/lightning_tx_dialog.py +++ b/electrum/gui/qt/lightning_tx_dialog.py @@ -32,6 +32,7 @@ from electrum.i18n import _ from electrum.lnworker import PaymentDirection +from electrum.invoices import Invoice from .util import WindowModalDialog, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, font_height from .qrtextedit import ShowQRTextEdit @@ -53,13 +54,11 @@ def __init__(self, parent: 'ElectrumWindow', tx_item: dict): self.amount = Decimal(tx_item['amount_msat']) / 1000 self.payment_hash = tx_item['payment_hash'] self.preimage = tx_item['preimage'] - invoice = (self.parent.wallet.get_invoice(self.payment_hash) - or self.parent.wallet.get_request(self.payment_hash)) + self.invoice = "" + invoice = self.parent.wallet.get_invoice(self.payment_hash) # only check outgoing invoices if invoice: assert invoice.is_lightning(), f"{self.invoice!r}" self.invoice = invoice.lightning_invoice - else: - self.invoice = '' self.setMinimumWidth(700) vbox = QVBoxLayout() self.setLayout(vbox) @@ -78,9 +77,10 @@ def __init__(self, parent: 'ElectrumWindow', tx_item: dict): vbox.addWidget(QLabel(_("Preimage") + ":")) self.preimage_e = ShowQRLineEdit(self.preimage, self.config, title=_("Preimage")) vbox.addWidget(self.preimage_e) - vbox.addWidget(QLabel(_("Lightning Invoice") + ":")) - self.invoice_e = ShowQRTextEdit(self.invoice, config=self.config) - self.invoice_e.setMaximumHeight(max(150, 10 * font_height())) - self.invoice_e.addCopyButton() - vbox.addWidget(self.invoice_e) + if self.invoice: + vbox.addWidget(QLabel(_("Lightning Invoice") + ":")) + self.invoice_e = ShowQRTextEdit(self.invoice, config=self.config) + self.invoice_e.setMaximumHeight(max(150, 10 * font_height())) + self.invoice_e.addCopyButton() + vbox.addWidget(self.invoice_e) vbox.addLayout(Buttons(CloseButton(self))) From ca0e4d21f1d6749a7cfa332377fb8a921210e9c1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 15:46:15 +0000 Subject: [PATCH 0257/1143] follow-up invoice changes: fix kivy lightning_tx_dialog follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c --- electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py b/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py index bdd588d73..04db15f4b 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py @@ -118,9 +118,7 @@ def __init__(self, app, tx_item): self.amount_str = format_amount(-self.amount if self.is_sent else self.amount) if tx_item.get('fee_msat'): self.fee_str = format_amount(Decimal(tx_item['fee_msat']) / 1000) - invoice = (self.app.wallet.get_invoice(self.payment_hash) - or self.app.wallet.get_request(self.payment_hash)) + self.invoice = '' + invoice = self.app.wallet.get_invoice(self.payment_hash) # only check outgoing invoices if invoice: self.invoice = invoice.lightning_invoice or '' - else: - self.invoice = '' From 9e81aba5781eb6c9703abb1434f18e82a3eb1418 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 15:56:00 +0000 Subject: [PATCH 0258/1143] follow-up invoice changes: fix kivy RequestDialog follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c --- electrum/gui/kivy/uix/dialogs/request_dialog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/kivy/uix/dialogs/request_dialog.py b/electrum/gui/kivy/uix/dialogs/request_dialog.py index 728f2cf34..a4aff499c 100644 --- a/electrum/gui/kivy/uix/dialogs/request_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/request_dialog.py @@ -186,7 +186,9 @@ def update_status(self): help_texts = self.app.wallet.get_help_texts_for_receive_request(req) address = req.get_address() or '' URI = self.app.wallet.get_request_URI(req) or '' - lnaddr = req.lightning_invoice or '' + lnaddr = "" + if req.is_lightning(): + lnaddr = self.app.wallet.get_bolt11_invoice(req) self.status = self.app.wallet.get_invoice_status(req) self.status_str = req.get_status_str(self.status) self.status_color = pr_color[self.status] From 81bd6f7d1bfd2de93f2116106b33158263476031 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 16:14:35 +0000 Subject: [PATCH 0259/1143] follow-up invoice changes: fix "Add lightning invoice to bitcoin URIs" follow-up 719b468eee8b3e13680f6e7b90194d618181fe0c --- electrum/invoices.py | 45 +++++++++++++++++++++++--------------------- electrum/wallet.py | 6 ++++-- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/electrum/invoices.py b/electrum/invoices.py index 59065162e..7aa2e0488 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -166,27 +166,6 @@ def get_amount_sat(self) -> Union[int, str, None]: return amount_msat return int(amount_msat // 1000) - def get_bip21_URI(self, *, include_lightning: bool = False) -> Optional[str]: - from electrum.util import create_bip21_uri - addr = self.get_address() - amount = self.get_amount_sat() - if amount is not None: - amount = int(amount) - message = self.message - extra = {} - if self.time and self.exp: - extra['time'] = str(int(self.time)) - extra['exp'] = str(int(self.exp)) - lightning = self.lightning_invoice if include_lightning else None - if lightning: - extra['lightning'] = lightning - if not addr and lightning: - return "bitcoin:?lightning="+lightning - if not addr and not lightning: - return None - uri = create_bip21_uri(addr, amount, message, extra_query_params=extra) - return str(uri) - @amount_msat.validator def _validate_amount(self, attribute, value): if value is None: @@ -316,6 +295,30 @@ def rhash(self) -> str: assert self.is_lightning() return self.payment_hash.hex() + def get_bip21_URI( + self, + *, + lightning_invoice: Optional[str] = None, + ) -> Optional[str]: + from electrum.util import create_bip21_uri + addr = self.get_address() + amount = self.get_amount_sat() + if amount is not None: + amount = int(amount) + message = self.message + extra = {} + if self.time and self.exp: + extra['time'] = str(int(self.time)) + extra['exp'] = str(int(self.exp)) + if lightning_invoice: + extra['lightning'] = lightning_invoice + if not addr and lightning_invoice: + return "bitcoin:?lightning="+lightning_invoice + if not addr and not lightning_invoice: + return None + uri = create_bip21_uri(addr, amount, message, extra_query_params=extra) + return str(uri) + def get_id_from_onchain_outputs(outputs: Sequence[PartialTxOutput], *, timestamp: int) -> str: outputs_str = "\n".join(f"{txout.scriptpubkey.hex()}, {txout.value}" for txout in outputs) diff --git a/electrum/wallet.py b/electrum/wallet.py index a1d607518..3ef44d9ba 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2362,8 +2362,10 @@ def delete_address(self, address: str) -> None: raise Exception("this wallet cannot delete addresses") def get_request_URI(self, req: Request) -> Optional[str]: - include_lightning = bool(self.config.get('bip21_lightning', False)) - return req.get_bip21_URI(include_lightning=include_lightning) + lightning_invoice = None + if self.config.get('bip21_lightning', False): + lightning_invoice = self.get_bolt11_invoice(req) + return req.get_bip21_URI(lightning_invoice=lightning_invoice) def check_expired_status(self, r: BaseInvoice, status): #if r.is_lightning() and r.exp == 0: From ec889b8c3ffeb830d94fe184700a58c0550cc5f4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 3 Mar 2023 16:35:34 +0000 Subject: [PATCH 0260/1143] wallet: fix import_requests, and mention quirk re preimages --- electrum/lnworker.py | 2 ++ electrum/wallet.py | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index eae2ac43a..9b69156a6 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1809,6 +1809,8 @@ def get_bolt11_invoice( self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}") invoice_features = self.features.for_invoice() payment_preimage = self.get_preimage(payment_hash) + if payment_preimage is None: # e.g. when export/importing requests between wallets + raise Exception("missing preimage for payment_hash") amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None if expiry == 0: expiry = LN_EXPIRY_NEVER diff --git a/electrum/wallet.py b/electrum/wallet.py index 3ef44d9ba..cd88432e5 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1066,13 +1066,14 @@ def import_requests(self, path): data = read_json_file(path) for x in data: try: - req = Invoice(**x) + req = Request(**x) except: raise FileImportFailed(_("Invalid invoice format")) self.add_payment_request(req, write_to_disk=False) self.save_db() def export_requests(self, path): + # note: this does not export preimages for LN bolt11 invoices write_json_file(path, list(self._receive_requests.values())) def import_invoices(self, path): @@ -1125,7 +1126,7 @@ def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> for txout in invoice.get_outputs(): self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key) - def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Optional[int], Sequence[str]]: + def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]: """Returns whether on-chain invoice/request is satisfied, num confs required txs have, and list of relevant TXIDs. """ @@ -1161,7 +1162,7 @@ def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Optional[int is_paid = False return is_paid, conf_needed, list(relevant_txs) - def is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Optional[int]]: + def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]: is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice) return is_paid, conf_needed From 0647a2cf9f66ce1c3ada0f1d05c29e73d6a95a59 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 13:28:31 +0000 Subject: [PATCH 0261/1143] transaction.py: rm PartialTxInput.{num_sig, script_type} --- electrum/commands.py | 5 -- electrum/descriptor.py | 90 +++++++++++++++++++ electrum/lnsweep.py | 3 - electrum/lnutil.py | 4 - electrum/plugins/bitbox02/bitbox02.py | 5 +- .../plugins/digitalbitbox/digitalbitbox.py | 9 +- electrum/plugins/jade/jade.py | 5 +- electrum/plugins/keepkey/keepkey.py | 14 +-- electrum/plugins/ledger/ledger.py | 35 ++++---- electrum/plugins/safe_t/safe_t.py | 14 +-- electrum/plugins/trezor/trezor.py | 14 +-- electrum/submarine_swaps.py | 4 - electrum/tests/test_transaction.py | 11 --- electrum/tests/test_wallet_vertical.py | 8 -- electrum/transaction.py | 63 +++++-------- electrum/wallet.py | 45 ++-------- 16 files changed, 169 insertions(+), 160 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 6d8fc7bf7..33073bb39 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -397,9 +397,6 @@ async def serialize(self, jsontx): keypairs[pubkey] = privkey, compressed desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) txin.script_descriptor = desc - txin.script_type = txin_type - txin.pubkeys = [bfh(pubkey)] - txin.num_sig = 1 inputs.append(txin) outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout.get('value', txout['value_sats']))) @@ -428,8 +425,6 @@ async def signtransaction_with_privkey(self, tx, privkey): if address in txins_dict.keys(): for txin in txins_dict[address]: txin.script_descriptor = desc - txin.pubkeys = [pubkey] - txin.script_type = txin_type tx.sign({pubkey.hex(): (priv2, compressed)}) return tx.serialize() diff --git a/electrum/descriptor.py b/electrum/descriptor.py index b87f52be7..be33bd3fc 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -43,6 +43,7 @@ Tuple, Sequence, Mapping, + Set, ) @@ -376,6 +377,22 @@ def satisfy( script_sig=script_sig, ) + def get_satisfaction_progress( + self, + *, + sigdata: Mapping[bytes, bytes] = None, # pubkey -> sig + ) -> Tuple[int, int]: + """Returns (num_sigs_we_have, num_sigs_required) towards satisfying this script. + Besides signatures, later this can also consider hash-preimages. + """ + assert not self.is_range() + nhave, nreq = 0, 0 + for desc in self.subdescriptors: + a, b = desc.get_satisfaction_progress() + nhave += a + nreq += b + return nhave, nreq + def is_range(self) -> bool: for pubkey in self.pubkeys: if pubkey.is_range(): @@ -388,6 +405,47 @@ def is_range(self) -> bool: def is_segwit(self) -> bool: return any([desc.is_segwit() for desc in self.subdescriptors]) + def get_all_pubkeys(self) -> Set[bytes]: + """Returns set of pubkeys that appear at any level in this descriptor.""" + assert not self.is_range() + all_pubkeys = set([p.get_pubkey_bytes() for p in self.pubkeys]) + for desc in self.subdescriptors: + all_pubkeys |= desc.get_all_pubkeys() + return all_pubkeys + + def get_simple_singlesig(self) -> Optional['Descriptor']: + """Returns innermost pk/pkh/wpkh descriptor, or None if we are not a simple singlesig. + + note: besides pk,pkh,sh(wpkh),wpkh, overly complicated stuff such as sh(pk),wsh(sh(pkh),etc is also accepted + """ + if len(self.subdescriptors) == 1: + return self.subdescriptors[0].get_simple_singlesig() + return None + + def get_simple_multisig(self) -> Optional['MultisigDescriptor']: + """Returns innermost multi descriptor, or None if we are not a simple multisig.""" + if len(self.subdescriptors) == 1: + return self.subdescriptors[0].get_simple_multisig() + return None + + def to_legacy_electrum_script_type(self) -> str: + if isinstance(self, PKDescriptor): + return "p2pk" + elif isinstance(self, PKHDescriptor): + return "p2pkh" + elif isinstance(self, WPKHDescriptor): + return "p2wpkh" + elif isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], WPKHDescriptor): + return "p2wpkh-p2sh" + elif isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], MultisigDescriptor): + return "p2sh" + elif isinstance(self, WSHDescriptor) and isinstance(self.subdescriptors[0], MultisigDescriptor): + return "p2wsh" + elif (isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], WSHDescriptor) + and isinstance(self.subdescriptors[0].subdescriptors[0], MultisigDescriptor)): + return "p2wsh-p2sh" + return "unknown" + class PKDescriptor(Descriptor): """ @@ -421,6 +479,14 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn witness_items=(sig,), ) + def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: + if sigdata is None: sigdata = {} + signatures = list(sigdata.values()) + return len(signatures), 1 + + def get_simple_singlesig(self) -> Optional['Descriptor']: + return self + class PKHDescriptor(Descriptor): """ @@ -455,6 +521,14 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn witness_items=(sig, pubkey), ) + def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: + if sigdata is None: sigdata = {} + signatures = list(sigdata.values()) + return len(signatures), 1 + + def get_simple_singlesig(self) -> Optional['Descriptor']: + return self + class WPKHDescriptor(Descriptor): """ @@ -492,9 +566,17 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn witness_items=(sig, pubkey), ) + def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: + if sigdata is None: sigdata = {} + signatures = list(sigdata.values()) + return len(signatures), 1 + def is_segwit(self) -> bool: return True + def get_simple_singlesig(self) -> Optional['Descriptor']: + return self + class MultisigDescriptor(Descriptor): """ @@ -552,6 +634,14 @@ def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionIn witness_items=(0, *signatures), ) + def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: + if sigdata is None: sigdata = {} + signatures = list(sigdata.values()) + return len(signatures), self.thresh + + def get_simple_multisig(self) -> Optional['MultisigDescriptor']: + return self + class SHDescriptor(Descriptor): """ diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index d9a989242..10151c33c 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -516,9 +516,6 @@ def create_sweeptx_their_ctx_to_remote( txin._trusted_value_sats = val desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey, script_type='p2wpkh') txin.script_descriptor = desc - txin.script_type = 'p2wpkh' - txin.pubkeys = [bfh(our_payment_pubkey)] - txin.num_sig = 1 sweep_inputs = [txin] tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 8fab2a86f..b325fb7ab 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -823,10 +823,6 @@ def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes ppubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=ppubkeys, thresh=2, is_sorted=True) c_input.script_descriptor = descriptor.WSHDescriptor(subdescriptor=multi) - - c_input.script_type = 'p2wsh' - c_input.pubkeys = [bfh(pk) for pk in pubkeys] - c_input.num_sig = 2 c_input._trusted_value_sats = funding_sat return c_input diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index cb7e614a1..4b328dc6b 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -444,9 +444,10 @@ def sign_transaction( } ) + assert (desc := txin.script_descriptor) if tx_script_type is None: - tx_script_type = txin.script_type - elif tx_script_type != txin.script_type: + tx_script_type = desc.to_legacy_electrum_script_type() + elif tx_script_type != desc.to_legacy_electrum_script_type(): raise Exception("Cannot mix different input script types") if tx_script_type == "p2wpkh": diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 2d64b2f66..1f774c62d 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -19,6 +19,7 @@ from electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.bitcoin import public_key_to_p2pkh from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation +from electrum import descriptor from electrum import ecc from electrum.ecc import msg_magic from electrum.wallet import Standard_Wallet @@ -527,7 +528,8 @@ def sign_transaction(self, tx, password): if txin.is_coinbase_input(): self.give_error("Coinbase not supported") # should never happen - if txin.script_type != 'p2pkh': + assert (desc := txin.script_descriptor) + if desc.to_legacy_electrum_script_type() != 'p2pkh': p2pkhTransaction = False my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin) @@ -557,9 +559,10 @@ def sign_transaction(self, tx, password): tx_copy = copy.deepcopy(tx) # monkey-patch method of tx_copy instance to change serialization def input_script(self, txin: PartialTxInput, *, estimate_size=False): - if txin.script_type == 'p2pkh': + desc = txin.script_descriptor + if isinstance(desc, descriptor.PKHDescriptor): return Transaction.get_preimage_script(txin) - raise Exception("unsupported type %s" % txin.script_type) + raise Exception(f"unsupported txin type. only p2pkh is supported. got: {desc.to_string()[:10]}") tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction) tx_dbb_serialized = tx_copy.serialize_to_network() else: diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 0869cee41..135713d6d 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -264,7 +264,7 @@ def sign_transaction(self, tx, password): jade_inputs = [] for txin in tx.inputs(): pubkey, path = self.find_my_pubkey_in_txinout(txin) - witness_input = txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh', 'p2wpkh', 'p2wsh'] + witness_input = txin.is_segwit() redeem_script = Transaction.get_preimage_script(txin) redeem_script = bytes.fromhex(redeem_script) if redeem_script is not None else None input_tx = txin.utxo @@ -280,6 +280,7 @@ def sign_transaction(self, tx, password): change = [None] * len(tx.outputs()) for index, txout in enumerate(tx.outputs()): if txout.is_mine and txout.is_change: + assert (desc := txout.script_descriptor) if is_multisig: # Multisig - wallet details must be registered on Jade hw multisig_name = _register_multisig_wallet(wallet, self, txout.address) @@ -294,7 +295,7 @@ def sign_transaction(self, tx, password): else: # Pass entire path pubkey, path = self.find_my_pubkey_in_txinout(txout) - change[index] = {'path':path, 'variant': txout.script_type} + change[index] = {'path':path, 'variant': desc.to_legacy_electrum_script_type()} # The txn itself txn_bytes = bytes.fromhex(tx.serialize_to_network()) diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 7bbb0d7ae..0e3c36019 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -376,12 +376,13 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeySto assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - if len(txin.pubkeys) > 1: + assert (desc := txin.script_descriptor) + if multi := desc.get_simple_multisig(): xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) - multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) else: multisig = None - script_type = self.get_keepkey_input_script_type(txin.script_type) + script_type = self.get_keepkey_input_script_type(desc.to_legacy_electrum_script_type()) txinputtype = self.types.TxInputType( script_type=script_type, multisig=multisig) @@ -418,10 +419,11 @@ def _make_multisig(self, m, xpubs): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'): def create_output_by_derivation(): - script_type = self.get_keepkey_output_script_type(txout.script_type) - if len(txout.pubkeys) > 1: + assert (desc := txout.script_descriptor) + script_type = self.get_keepkey_output_script_type(desc.to_legacy_electrum_script_type()) + if multi := desc.get_simple_multisig(): xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) - multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) else: multisig = None my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index be1ca437f..5b1d213db 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -8,6 +8,7 @@ from electrum import bip32, constants, ecc +from electrum import descriptor from electrum.base_wizard import ScriptTypeNotSupported from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath from electrum.bitcoin import EncodeBase58Check, int_to_hex, is_b58_address, is_segwit_script_type, var_int @@ -16,7 +17,7 @@ from electrum.keystore import Hardware_KeyStore from electrum.logging import get_logger from electrum.plugin import Device, runs_in_hwd_thread -from electrum.transaction import PartialTransaction, Transaction +from electrum.transaction import PartialTransaction, Transaction, PartialTxInput from electrum.util import bfh, UserFacingException, versiontuple from electrum.wallet import Standard_Wallet @@ -544,20 +545,25 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, pin = "" # prompt for the PIN before displaying the dialog if necessary + def is_txin_legacy_multisig(txin: PartialTxInput) -> bool: + desc = txin.script_descriptor + return (isinstance(desc, descriptor.SHDescriptor) + and isinstance(desc.subdescriptors[0], descriptor.MultisigDescriptor)) + # Fetch inputs of the transaction to sign for txin in tx.inputs(): if txin.is_coinbase_input(): self.give_error("Coinbase not supported") # should never happen - if txin.script_type in ['p2sh']: + if is_txin_legacy_multisig(txin): p2shTransaction = True - if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']: + if txin.is_p2sh_segwit(): if not self.supports_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True - if txin.script_type in ['p2wpkh', 'p2wsh']: + if txin.is_native_segwit(): if not self.supports_native_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True @@ -584,7 +590,7 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, # Sanity check if p2shTransaction: for txin in tx.inputs(): - if txin.script_type != 'p2sh': + if not is_txin_legacy_multisig(txin): self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen txOutput = var_int(len(tx.outputs())) @@ -1083,7 +1089,6 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, raise UserFacingException("Coinbase not supported") # should never happen utxo = None - scriptcode = b"" if psbt_in.witness_utxo: utxo = psbt_in.witness_utxo if psbt_in.non_witness_utxo: @@ -1094,19 +1099,9 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, if utxo is None: continue - scriptcode = utxo.scriptPubKey - if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh']: - if len(psbt_in.redeem_script) == 0: - continue - scriptcode = psbt_in.redeem_script - elif electrum_txin.script_type in ['p2wsh', 'p2wsh-p2sh']: - if len(psbt_in.witness_script) == 0: - continue - scriptcode = psbt_in.witness_script - - p2sh = False - if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh', 'p2wsh-p2sh']: - p2sh = True + if (desc := electrum_txin.script_descriptor) is None: + raise Exception("script_descriptor missing for txin ") + scriptcode = desc.expand().scriptcode_for_sighash is_wit, wit_ver, __ = is_witness(psbt_in.redeem_script or utxo.scriptPubKey) @@ -1115,7 +1110,7 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, # if it's a segwit spend (any version), make sure the witness_utxo is also present psbt_in.witness_utxo = utxo - if p2sh: + if electrum_txin.is_p2sh_segwit(): if wit_ver == 0: script_addrtype = AddressType.SH_WIT else: diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 376bbfa60..863fa796d 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -346,12 +346,13 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - if len(txin.pubkeys) > 1: + assert (desc := txin.script_descriptor) + if multi := desc.get_simple_multisig(): xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) - multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) else: multisig = None - script_type = self.get_safet_input_script_type(txin.script_type) + script_type = self.get_safet_input_script_type(desc.to_legacy_electrum_script_type()) txinputtype = self.types.TxInputType( script_type=script_type, multisig=multisig) @@ -388,10 +389,11 @@ def _make_multisig(self, m, xpubs): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'): def create_output_by_derivation(): - script_type = self.get_safet_output_script_type(txout.script_type) - if len(txout.pubkeys) > 1: + assert (desc := txout.script_descriptor) + script_type = self.get_safet_output_script_type(desc.to_legacy_electrum_script_type()) + if multi := desc.get_simple_multisig(): xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) - multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) else: multisig = None my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index db52bd53f..503eed6bb 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -417,10 +417,11 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - if len(txin.pubkeys) > 1: + assert (desc := txin.script_descriptor) + if multi := desc.get_simple_multisig(): xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) - txinputtype.multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) - txinputtype.script_type = self.get_trezor_input_script_type(txin.script_type) + txinputtype.multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type()) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) if full_path: txinputtype.address_n = full_path @@ -445,10 +446,11 @@ def _make_multisig(self, m, xpubs): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'): def create_output_by_derivation(): - script_type = self.get_trezor_output_script_type(txout.script_type) - if len(txout.pubkeys) > 1: + assert (desc := txout.script_descriptor) + script_type = self.get_trezor_output_script_type(desc.to_legacy_electrum_script_type()) + if multi := desc.get_simple_multisig(): xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) - multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) else: multisig = None my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 9532bc8a6..7b82b739a 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -124,7 +124,6 @@ def create_claim_tx( """Create tx to either claim successful reverse-swap, or to get refunded for timed-out forward-swap. """ - txin.script_type = 'p2wsh' txin.script_sig = b'' txin.witness_script = witness_script txout = PartialTxOutput.from_address_and_value(address, amount_sat) @@ -622,8 +621,6 @@ def add_txin_info(self, txin: PartialTxInput) -> None: return preimage = swap.preimage if swap.is_reverse else 0 witness_script = swap.redeem_script - txin.script_type = 'p2wsh' - txin.num_sig = 1 # hack so that txin not considered "is_complete" txin.script_sig = b'' txin.witness_script = witness_script sig_dummy = b'\x00' * 71 # DER-encoded ECDSA sig, with low S and low R @@ -637,7 +634,6 @@ def sign_tx(cls, tx: PartialTransaction, swap: SwapData) -> None: txin = tx.inputs()[0] assert len(tx.inputs()) == 1, f"expected 1 input for swap claim tx. found {len(tx.inputs())}" assert txin.prevout.txid.hex() == swap.funding_txid - txin.script_type = 'p2wsh' txin.script_sig = b'' txin.witness_script = witness_script sig = bytes.fromhex(tx.sign_txin(0, swap.privkey)) diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py index 1e7264aad..50fbafeda 100644 --- a/electrum/tests/test_transaction.py +++ b/electrum/tests/test_transaction.py @@ -94,9 +94,6 @@ def test_tx_update_signatures(self): script_type = 'p2pkh' desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=script_type) tx.inputs()[0].script_descriptor = desc - tx.inputs()[0].script_type = script_type - tx.inputs()[0].pubkeys = [pubkey] - tx.inputs()[0].num_sig = 1 tx.update_signatures(signed_blob_signatures) self.assertEqual(tx.serialize(), signed_blob) @@ -878,7 +875,6 @@ def test_spending_op_cltv_p2sh(self): prevout = TxOutpoint(txid=bfh('6d500966f9e494b38a04545f0cea35fc7b3944e341a64b804fed71cdee11d434'), out_idx=1) txin = PartialTxInput(prevout=prevout) txin.nsequence = 2 ** 32 - 3 - txin.script_type = 'p2sh' redeem_script = bfh(construct_script([ locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG, ])) @@ -940,7 +936,6 @@ class TestSighashTypes(ElectrumTestCase): prevout = TxOutpoint(txid=bfh('6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436'), out_idx=1) txin = PartialTxInput(prevout=prevout) txin.nsequence=0xffffffff - txin.script_type='p2sh-p2wsh' txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae') txin.redeem_script = bfh('0020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54') txin._trusted_value_sats = 987654321 @@ -950,7 +945,6 @@ class TestSighashTypes(ElectrumTestCase): def test_check_sighash_types_sighash_all(self): self.txin.sighash=Sighash.ALL - self.txin.pubkeys = [bfh('0307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba3')] privkey = bfh('730fff80e1413068a05b57d6a58261f07551163369787f349438ea38ca80fac6') tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) sig = tx.sign_txin(0,privkey) @@ -959,7 +953,6 @@ def test_check_sighash_types_sighash_all(self): def test_check_sighash_types_sighash_none(self): self.txin.sighash=Sighash.NONE - self.txin.pubkeys = [bfh('03b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b')] privkey = bfh('11fa3d25a17cbc22b29c44a484ba552b5a53149d106d3d853e22fdd05a2d8bb3') tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) sig = tx.sign_txin(0,privkey) @@ -968,7 +961,6 @@ def test_check_sighash_types_sighash_none(self): def test_check_sighash_types_sighash_single(self): self.txin.sighash=Sighash.SINGLE - self.txin.pubkeys = [bfh('034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a')] privkey = bfh('77bf4141a87d55bdd7f3cd0bdccf6e9e642935fec45f2f30047be7b799120661') tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) sig = tx.sign_txin(0,privkey) @@ -978,7 +970,6 @@ def test_check_sighash_types_sighash_single(self): @disable_ecdsa_r_value_grinding def test_check_sighash_types_sighash_all_anyonecanpay(self): self.txin.sighash=Sighash.ALL|Sighash.ANYONECANPAY - self.txin.pubkeys = [bfh('033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f4')] privkey = bfh('14af36970f5025ea3e8b5542c0f8ebe7763e674838d08808896b63c3351ffe49') tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) sig = tx.sign_txin(0,privkey) @@ -988,7 +979,6 @@ def test_check_sighash_types_sighash_all_anyonecanpay(self): @disable_ecdsa_r_value_grinding def test_check_sighash_types_sighash_none_anyonecanpay(self): self.txin.sighash=Sighash.NONE|Sighash.ANYONECANPAY - self.txin.pubkeys = [bfh('03a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac16')] privkey = bfh('fe9a95c19eef81dde2b95c1284ef39be497d128e2aa46916fb02d552485e0323') tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) sig = tx.sign_txin(0,privkey) @@ -997,7 +987,6 @@ def test_check_sighash_types_sighash_none_anyonecanpay(self): def test_check_sighash_types_sighash_single_anyonecanpay(self): self.txin.sighash=Sighash.SINGLE|Sighash.ANYONECANPAY - self.txin.pubkeys = [bfh('02d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b')] privkey = bfh('428a7aee9f0c2af0cd19af3cf1c78149951ea528726989b2e83e4778d2c3f890') tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) sig = tx.sign_txin(0,privkey) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 83d90d2b3..f61c8c3d1 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -729,7 +729,6 @@ async def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db): self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1.is_mine(wallet1.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -749,7 +748,6 @@ async def test_sending_between_p2wpkh_and_compressed_p2pkh(self, mock_save_db): self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -809,7 +807,6 @@ async def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_ self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -829,7 +826,6 @@ async def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_ self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -908,7 +904,6 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -937,7 +932,6 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2a.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2a.is_mine(wallet2a.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -987,7 +981,6 @@ async def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0]))) @@ -1007,7 +1000,6 @@ async def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type) tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0]))) diff --git a/electrum/transaction.py b/electrum/transaction.py index 436a46e43..46805873a 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -741,7 +741,6 @@ def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: return '' assert isinstance(txin, PartialTxInput) - _type = txin.script_type if not txin.is_segwit(): return construct_witness([]) @@ -801,7 +800,7 @@ def get_preimage_script(cls, txin: 'PartialTxInput') -> str: if script := sc.scriptcode_for_sighash: return script.hex() raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}") - raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}') + raise UnknownTxinType(f'cannot construct preimage_script') def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: inputs = self.inputs() @@ -1188,9 +1187,6 @@ def __init__(self, *args, **kwargs): self._unknown = {} # type: Dict[bytes, bytes] self.script_descriptor = None # type: Optional[Descriptor] - self.script_type = 'unknown' - self.num_sig = 0 # type: int # num req sigs for multisig - self.pubkeys = [] # type: List[bytes] # note: order matters self._trusted_value_sats = None # type: Optional[int] self._trusted_address = None # type: Optional[str] self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown @@ -1223,6 +1219,12 @@ def witness_utxo(self, value: Optional[TxOutput]): self._witness_utxo = value self.validate_data() + @property + def pubkeys(self) -> Set[bytes]: + if desc := self.script_descriptor: + return desc.get_all_pubkeys() + return set() + def to_json(self): d = super().to_json() d.update({ @@ -1402,22 +1404,6 @@ def scriptpubkey(self) -> Optional[bytes]: return self.witness_utxo.scriptpubkey return None - def set_script_type(self) -> None: - if self.scriptpubkey is None: - return - type = get_script_type_from_output_script(self.scriptpubkey) - inner_type = None - if type is not None: - if type == 'p2sh': - inner_type = get_script_type_from_output_script(self.redeem_script) - elif type == 'p2wsh': - inner_type = get_script_type_from_output_script(self.witness_script) - if inner_type is not None: - type = inner_type + '-' + type - if type in ('p2pkh', 'p2wpkh-p2sh', 'p2wpkh'): - self.script_type = type - return - def is_complete(self) -> bool: if self.script_sig is not None and self.witness is not None: return True @@ -1434,6 +1420,11 @@ def is_complete(self) -> bool: return True return False + def get_satisfaction_progress(self) -> Tuple[int, int]: + if desc := self.script_descriptor: + return desc.get_satisfaction_progress(sigdata=self.part_sigs) + return 0, 0 + def finalize(self) -> None: def clear_fields_when_finalized(): # BIP-174: "All other data except the UTXO and unknown fields in the @@ -1547,12 +1538,15 @@ def __init__(self, *args, **kwargs): self._unknown = {} # type: Dict[bytes, bytes] self.script_descriptor = None # type: Optional[Descriptor] - self.script_type = 'unknown' - self.num_sig = 0 # num req sigs for multisig - self.pubkeys = [] # type: List[bytes] # note: order matters self.is_mine = False # type: bool # whether the wallet considers the output to be ismine self.is_change = False # type: bool # whether the wallet considers the output to be change + @property + def pubkeys(self) -> Set[bytes]: + if desc := self.script_descriptor: + return desc.get_all_pubkeys() + return set() + def to_json(self): d = super().to_json() d.update({ @@ -1947,15 +1941,12 @@ def is_complete(self) -> bool: return all([txin.is_complete() for txin in self.inputs()]) def signature_count(self) -> Tuple[int, int]: - s = 0 # "num Sigs we have" - r = 0 # "Required" + nhave, nreq = 0, 0 for txin in self.inputs(): - if txin.is_coinbase_input(): - continue - signatures = list(txin.part_sigs.values()) - s += len(signatures) - r += txin.num_sig - return s, r + a, b = txin.get_satisfaction_progress() + nhave += a + nreq += b + return nhave, nreq def serialize(self) -> str: """Returns PSBT as base64 text, or raw hex of network tx (if complete).""" @@ -2104,14 +2095,6 @@ def remove_signatures(self): assert not self.is_complete() self.invalidate_ser_cache() - def update_txin_script_type(self): - """Determine the script_type of each input by analyzing the scripts. - It updates all tx-Inputs, NOT only the wallet owned, if the - scriptpubkey is present. - """ - for txin in self.inputs(): - if txin.script_type in ('unknown', 'address'): - txin.set_script_type() def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: if len(xfp) != 4: diff --git a/electrum/wallet.py b/electrum/wallet.py index 21bdda164..fc62cd3d3 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -126,13 +126,6 @@ async def append_single_utxo(item): txin.utxo = prev_tx txin.block_height = int(item['height']) txin.script_descriptor = script_descriptor - # TODO rm as much of below (.num_sig / .pubkeys) as possible - # TODO need unit tests for other scripts (only have p2pk atm) - txin.script_type = txin_type - txin.pubkeys = [bfh(pubkey)] - txin.num_sig = 1 - if txin_type == 'p2wpkh-p2sh': - txin.redeem_script = bfh(bitcoin.p2wpkh_nested_script(pubkey)) inputs.append(txin) u = await network.listunspent_for_scripthash(scripthash) @@ -2151,10 +2144,6 @@ def dscancel( tx_new.add_info_from_wallet(self) return tx_new - @abstractmethod - def _add_input_sig_info(self, txin: PartialTxInput, address: str, *, only_der_suffix: bool) -> None: - pass - def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput], address: str, *, only_der_suffix: bool) -> None: pass # implemented by subclasses @@ -2208,22 +2197,19 @@ def add_input_info( self.lnworker.swap_manager.add_txin_info(txin) return txin.script_descriptor = self._get_script_descriptor_for_address(address) - # set script_type first, as later checks might rely on it: # TODO rm most of below in favour of osd - txin.script_type = self.get_txin_type(address) - txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1 - if txin.redeem_script is None: + if txin.redeem_script is None: # FIXME should be set in transaction.py instead, based on the script desc try: redeem_script_hex = self.get_redeem_script(address) txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None except UnknownTxinType: pass - if txin.witness_script is None: + if txin.witness_script is None: # FIXME should be set in transaction.py instead, based on the script desc try: witness_script_hex = self.get_witness_script(address) txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None except UnknownTxinType: pass - self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix) + self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height def _get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]: @@ -2306,19 +2292,16 @@ def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = Fal if not is_mine: return txout.script_descriptor = self._get_script_descriptor_for_address(address) - txout.script_type = self.get_txin_type(address) txout.is_mine = True txout.is_change = self.is_change(address) - if isinstance(self, Multisig_Wallet): - txout.num_sig = self.m self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) - if txout.redeem_script is None: + if txout.redeem_script is None: # FIXME should be set in transaction.py instead, based on the script desc try: redeem_script_hex = self.get_redeem_script(address) txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None except UnknownTxinType: pass - if txout.witness_script is None: + if txout.witness_script is None: # FIXME should be set in transaction.py instead, based on the script desc try: witness_script_hex = self.get_witness_script(address) txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None @@ -3189,20 +3172,6 @@ def check_address_for_corruption(self, addr): if addr != bitcoin.pubkey_to_address(txin_type, pubkey): raise InternalAddressCorruption() - def _add_input_sig_info(self, txin, address, *, only_der_suffix): - if not self.is_mine(address): - return - if txin.script_type in ('unknown', 'address'): - return - elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): - pubkey = self.get_public_key(address) - if not pubkey: - return - txin.pubkeys = [bfh(pubkey)] - else: - raise Exception(f'Unexpected script type: {txin.script_type}. ' - f'Imported wallets are not implemented to handle this.') - def pubkeys_to_address(self, pubkeys): pubkey = pubkeys[0] # FIXME This is slow. @@ -3330,14 +3299,10 @@ def get_public_keys_with_deriv_info(self, address: str): return {k.derive_pubkey(*der_suffix): (k, der_suffix) for k in self.get_keystores()} - def _add_input_sig_info(self, txin, address, *, only_der_suffix): - self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) - def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix): if not self.is_mine(address): return pubkey_deriv_info = self.get_public_keys_with_deriv_info(address) - txinout.pubkeys = sorted([pk for pk in list(pubkey_deriv_info)]) for pubkey in pubkey_deriv_info: ks, der_suffix = pubkey_deriv_info[pubkey] fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, From 36986a988175ae990a5af6c3ed98c7daa23a0717 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 13:55:16 +0000 Subject: [PATCH 0262/1143] transaction.py: set txio.{witness,redeem}|script based on descriptor --- electrum/transaction.py | 30 ++++++++++++++++++++++++++++-- electrum/wallet.py | 24 ------------------------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 46805873a..3e1ff536b 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1186,7 +1186,7 @@ def __init__(self, *args, **kwargs): self.witness_script = None # type: Optional[bytes] self._unknown = {} # type: Dict[bytes, bytes] - self.script_descriptor = None # type: Optional[Descriptor] + self._script_descriptor = None # type: Optional[Descriptor] self._trusted_value_sats = None # type: Optional[int] self._trusted_address = None # type: Optional[str] self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown @@ -1225,6 +1225,19 @@ def pubkeys(self) -> Set[bytes]: return desc.get_all_pubkeys() return set() + @property + def script_descriptor(self): + return self._script_descriptor + + @script_descriptor.setter + def script_descriptor(self, desc: Optional[Descriptor]): + self._script_descriptor = desc + if desc: + if self.redeem_script is None: + self.redeem_script = desc.expand().redeem_script + if self.witness_script is None: + self.witness_script = desc.expand().witness_script + def to_json(self): d = super().to_json() d.update({ @@ -1537,7 +1550,7 @@ def __init__(self, *args, **kwargs): self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self._unknown = {} # type: Dict[bytes, bytes] - self.script_descriptor = None # type: Optional[Descriptor] + self._script_descriptor = None # type: Optional[Descriptor] self.is_mine = False # type: bool # whether the wallet considers the output to be ismine self.is_change = False # type: bool # whether the wallet considers the output to be change @@ -1547,6 +1560,19 @@ def pubkeys(self) -> Set[bytes]: return desc.get_all_pubkeys() return set() + @property + def script_descriptor(self): + return self._script_descriptor + + @script_descriptor.setter + def script_descriptor(self, desc: Optional[Descriptor]): + self._script_descriptor = desc + if desc: + if self.redeem_script is None: + self.redeem_script = desc.expand().redeem_script + if self.witness_script is None: + self.witness_script = desc.expand().witness_script + def to_json(self): d = super().to_json() d.update({ diff --git a/electrum/wallet.py b/electrum/wallet.py index fc62cd3d3..6d0270fef 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2197,18 +2197,6 @@ def add_input_info( self.lnworker.swap_manager.add_txin_info(txin) return txin.script_descriptor = self._get_script_descriptor_for_address(address) - if txin.redeem_script is None: # FIXME should be set in transaction.py instead, based on the script desc - try: - redeem_script_hex = self.get_redeem_script(address) - txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None - except UnknownTxinType: - pass - if txin.witness_script is None: # FIXME should be set in transaction.py instead, based on the script desc - try: - witness_script_hex = self.get_witness_script(address) - txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None - except UnknownTxinType: - pass self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height @@ -2295,18 +2283,6 @@ def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = Fal txout.is_mine = True txout.is_change = self.is_change(address) self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) - if txout.redeem_script is None: # FIXME should be set in transaction.py instead, based on the script desc - try: - redeem_script_hex = self.get_redeem_script(address) - txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None - except UnknownTxinType: - pass - if txout.witness_script is None: # FIXME should be set in transaction.py instead, based on the script desc - try: - witness_script_hex = self.get_witness_script(address) - txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None - except UnknownTxinType: - pass def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: if self.is_watching_only(): From 93b9591f2567779d19fc7408e8d86c70a06766f0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 14:00:26 +0000 Subject: [PATCH 0263/1143] tests: add test_descriptor.py from bitcoin-core/HWI --- electrum/tests/test_descriptor.py | 218 ++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 electrum/tests/test_descriptor.py diff --git a/electrum/tests/test_descriptor.py b/electrum/tests/test_descriptor.py new file mode 100644 index 000000000..3c4f0850c --- /dev/null +++ b/electrum/tests/test_descriptor.py @@ -0,0 +1,218 @@ +# Copyright (c) 2018-2023 The HWI developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# +# originally from https://github.com/bitcoin-core/HWI/blob/f5a9b29c00e483cc99a1b8f4f5ef75413a092869/test/test_descriptor.py + +from binascii import unhexlify + +from electrum.descriptor import ( + parse_descriptor, + MultisigDescriptor, + SHDescriptor, + TRDescriptor, + PKHDescriptor, + WPKHDescriptor, + WSHDescriptor, +) + +from . import ElectrumTestCase, as_testnet + + +class TestDescriptor(ElectrumTestCase): + + @as_testnet + def test_parse_descriptor_with_origin(self): + d = "wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + @as_testnet + def test_parse_multisig_descriptor_with_origin(self): + d = "wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WSHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor)) + self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0") + + self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) + + d = "sh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, SHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor)) + self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0") + + self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("a91495ee6326805b1586bb821fc3c0eeab2c68441b4187")) + self.assertEqual(e.redeem_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) + self.assertEqual(e.witness_script, None) + + d = "sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, SHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptors[0], WSHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptors[0].subdescriptors[0], MultisigDescriptor)) + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].deriv_path, "/0/0") + + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002") + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("a914779ae0f6958e98b997cc177f9b554289905fbb5587")) + self.assertEqual(e.redeem_script, unhexlify("002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59")) + self.assertEqual(e.witness_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) + + @as_testnet + def test_parse_descriptor_without_origin(self): + d = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin, None) + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + @as_testnet + def test_parse_descriptor_with_origin_fingerprint_only(self): + d = "wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(len(desc.pubkeys[0].origin.path), 0) + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + def test_parse_descriptor_with_key_at_end_with_origin(self): + d = "wpkh([00000001/84h/1h/0h/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h/0/0") + self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + d = "pkh([00000001/84h/1h/0h/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, PKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h/0/0") + self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand() + self.assertEqual(e.output_script, unhexlify("76a914d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa88ac")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + def test_parse_descriptor_with_key_at_end_without_origin(self): + d = "wpkh(02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin, None) + self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") + self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.to_string_no_checksum(), d) + + def test_parse_empty_descriptor(self): + self.assertRaises(ValueError, parse_descriptor, "") + + @as_testnet + def test_parse_descriptor_replace_h(self): + d = "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertIsNotNone(desc) + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + + def test_checksums(self): + with self.subTest(msg="Valid checksum"): + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmsckna")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))")) + self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))")) + with self.subTest(msg="Empty Checksum"): + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#") + with self.subTest(msg="Too long Checksum"): + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwjq") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmscknaq") + with self.subTest(msg="Too Short Checksum"): + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kw") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmsckn") + with self.subTest(msg="Error in Payload"): + self.assertRaises(ValueError, parse_descriptor, "sh(multi(3,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(3,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5") + with self.subTest(msg="Error in Checksum"): + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kej") + self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09y5") + + @as_testnet + def test_tr_descriptor(self): + d = "tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, TRDescriptor)) + self.assertEqual(len(desc.pubkeys), 1) + self.assertEqual(len(desc.subdescriptors), 0) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.to_string_no_checksum(), d) + + d = "tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),{{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)},pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)}})" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, TRDescriptor)) + self.assertEqual(len(desc.subdescriptors), 4) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.depths, [1, 3, 3, 2]) + self.assertEqual(desc.to_string_no_checksum(), d) From 144aac4523451a9af3ec0d41a8f5553bb0fc72d7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 26 Feb 2023 14:05:50 +0000 Subject: [PATCH 0264/1143] descriptors: add more sanity checks, and unit tests --- electrum/descriptor.py | 81 +++++++++++++----------- electrum/tests/test_descriptor.py | 102 ++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 38 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index be33bd3fc..3d122c396 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -9,21 +9,12 @@ # See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md # # TODO allow xprv +# TODO hardened derivation # TODO allow WIF privkeys # TODO impl ADDR descriptors # TODO impl RAW descriptors -# TODO disable descs we cannot solve: TRDescriptor -# TODO add checks to validate nestings -# https://github.com/bitcoin/bitcoin/blob/94070029fb6b783833973f9fe08a3a871994492f/doc/descriptors.md#reference -# e.g. sh is top-level only, wsh is top-level or directly inside sh -# -# TODO tests -# - port https://github.com/bitcoin-core/HWI/blob/master/test/test_descriptor.py -# - ranged descriptors (that have a "*") -# -# TODO solver? integrate with transaction.py... -# Transaction.input_script/get_preimage_script/serialize_witness +import enum from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo from . import bitcoin @@ -188,8 +179,13 @@ def __init__( self.origin = origin self.pubkey = pubkey self.deriv_path = deriv_path - # TODO check that deriv_path only has a single "*" (and that it is in the last pos. but can end with e.g. "*h") - + if deriv_path: + wildcard_count = deriv_path.count("*") + if wildcard_count > 1: + raise ValueError("only one wildcard(*) is allowed in a descriptor") + if wildcard_count == 1: + if deriv_path[-1] != "*": + raise ValueError("wildcard in descriptor only allowed in last position") # Make ExtendedKey from pubkey if it isn't hex self.extkey = None try: @@ -240,6 +236,7 @@ def to_string(self) -> str: def get_pubkey_bytes(self, *, pos: Optional[int] = None) -> bytes: if self.is_range() and pos is None: raise ValueError("pos must be set for ranged descriptor") + # note: if not ranged, we ignore pos. if self.extkey is not None: compressed = True # bip32 implies compressed pubkeys if self.deriv_path is None: @@ -298,11 +295,21 @@ def is_range(self) -> bool: return True return False + def has_uncompressed_pubkey(self) -> bool: + if self.is_range(): # bip32 implies compressed + return False + return b"\x04" == self.get_pubkey_bytes()[:1] + class Descriptor(object): r""" An abstract class for Descriptors themselves. Descriptors can contain multiple :class:`PubkeyProvider`\ s and multiple ``Descriptor`` as subdescriptors. + + Note: a significant portion of input validation logic is in parse_descriptor(), + maybe these checks should be moved to (or also done in) this class? + For example, sh() must be top-level, or segwit mandates compressed pubkeys, + or bare-multisig cannot have >3 pubkeys. """ def __init__( self, @@ -823,7 +830,7 @@ def _get_expr(s: str) -> Tuple[str, str]: break return s[0:i], s[i:] -def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]: +def parse_pubkey(expr: str, *, ctx: '_ParseDescriptorContext') -> Tuple['PubkeyProvider', str]: """ Parses an individual pubkey expression from a string that may contain more than one pubkey expression. @@ -836,7 +843,11 @@ def parse_pubkey(expr: str) -> Tuple['PubkeyProvider', str]: if comma_idx != -1: end = comma_idx next_expr = expr[end + 1:] - return PubkeyProvider.parse(expr[:end]), next_expr + pubkey_provider = PubkeyProvider.parse(expr[:end]) + permit_uncompressed = ctx in (_ParseDescriptorContext.TOP, _ParseDescriptorContext.P2SH) + if not permit_uncompressed and pubkey_provider.has_uncompressed_pubkey(): + raise ValueError("uncompressed pubkeys are not allowed") + return pubkey_provider, next_expr class _ParseDescriptorContext(Enum): @@ -847,20 +858,14 @@ class _ParseDescriptorContext(Enum): Some expressions aren't allowed at certain levels, this helps us track those. """ - TOP = 1 - """The top level, not within any descriptor""" - - P2SH = 2 - """Within a ``sh()`` descriptor""" - - P2WSH = 3 - """Within a ``wsh()`` descriptor""" - - P2TR = 4 - """Within a ``tr()`` descriptor""" + TOP = enum.auto() # The top level, not within any descriptor + P2SH = enum.auto() # Within an sh() descriptor + P2WPKH = enum.auto() # Within wpkh() descriptor + P2WSH = enum.auto() # Within a wsh() descriptor + P2TR = enum.auto() # Within a tr() descriptor -def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor': +def _parse_descriptor(desc: str, *, ctx: '_ParseDescriptorContext') -> 'Descriptor': """ :meta private: @@ -874,14 +879,14 @@ def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor' """ func, expr = _get_func_expr(desc) if func == "pk": - pubkey, expr = parse_pubkey(expr) + pubkey, expr = parse_pubkey(expr, ctx=ctx) if expr: raise ValueError("more than one pubkey in pk descriptor") return PKDescriptor(pubkey) if func == "pkh": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH): raise ValueError("Can only have pkh at top level, in sh(), or in wsh()") - pubkey, expr = parse_pubkey(expr) + pubkey, expr = parse_pubkey(expr, ctx=ctx) if expr: raise ValueError("More than one pubkey in pkh descriptor") return PKHDescriptor(pubkey) @@ -894,10 +899,10 @@ def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor' expr = expr[comma_idx + 1:] pubkeys = [] while expr: - pubkey, expr = parse_pubkey(expr) + pubkey, expr = parse_pubkey(expr, ctx=ctx) pubkeys.append(pubkey) - if len(pubkeys) == 0 or len(pubkeys) > 16: - raise ValueError("Cannot have {} keys in a multisig; must have between 1 and 16 keys, inclusive".format(len(pubkeys))) + if len(pubkeys) == 0 or len(pubkeys) > 15: + raise ValueError("Cannot have {} keys in a multisig; must have between 1 and 15 keys, inclusive".format(len(pubkeys))) elif thresh < 1: raise ValueError("Multisig threshold cannot be {}, must be at least 1".format(thresh)) elif thresh > len(pubkeys): @@ -908,24 +913,24 @@ def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor' if func == "wpkh": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH): raise ValueError("Can only have wpkh() at top level or inside sh()") - pubkey, expr = parse_pubkey(expr) + pubkey, expr = parse_pubkey(expr, ctx=_ParseDescriptorContext.P2WPKH) if expr: raise ValueError("More than one pubkey in pkh descriptor") return WPKHDescriptor(pubkey) if func == "sh": if ctx != _ParseDescriptorContext.TOP: raise ValueError("Can only have sh() at top level") - subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2SH) + subdesc = _parse_descriptor(expr, ctx=_ParseDescriptorContext.P2SH) return SHDescriptor(subdesc) if func == "wsh": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH): raise ValueError("Can only have wsh() at top level or inside sh()") - subdesc = _parse_descriptor(expr, _ParseDescriptorContext.P2WSH) + subdesc = _parse_descriptor(expr, ctx=_ParseDescriptorContext.P2WSH) return WSHDescriptor(subdesc) if func == "tr": if ctx != _ParseDescriptorContext.TOP: raise ValueError("Can only have tr at top level") - internal_key, expr = parse_pubkey(expr) + internal_key, expr = parse_pubkey(expr, ctx=ctx) subscripts = [] depths = [] if expr: @@ -945,7 +950,7 @@ def _parse_descriptor(desc: str, ctx: '_ParseDescriptorContext') -> 'Descriptor' raise ValueError(f"tr() supports at most {MAX_TAPROOT_NODES} nesting levels") # TODO xxxx fixed upstream bug here # Process script expression sarg, expr = _get_expr(expr) - subscripts.append(_parse_descriptor(sarg, _ParseDescriptorContext.P2TR)) + subscripts.append(_parse_descriptor(sarg, ctx=_ParseDescriptorContext.P2TR)) depths.append(len(branches)) # Process closing braces while len(branches) > 0 and branches[-1]: @@ -982,7 +987,7 @@ def parse_descriptor(desc: str) -> 'Descriptor': computed = DescriptorChecksum(desc) if computed != checksum: raise ValueError("The checksum does not match; Got {}, expected {}".format(checksum, computed)) - return _parse_descriptor(desc, _ParseDescriptorContext.TOP) + return _parse_descriptor(desc, ctx=_ParseDescriptorContext.TOP) ##### diff --git a/electrum/tests/test_descriptor.py b/electrum/tests/test_descriptor.py index 3c4f0850c..6c1b80104 100644 --- a/electrum/tests/test_descriptor.py +++ b/electrum/tests/test_descriptor.py @@ -1,10 +1,12 @@ # Copyright (c) 2018-2023 The HWI developers +# Copyright (c) 2023 The Electrum developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # originally from https://github.com/bitcoin-core/HWI/blob/f5a9b29c00e483cc99a1b8f4f5ef75413a092869/test/test_descriptor.py from binascii import unhexlify +import unittest from electrum.descriptor import ( parse_descriptor, @@ -15,6 +17,8 @@ WPKHDescriptor, WSHDescriptor, ) +from electrum import ecc +from electrum.util import bfh from . import ElectrumTestCase, as_testnet @@ -35,6 +39,7 @@ def test_parse_descriptor_with_origin(self): self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) self.assertEqual(e.redeem_script, None) self.assertEqual(e.witness_script, None) + self.assertEqual(e.address(), "tb1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th690vysp") @as_testnet def test_parse_multisig_descriptor_with_origin(self): @@ -216,3 +221,100 @@ def test_tr_descriptor(self): self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") self.assertEqual(desc.depths, [1, 3, 3, 2]) self.assertEqual(desc.to_string_no_checksum(), d) + + @as_testnet + def test_parse_descriptor_with_range(self): + d = "wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WPKHDescriptor)) + self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.pubkeys[0].deriv_path, "/0/*") + self.assertEqual(desc.to_string_no_checksum(), d) + with self.assertRaises(ValueError): # "pos" arg needed due to "*" + e = desc.expand() + e = desc.expand(pos=7) + self.assertEqual(e.output_script, unhexlify("0014c5f80de08f6ae8dd720bf4e4948ba498c96256a1")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, None) + + with self.assertRaises(ValueError): # wildcard only allowed in last position + parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*/0)") + with self.assertRaises(ValueError): # only one wildcard(*) is allowed + parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*/*)") + + @as_testnet + def test_parse_multisig_descriptor_with_range(self): + d = "wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/*))" + desc = parse_descriptor(d) + self.assertTrue(isinstance(desc, WSHDescriptor)) + self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor)) + self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/*") + + self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty") + self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, "/0/*") + self.assertEqual(desc.to_string_no_checksum(), d) + e = desc.expand(pos=7) + self.assertEqual(e.output_script, unhexlify("0020453cdf90aef0997947bc0605481f81dd2978ecd2d04ac36fb57397a82341682d")) + self.assertEqual(e.redeem_script, None) + self.assertEqual(e.witness_script, unhexlify("5221034e703dfcd64f23ad5d6156ee3b9dd7566137626c663bb521bf710957599723342102c35627535d26de98ae749b7a7849df99cbe53af795005437ca647c8af9a006af52ae")) + + @as_testnet + def test_multisig_descriptor_with_mixed_range(self): + d = "sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))" + desc = parse_descriptor(d) + e = desc.expand(pos=7) + self.assertEqual(e.output_script, bfh("a914644ece12bab2f84ad6de96ec18de51e6168c028987")) + self.assertEqual(e.redeem_script, bfh("0020824ce4ffab74a8d09c2f77ed447fb040ea5dfbed06f8e3b3327127a18634f6a7")) + self.assertEqual(e.witness_script, bfh("5221034e703dfcd64f23ad5d6156ee3b9dd7566137626c663bb521bf7109575997233421033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae")) + self.assertEqual(e.address(), "2N2Pbxw3HNJ9jrUw8LCSfXyDWx9TKGRT2an") + + @as_testnet + def test_uncompressed_pubkey_in_segwit(self): + pubkey = ecc.ECPubkey(bfh("02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc")) + pubkey_comp_hex = pubkey.get_public_key_hex(compressed=True) + pubkey_uncomp_hex = pubkey.get_public_key_hex(compressed=False) + self.assertEqual(pubkey_comp_hex, "02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc") + self.assertEqual(pubkey_uncomp_hex, "04a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc3ccfc29410b8f23c15d88413a6b88c8cd44b016a7f1dd91a8d64c3107c6bce1a") + # pkh + desc = parse_descriptor(f"pkh({pubkey_comp_hex})") + self.assertEqual(desc.expand().output_script, bfh("76a9140297bde2689a3c79ffe050583b62f86f2d9dae5488ac")) + desc = parse_descriptor(f"pkh({pubkey_uncomp_hex})") + self.assertEqual(desc.expand().output_script, bfh("76a914e1f4a76b122f0288b013404cd52a9d1de0ced3c488ac")) + # wpkh + desc = parse_descriptor(f"wpkh({pubkey_comp_hex})") + self.assertEqual(desc.expand().output_script, bfh("00140297bde2689a3c79ffe050583b62f86f2d9dae54")) + with self.assertRaises(ValueError): # only compressed public keys can be used in segwit scripts + desc = parse_descriptor(f"wpkh({pubkey_uncomp_hex})") + # sh(wsh(multi())) + desc = parse_descriptor(f"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,{pubkey_comp_hex})))") + self.assertEqual(desc.expand(pos=2).output_script, bfh("a9148f162cce29ad81e63ed45cd09aff83418316eab687")) + with self.assertRaises(ValueError): # only compressed public keys can be used in segwit scripts + desc = parse_descriptor(f"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,{pubkey_uncomp_hex})))") + + @as_testnet + def test_parse_descriptor_context(self): + desc = parse_descriptor("sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))") + self.assertTrue(isinstance(desc, SHDescriptor)) + with self.assertRaises(ValueError): # Can only have sh() at top level + desc = parse_descriptor("wsh(sh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))") + with self.assertRaises(ValueError): # Can only have wsh() at top level or inside sh() + desc = parse_descriptor("wsh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))") + + desc = parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)") + self.assertTrue(isinstance(desc, WPKHDescriptor)) + with self.assertRaises(ValueError): # Can only have wpkh() at top level or inside sh() + desc = parse_descriptor("wsh(wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0))") + + def test_parse_descriptor_ypub_zpub_forbidden(self): + desc = parse_descriptor("wpkh([535e473f/0h]xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4/0/*)") + with self.assertRaises(ValueError): # only standard xpub/xprv allowed + desc = parse_descriptor("wpkh([535e473f/0h]ypub6TLJVy4mZfqBJhoQBTgDR1TzM7s91WbVnMhZj31swV6xxPiwCqeGYrBn2dNHbDrP86qqxbM6FNTX3VjhRjNoXYyBAR5G3o75D3r2djmhZwM/0/*)") + with self.assertRaises(ValueError): # only standard xpub/xprv allowed + desc = parse_descriptor("wpkh([535e473f/0h]zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr/0/*)") From a536658eef6670f57a1359e9a7e9548175c134ab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 27 Feb 2023 17:07:03 +0000 Subject: [PATCH 0265/1143] descriptor.py: fix get_satisfaction_progress --- electrum/descriptor.py | 2 +- electrum/tests/test_wallet_vertical.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 3d122c396..f11adef65 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -395,7 +395,7 @@ def get_satisfaction_progress( assert not self.is_range() nhave, nreq = 0, 0 for desc in self.subdescriptors: - a, b = desc.get_satisfaction_progress() + a, b = desc.get_satisfaction_progress(sigdata=sigdata) nhave += a nreq += b return nhave, nreq diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index f61c8c3d1..9c4aa8e53 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -891,7 +891,10 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db # wallet1 -> wallet2 outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)] - tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) + tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False) + self.assertEqual((0, 2), tx.signature_count()) + wallet1a.sign_transaction(tx, password=None) + self.assertEqual((1, 2), tx.signature_count()) txid = tx.txid() partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100eb01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c1130022020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a1053b77ddb010000800100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd51043067d63010000800100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98010b2e35a7d0100008001000000000000000000", @@ -902,6 +905,7 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db wallet1b.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) + self.assertEqual((2, 2), tx.signature_count()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) tx_copy = tx_from_any(tx.serialize()) @@ -920,6 +924,7 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) + self.assertEqual((1, 2), tx.signature_count()) txid = tx.txid() partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000", @@ -930,6 +935,7 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db wallet2b.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) + self.assertEqual((2, 2), tx.signature_count()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) tx_copy = tx_from_any(tx.serialize()) @@ -2718,6 +2724,7 @@ async def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db): tx.version = 1 self.assertFalse(tx.is_complete()) + self.assertEqual((0, 1), tx.signature_count()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) partial_tx = tx.serialize_as_bytes().hex() @@ -2731,6 +2738,7 @@ async def test_sending_offline_old_electrum_seed_online_mpk(self, mock_save_db): # sign tx tx = wallet_offline.sign_transaction(tx_copy, password=None) self.assertTrue(tx.is_complete()) + self.assertEqual((1, 1), tx.signature_count()) self.assertFalse(tx.is_segwit()) self.assertEqual('01000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b020000008a47304402206bed3e02af8a38f6ba2fa3bf5908cb8c643aa62e78e8de6d9af2e19dec55fafc0220039cc1d81d4e5e0292bbc54ea92b8ec4ec016d4828eedc8975a66952cedf13a1014104e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f5fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600', str(tx)) @@ -2863,6 +2871,7 @@ async def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): tx.version = 1 self.assertFalse(tx.is_complete()) + self.assertEqual((0, 1), tx.signature_count()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) @@ -2887,6 +2896,7 @@ async def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): # sign tx tx = wallet_offline.sign_transaction(tx_copy, password=None) self.assertTrue(tx.is_complete()) + self.assertEqual((1, 1), tx.signature_count()) self.assertTrue(tx.is_segwit()) self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid()) self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) @@ -3241,6 +3251,7 @@ async def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db): # sign tx - first tx = wallet_offline1.sign_transaction(tx_copy, password=None) self.assertFalse(tx.is_complete()) + self.assertEqual((1, 2), tx.signature_count()) partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000", partial_tx) @@ -3249,6 +3260,7 @@ async def test_sending_offline_hd_multisig_online_addr_p2sh(self, mock_save_db): # sign tx - second tx = wallet_offline2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) + self.assertEqual((2, 2), tx.signature_count()) tx = tx_from_any(tx.serialize()) self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fc004730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101473044022052980154bdf2e43d6bd8775316cc220ef5ae13b4b9574a7a904a691ee3c5efd3022069b3eddf904cc645bd8fc8b2aaa7aaf7eb5bbfb7bbbd3b6e6cd89b37dfb2856c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400', From e7849bce941acdfa7250a435c2dcfe08332a37bf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Feb 2023 16:08:11 +0000 Subject: [PATCH 0266/1143] descriptor.py: clean-up and test PubkeyProvider.get_full_derivation_* --- electrum/bip32.py | 8 ++++++-- electrum/descriptor.py | 28 ++++++++++++---------------- electrum/tests/test_descriptor.py | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 74ee1aeb8..225427f78 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -334,14 +334,18 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: # makes concatenating paths easier continue prime = 0 - if x.endswith("'") or x.endswith("h"): + if x.endswith("'") or x.endswith("h"): # note: some implementations also accept "H", "p", "P" x = x[:-1] prime = BIP32_PRIME if x.startswith('-'): if prime: raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways") prime = BIP32_PRIME - child_index = abs(int(x)) | prime + try: + x_int = int(x) + except ValueError as e: + raise ValueError(f"failed to parse bip32 path: {(str(e))}") from None + child_index = abs(x_int) | prime if child_index > UINT32_MAX: raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}") path.append(child_index) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index f11adef65..909c9de2c 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -16,7 +16,7 @@ import enum -from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo +from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo, BIP32_PRIME from . import bitcoin from .bitcoin import construct_script, opcodes, construct_witness from . import constants @@ -254,35 +254,31 @@ def get_pubkey_bytes(self, *, pos: Optional[int] = None) -> bytes: assert not self.is_range() return unhexlify(self.pubkey) - def get_full_derivation_path(self, pos: int) -> str: + def get_full_derivation_path(self, *, pos: Optional[int] = None) -> str: """ Returns the full derivation path at the given position, including the origin """ - path = self.origin.get_derivation_path() if self.origin is not None else "m/" + if self.is_range() and pos is None: + raise ValueError("pos must be set for ranged descriptor") + path = self.origin.get_derivation_path() if self.origin is not None else "m" path += self.deriv_path if self.deriv_path is not None else "" if path[-1] == "*": path = path[:-1] + str(pos) return path - def get_full_derivation_int_list(self, pos: int) -> List[int]: + def get_full_derivation_int_list(self, *, pos: Optional[int] = None) -> List[int]: """ Returns the full derivation path as an integer list at the given position. Includes the origin and master key fingerprint as an int """ + if self.is_range() and pos is None: + raise ValueError("pos must be set for ranged descriptor") path: List[int] = self.origin.get_full_int_list() if self.origin is not None else [] if self.deriv_path is not None: - der_split = self.deriv_path.split("/") - for p in der_split: - if not p: - continue - if p == "*": - i = pos - elif p[-1] in "'phHP": - assert len(p) >= 2 - i = int(p[:-1]) | 0x80000000 - else: - i = int(p) - path.append(i) + der_suffix = self.deriv_path + assert (wc_count := der_suffix.count("*")) <= 1, wc_count + der_suffix = der_suffix.replace("*", str(pos)) + path.extend(convert_bip32_path_to_list_of_uint32(der_suffix)) return path def __lt__(self, other: 'PubkeyProvider') -> bool: diff --git a/electrum/tests/test_descriptor.py b/electrum/tests/test_descriptor.py index 6c1b80104..096f48f74 100644 --- a/electrum/tests/test_descriptor.py +++ b/electrum/tests/test_descriptor.py @@ -34,6 +34,8 @@ def test_parse_descriptor_with_origin(self): self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/84h/1h/0h/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0]) self.assertEqual(desc.to_string_no_checksum(), d) e = desc.expand() self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) @@ -51,6 +53,8 @@ def test_parse_multisig_descriptor_with_origin(self): self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h") self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_path(), "m/48h/0h/0h/2h/0/0") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483696, 2147483648, 2147483648, 2147483650, 0, 0]) self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002") self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") @@ -109,6 +113,8 @@ def test_parse_descriptor_without_origin(self): self.assertEqual(desc.pubkeys[0].origin, None) self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [0, 0]) self.assertEqual(desc.to_string_no_checksum(), d) e = desc.expand() self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) @@ -138,6 +144,8 @@ def test_parse_descriptor_with_key_at_end_with_origin(self): self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h/0/0") self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/84h/1h/0h/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0]) self.assertEqual(desc.to_string_no_checksum(), d) e = desc.expand() self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) @@ -164,6 +172,8 @@ def test_parse_descriptor_with_key_at_end_without_origin(self): self.assertEqual(desc.pubkeys[0].origin, None) self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), []) self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_empty_descriptor(self): @@ -176,6 +186,13 @@ def test_parse_descriptor_replace_h(self): self.assertIsNotNone(desc) self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + @as_testnet + def test_parse_descriptor_unknown_notation_for_hardened_derivation(self): + with self.assertRaises(ValueError): + desc = parse_descriptor("wpkh([00000001/84x/1x/0x]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)") + with self.assertRaises(ValueError): + desc = parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0x)") + def test_checksums(self): with self.subTest(msg="Valid checksum"): self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj")) From 9127c12fa3e88f6ba33fc778f05381f4c3cc9c9d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Feb 2023 17:05:19 +0000 Subject: [PATCH 0267/1143] descriptor.py: do more validation in PubkeyProvider, and add tests --- electrum/descriptor.py | 4 ++++ electrum/tests/test_descriptor.py | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 909c9de2c..824fe75fe 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -186,6 +186,8 @@ def __init__( if wildcard_count == 1: if deriv_path[-1] != "*": raise ValueError("wildcard in descriptor only allowed in last position") + if deriv_path[0] != "/": + raise ValueError(f"deriv_path suffix must start with a '/'. got {deriv_path!r}") # Make ExtendedKey from pubkey if it isn't hex self.extkey = None try: @@ -194,6 +196,8 @@ def __init__( except Exception: # Not hex, maybe xpub (but don't allow ypub/zpub) self.extkey = BIP32Node.from_xkey(pubkey, allow_custom_headers=False) + if deriv_path and self.extkey is None: + raise ValueError("deriv_path suffix present for simple pubkey") @classmethod def parse(cls, s: str) -> 'PubkeyProvider': diff --git a/electrum/tests/test_descriptor.py b/electrum/tests/test_descriptor.py index 096f48f74..a27367592 100644 --- a/electrum/tests/test_descriptor.py +++ b/electrum/tests/test_descriptor.py @@ -16,6 +16,7 @@ PKHDescriptor, WPKHDescriptor, WSHDescriptor, + PubkeyProvider, ) from electrum import ecc from electrum.util import bfh @@ -335,3 +336,27 @@ def test_parse_descriptor_ypub_zpub_forbidden(self): desc = parse_descriptor("wpkh([535e473f/0h]ypub6TLJVy4mZfqBJhoQBTgDR1TzM7s91WbVnMhZj31swV6xxPiwCqeGYrBn2dNHbDrP86qqxbM6FNTX3VjhRjNoXYyBAR5G3o75D3r2djmhZwM/0/*)") with self.assertRaises(ValueError): # only standard xpub/xprv allowed desc = parse_descriptor("wpkh([535e473f/0h]zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr/0/*)") + + def test_pubkey_provider_deriv_path(self): + xpub = "xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4" + # valid: + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="/1/7") + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="/1/*") + # invalid: + with self.assertRaises(ValueError): + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="1") + with self.assertRaises(ValueError): + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="1/7") + with self.assertRaises(ValueError): + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="m/1/7") + with self.assertRaises(ValueError): + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="*/7") + with self.assertRaises(ValueError): + pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="*/*") + + pubkey_hex = "02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc" + # valid: + pp = PubkeyProvider(origin=None, pubkey=pubkey_hex, deriv_path=None) + # invalid: + with self.assertRaises(ValueError): + pp = PubkeyProvider(origin=None, pubkey=pubkey_hex, deriv_path="/1/7") From 847b4fa4c42bb86eb201bd5bfd78afe1f0ff8c49 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Feb 2023 18:38:46 +0000 Subject: [PATCH 0268/1143] descriptor.py: sortedmulti to sort .pubkeys already in __init__ --- electrum/descriptor.py | 23 +++++++++++++++++------ electrum/tests/test_descriptor.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 824fe75fe..bd3b0a4ec 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -278,13 +278,17 @@ def get_full_derivation_int_list(self, *, pos: Optional[int] = None) -> List[int if self.is_range() and pos is None: raise ValueError("pos must be set for ranged descriptor") path: List[int] = self.origin.get_full_int_list() if self.origin is not None else [] - if self.deriv_path is not None: - der_suffix = self.deriv_path - assert (wc_count := der_suffix.count("*")) <= 1, wc_count - der_suffix = der_suffix.replace("*", str(pos)) - path.extend(convert_bip32_path_to_list_of_uint32(der_suffix)) + path.extend(self.get_der_suffix_int_list(pos=pos)) return path + def get_der_suffix_int_list(self, *, pos: Optional[int] = None) -> List[int]: + if not self.deriv_path: + return [] + der_suffix = self.deriv_path + assert (wc_count := der_suffix.count("*")) <= 1, wc_count + der_suffix = der_suffix.replace("*", str(pos)) + return convert_bip32_path_to_list_of_uint32(der_suffix) + def __lt__(self, other: 'PubkeyProvider') -> bool: return self.pubkey < other.pubkey @@ -606,7 +610,14 @@ def __init__( self.thresh = thresh self.is_sorted = is_sorted if self.is_sorted: - self.pubkeys.sort() + if not self.is_range(): + # sort xpubs using the order of pubkeys + der_pks = [p.get_pubkey_bytes() for p in self.pubkeys] + self.pubkeys = [x[1] for x in sorted(zip(der_pks, self.pubkeys))] + else: + # not possible to sort according to final order in expanded scripts, + # but for easier visual comparison, we do a lexicographical sort + self.pubkeys.sort() def to_string_no_checksum(self) -> str: return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) diff --git a/electrum/tests/test_descriptor.py b/electrum/tests/test_descriptor.py index a27367592..936d36683 100644 --- a/electrum/tests/test_descriptor.py +++ b/electrum/tests/test_descriptor.py @@ -337,6 +337,34 @@ def test_parse_descriptor_ypub_zpub_forbidden(self): with self.assertRaises(ValueError): # only standard xpub/xprv allowed desc = parse_descriptor("wpkh([535e473f/0h]zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr/0/*)") + @as_testnet + def test_sortedmulti_ranged_pubkey_order(self): + xpub1 = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B" + xpub2 = "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty" + # if ranged, we sort lexicographically + desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/*,[00000002/48h/0h/0h/2h]{xpub2}/0/*)))") + self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000002/48h/0h/0h/2h]{xpub2}/0/*,[00000001/48h/0h/0h/2h]{xpub1}/0/*)))") + self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + # if unsorted "multi", don't touch order + desc = parse_descriptor(f"sh(wsh(multi(2,[00000002/48h/0h/0h/2h]{xpub2}/0/*,[00000001/48h/0h/0h/2h]{xpub1}/0/*)))") + self.assertEqual([xpub2, xpub1], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + + @as_testnet + def test_sortedmulti_unranged_pubkey_order(self): + xpub1 = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B" + xpub2 = "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty" + # if not ranged, we sort according to final derived pubkey order + desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/0,[00000002/48h/0h/0h/2h]{xpub2}/0/0)))") + self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/1,[00000002/48h/0h/0h/2h]{xpub2}/0/1)))") + self.assertEqual([xpub2, xpub1], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/4,[00000002/48h/0h/0h/2h]{xpub2}/0/4)))") + self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + # if unsorted "multi", don't touch order + desc = parse_descriptor(f"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]{xpub1}/0/1,[00000002/48h/0h/0h/2h]{xpub2}/0/1)))") + self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys]) + def test_pubkey_provider_deriv_path(self): xpub = "xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4" # valid: From 31f457c242e985efdac986b6b30867bcc8a6b314 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Feb 2023 18:42:22 +0000 Subject: [PATCH 0269/1143] wallet.get_script_desc_for_addr: use xpub instead of derived pubkey also put key origin info into descriptor, if available --- electrum/keystore.py | 43 +++++++++++++++++++++++++- electrum/tests/test_wallet_vertical.py | 24 +++++++++++++- electrum/wallet.py | 36 ++++++++++++--------- 3 files changed, 86 insertions(+), 17 deletions(-) diff --git a/electrum/keystore.py b/electrum/keystore.py index e0419fb4f..9ccf2ab30 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -36,7 +36,9 @@ from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, - convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info) + convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info, + KeyOriginInfo) +from .descriptor import PubkeyProvider from .ecc import string_to_number from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160, @@ -179,6 +181,10 @@ def get_pubkey_derivation(self, pubkey: bytes, """ pass + @abstractmethod + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + pass + def find_my_pubkey_in_txinout( self, txinout: Union['PartialTxInput', 'PartialTxOutput'], *, only_der_suffix: bool = False @@ -302,6 +308,15 @@ def get_pubkey_derivation(self, pubkey, txin, *, only_der_suffix=True): return pubkey.hex() return None + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + if sequence in self.keypairs: + return PubkeyProvider( + origin=None, + pubkey=sequence, + deriv_path=None, + ) + return None + def update_password(self, old_password, new_password): self.check_password(old_password) if new_password == '': @@ -403,6 +418,9 @@ def get_fp_and_derivation_to_be_used_in_partial_tx( """ pass + def get_key_origin_info(self) -> Optional[KeyOriginInfo]: + return None + @abstractmethod def derive_pubkey(self, for_change: int, n: int) -> bytes: """Returns pubkey at given path. @@ -532,6 +550,22 @@ def get_xpub_to_be_used_in_partial_tx(self, *, only_der_suffix: bool) -> str: ) return bip32node.to_xpub() + def get_key_origin_info(self) -> Optional[KeyOriginInfo]: + fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx( + der_suffix=[], only_der_suffix=False) + origin = KeyOriginInfo(fingerprint=fp_bytes, path=der_full) + return origin + + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + strpath = convert_bip32_intpath_to_strpath(sequence) + strpath = strpath[1:] # cut leading "m" + bip32node = self.get_bip32_node_for_xpub() + return PubkeyProvider( + origin=self.get_key_origin_info(), + pubkey=bip32node._replace(xtype="standard").to_xkey(), + deriv_path=strpath, + ) + def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node): assert self.xpub # try to derive ourselves from what we were given @@ -802,6 +836,13 @@ def get_fp_and_derivation_to_be_used_in_partial_tx( der_full = der_prefix_ints + list(der_suffix) return fingerprint_bytes, der_full + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + return PubkeyProvider( + origin=None, + pubkey=self.derive_pubkey(*sequence).hex(), + deriv_path=None, + ) + def update_password(self, old_password, new_password): self.check_password(old_password) if new_password == '': diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 9c4aa8e53..dac6b64d7 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -821,7 +821,11 @@ async def test_sending_between_p2sh_2of3_and_uncompressed_p2pkh(self, mock_save_ # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] - tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) + tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False) + self.assertEqual( + "pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) + wallet2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) @@ -893,6 +897,9 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)] tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False) self.assertEqual((0, 2), tx.signature_count()) + self.assertEqual( + "wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) wallet1a.sign_transaction(tx, password=None) self.assertEqual((1, 2), tx.signature_count()) txid = tx.txid() @@ -925,6 +932,9 @@ async def test_sending_between_p2wsh_2of3_and_p2wsh_p2sh_2of2(self, mock_save_db outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) self.assertEqual((1, 2), tx.signature_count()) + self.assertEqual( + "sh(wsh(sortedmulti(2,[d1dbcc21]tpubDDsv4RpsGViZeEVwivuj3aaKhFQSv1kYsz64mwRoHkqBfw8qBSYEmc8TtyVGotJb44V3pviGzefP9m9hidRg9dPPaDWL2yoRpMW3hdje3Rk/0/0,[17cea914]tpubDCZU2kACPGACYDvAXvZUXQ7cE7msFfCtpah5QCuaz8iarKMLTgR4c2u8RGKdFhbb3YJxzmktDd1rCtF58ksyVgFw28pchY55uwkDiXjY9hU/0/0)))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) txid = tx.txid() partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000", @@ -2613,6 +2623,9 @@ async def test_export_psbt_with_xpubs__multisig(self, mock_save_db): tx.version = 2 tx.locktime = 2378363 self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) + self.assertEqual( + "wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[015148ee]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), @@ -2640,6 +2653,9 @@ async def test_export_psbt_with_xpubs__multisig(self, mock_save_db): tx.version = 2 tx.locktime = 2378363 self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) + self.assertEqual( + "wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[30cf1be5/48h/1h/0h/2h]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), @@ -3036,6 +3052,9 @@ async def test_sending_offline_wif_online_addr_p2wpkh_p2sh(self, mock_save_db): # sign tx tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertEqual( + "sh(wpkh(03845818239fe468a9e7c7ae1a3d3653a8333f89ff316a771a3acf6854b4d8c6db))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) @@ -3114,6 +3133,9 @@ async def test_sending_offline_xprv_online_addr_p2pkh(self, mock_save_db): # co # sign tx tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertEqual( + "pkh([233d2ae4]tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF/0/1)", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) diff --git a/electrum/wallet.py b/electrum/wallet.py index 6d0270fef..c1e0198e2 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -686,14 +686,14 @@ def get_address_path_str(self, address: str) -> Optional[str]: pass def get_redeem_script(self, address: str) -> Optional[str]: - desc = self._get_script_descriptor_for_address(address) + desc = self.get_script_descriptor_for_address(address) if desc is None: return None redeem_script = desc.expand().redeem_script if redeem_script: return redeem_script.hex() def get_witness_script(self, address: str) -> Optional[str]: - desc = self._get_script_descriptor_for_address(address) + desc = self.get_script_descriptor_for_address(address) if desc is None: return None witness_script = desc.expand().witness_script if witness_script: @@ -2196,32 +2196,38 @@ def add_input_info( if self.lnworker: self.lnworker.swap_manager.add_txin_info(txin) return - txin.script_descriptor = self._get_script_descriptor_for_address(address) + txin.script_descriptor = self.get_script_descriptor_for_address(address) self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height - def _get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]: + def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]: if not self.is_mine(address): return None script_type = self.get_txin_type(address) if script_type in ('address', 'unknown'): return None - if script_type in ('p2pk', 'p2pkh', 'p2wpkh-p2sh', 'p2wpkh'): - pubkey = self.get_public_keys(address)[0] - return descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type) + addr_index = self.get_address_index(address) + if addr_index is None: + return None + pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()] + if not pubkeys: + return None + if script_type == 'p2pk': + return descriptor.PKDescriptor(pubkey=pubkeys[0]) + elif script_type == 'p2pkh': + return descriptor.PKHDescriptor(pubkey=pubkeys[0]) + elif script_type == 'p2wpkh': + return descriptor.WPKHDescriptor(pubkey=pubkeys[0]) + elif script_type == 'p2wpkh-p2sh': + wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0]) + return descriptor.SHDescriptor(subdescriptor=wpkh) elif script_type == 'p2sh': - pubkeys = self.get_public_keys(address) - pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) return descriptor.SHDescriptor(subdescriptor=multi) elif script_type == 'p2wsh': - pubkeys = self.get_public_keys(address) - pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) return descriptor.WSHDescriptor(subdescriptor=multi) elif script_type == 'p2wsh-p2sh': - pubkeys = self.get_public_keys(address) - pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) wsh = descriptor.WSHDescriptor(subdescriptor=multi) return descriptor.SHDescriptor(subdescriptor=wsh) @@ -2279,7 +2285,7 @@ def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = Fal is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address) if not is_mine: return - txout.script_descriptor = self._get_script_descriptor_for_address(address) + txout.script_descriptor = self.get_script_descriptor_for_address(address) txout.is_mine = True txout.is_change = self.is_change(address) self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) @@ -3390,7 +3396,7 @@ def get_public_key(self, address): return pubkeys[0] def load_keystore(self): - self.keystore = load_keystore(self.db, 'keystore') + self.keystore = load_keystore(self.db, 'keystore') # type: KeyStoreWithMPK try: xtype = bip32.xpub_type(self.keystore.xpub) except: From e457bb50e9707d3c57d513a7c04a485cedf28957 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Feb 2023 18:47:17 +0000 Subject: [PATCH 0270/1143] trezor: TrezorPlugin._make_multisig to use MultisigDescriptor This fixes a regression where the plugin was assuming ordering for txin.pubkeys (which is now a set). (previously txin.pubkeys was a list ordered according to the final sort order of keys inside the bitcoin script) --- electrum/plugins/hw_wallet/plugin.py | 19 -------------- electrum/plugins/keepkey/keepkey.py | 39 +++++++++++++--------------- electrum/plugins/safe_t/safe_t.py | 39 +++++++++++++--------------- electrum/plugins/trezor/trezor.py | 39 +++++++++++++--------------- 4 files changed, 54 insertions(+), 82 deletions(-) diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py index 5886ca57c..4c347da80 100644 --- a/electrum/plugins/hw_wallet/plugin.py +++ b/electrum/plugins/hw_wallet/plugin.py @@ -354,25 +354,6 @@ def validate_op_return_output(output: TxOutput, *, max_size: int = None) -> None raise UserFacingException(_("Amount for OP_RETURN output must be zero.")) -def get_xpubs_and_der_suffixes_from_txinout(tx: PartialTransaction, - txinout: Union[PartialTxInput, PartialTxOutput]) \ - -> List[Tuple[str, List[int]]]: - xfp_to_xpub_map = {xfp: bip32node for bip32node, (xfp, path) - in tx.xpubs.items()} # type: Dict[bytes, BIP32Node] - xfps = [txinout.bip32_paths[pubkey][0] for pubkey in txinout.pubkeys] - try: - xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps] - except KeyError as e: - raise Exception(f"Partial transaction is missing global xpub for " - f"fingerprint ({str(e)}) in input/output") from e - xpubs_and_deriv_suffixes = [] - for bip32node, pubkey in zip(xpubs, txinout.pubkeys): - xfp, path = txinout.bip32_paths[pubkey] - der_suffix = list(path)[bip32node.depth:] - xpubs_and_deriv_suffixes.append((bip32node.to_xpub(), der_suffix)) - return xpubs_and_deriv_suffixes - - def only_hook_if_libraries_available(func): # note: this decorator must wrap @hook, not the other way around, # as 'hook' uses the name of the function it wraps diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 0e3c36019..573819ec6 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -1,10 +1,11 @@ from binascii import hexlify, unhexlify import traceback import sys -from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence from electrum.util import bfh, UserCancelled, UserFacingException from electrum.bip32 import BIP32Node +from electrum import descriptor from electrum import constants from electrum.i18n import _ from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, Sighash @@ -13,8 +14,7 @@ from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, - get_xpubs_and_der_suffixes_from_txinout) +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data if TYPE_CHECKING: import usb1 @@ -271,7 +271,7 @@ def _initialize_device(self, settings, method, device_id, wizard, handler): client.load_device_by_xprv(item, pin, passphrase_protection, label, language) - def _make_node_path(self, xpub, address_n): + def _make_node_path(self, xpub: str, address_n: Sequence[int]): bip32node = BIP32Node.from_xkey(xpub) node = self.types.HDNodeType( depth=bip32node.depth, @@ -351,14 +351,9 @@ def show_address(self, wallet, address, keystore=None): script_type = self.get_keepkey_input_script_type(wallet.txin_type) # prepare multisig, if available: - xpubs = wallet.get_master_public_keys() - if len(xpubs) > 1: - pubkeys = wallet.get_public_keys(address) - # sort xpubs using the order of pubkeys - sorted_pairs = sorted(zip(pubkeys, xpubs)) - multisig = self._make_multisig( - wallet.m, - [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs]) + desc = wallet.get_script_descriptor_for_address(address) + if multi := desc.get_simple_multisig(): + multisig = self._make_multisig(multi) else: multisig = None @@ -378,8 +373,7 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeySto assert keystore assert (desc := txin.script_descriptor) if multi := desc.get_simple_multisig(): - xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) - multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi) else: multisig = None script_type = self.get_keepkey_input_script_type(desc.to_legacy_electrum_script_type()) @@ -407,14 +401,18 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeySto return inputs - def _make_multisig(self, m, xpubs): - if len(xpubs) == 1: - return None - pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] + def _make_multisig(self, desc: descriptor.MultisigDescriptor): + pubkeys = [] + for pubkey_provider in desc.pubkeys: + assert not pubkey_provider.is_range() + assert pubkey_provider.extkey is not None + xpub = pubkey_provider.pubkey + der_suffix = pubkey_provider.get_der_suffix_int_list() + pubkeys.append(self._make_node_path(xpub, der_suffix)) return self.types.MultisigRedeemScriptType( pubkeys=pubkeys, signatures=[b''] * len(pubkeys), - m=m) + m=desc.thresh) def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'): @@ -422,8 +420,7 @@ def create_output_by_derivation(): assert (desc := txout.script_descriptor) script_type = self.get_keepkey_output_script_type(desc.to_legacy_electrum_script_type()) if multi := desc.get_simple_multisig(): - xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) - multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi) else: multisig = None my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 863fa796d..750171fc0 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -1,10 +1,11 @@ from binascii import hexlify, unhexlify import traceback import sys -from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException from electrum.bip32 import BIP32Node +from electrum import descriptor from electrum import constants from electrum.i18n import _ from electrum.plugin import Device, runs_in_hwd_thread @@ -13,8 +14,7 @@ from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, - get_xpubs_and_der_suffixes_from_txinout) +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data if TYPE_CHECKING: from .client import SafeTClient @@ -241,7 +241,7 @@ def _initialize_device(self, settings, method, device_id, wizard, handler): client.load_device_by_xprv(item, pin, passphrase_protection, label, language) - def _make_node_path(self, xpub, address_n): + def _make_node_path(self, xpub: str, address_n: Sequence[int]): bip32node = BIP32Node.from_xkey(xpub) node = self.types.HDNodeType( depth=bip32node.depth, @@ -321,14 +321,9 @@ def show_address(self, wallet, address, keystore=None): script_type = self.get_safet_input_script_type(wallet.txin_type) # prepare multisig, if available: - xpubs = wallet.get_master_public_keys() - if len(xpubs) > 1: - pubkeys = wallet.get_public_keys(address) - # sort xpubs using the order of pubkeys - sorted_pairs = sorted(zip(pubkeys, xpubs)) - multisig = self._make_multisig( - wallet.m, - [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs]) + desc = wallet.get_script_descriptor_for_address(address) + if multi := desc.get_simple_multisig(): + multisig = self._make_multisig(multi) else: multisig = None @@ -348,8 +343,7 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' assert keystore assert (desc := txin.script_descriptor) if multi := desc.get_simple_multisig(): - xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) - multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi) else: multisig = None script_type = self.get_safet_input_script_type(desc.to_legacy_electrum_script_type()) @@ -377,14 +371,18 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' return inputs - def _make_multisig(self, m, xpubs): - if len(xpubs) == 1: - return None - pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] + def _make_multisig(self, desc: descriptor.MultisigDescriptor): + pubkeys = [] + for pubkey_provider in desc.pubkeys: + assert not pubkey_provider.is_range() + assert pubkey_provider.extkey is not None + xpub = pubkey_provider.pubkey + der_suffix = pubkey_provider.get_der_suffix_int_list() + pubkeys.append(self._make_node_path(xpub, der_suffix)) return self.types.MultisigRedeemScriptType( pubkeys=pubkeys, signatures=[b''] * len(pubkeys), - m=m) + m=desc.thresh) def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'): @@ -392,8 +390,7 @@ def create_output_by_derivation(): assert (desc := txout.script_descriptor) script_type = self.get_safet_output_script_type(desc.to_legacy_electrum_script_type()) if multi := desc.get_simple_multisig(): - xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) - multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi) else: multisig = None my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 503eed6bb..390a812fa 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -1,9 +1,10 @@ import traceback import sys -from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path +from electrum import descriptor from electrum import constants from electrum.i18n import _ from electrum.plugin import Device, runs_in_hwd_thread @@ -14,8 +15,7 @@ from ..hw_wallet import HW_PluginBase from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, - LibraryFoundButUnusable, OutdatedHwFirmwareException, - get_xpubs_and_der_suffixes_from_txinout) + LibraryFoundButUnusable, OutdatedHwFirmwareException) _logger = get_logger(__name__) @@ -284,7 +284,7 @@ def _initialize_device(self, settings: TrezorInitSettings, method, device_id, wi else: raise RuntimeError("Unsupported recovery method") - def _make_node_path(self, xpub, address_n): + def _make_node_path(self, xpub: str, address_n: Sequence[int]): bip32node = BIP32Node.from_xkey(xpub) node = HDNodeType( depth=bip32node.depth, @@ -386,14 +386,9 @@ def show_address(self, wallet, address, keystore=None): script_type = self.get_trezor_input_script_type(wallet.txin_type) # prepare multisig, if available: - xpubs = wallet.get_master_public_keys() - if len(xpubs) > 1: - pubkeys = wallet.get_public_keys(address) - # sort xpubs using the order of pubkeys - sorted_pairs = sorted(zip(pubkeys, xpubs)) - multisig = self._make_multisig( - wallet.m, - [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs]) + desc = wallet.get_script_descriptor_for_address(address) + if multi := desc.get_simple_multisig(): + multisig = self._make_multisig(multi) else: multisig = None @@ -419,8 +414,7 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore assert keystore assert (desc := txin.script_descriptor) if multi := desc.get_simple_multisig(): - xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) - txinputtype.multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + txinputtype.multisig = self._make_multisig(multi) txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type()) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) if full_path: @@ -434,14 +428,18 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore return inputs - def _make_multisig(self, m, xpubs): - if len(xpubs) == 1: - return None - pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] + def _make_multisig(self, desc: descriptor.MultisigDescriptor): + pubkeys = [] + for pubkey_provider in desc.pubkeys: + assert not pubkey_provider.is_range() + assert pubkey_provider.extkey is not None + xpub = pubkey_provider.pubkey + der_suffix = pubkey_provider.get_der_suffix_int_list() + pubkeys.append(self._make_node_path(xpub, der_suffix)) return MultisigRedeemScriptType( pubkeys=pubkeys, signatures=[b''] * len(pubkeys), - m=m) + m=desc.thresh) def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'): @@ -449,8 +447,7 @@ def create_output_by_derivation(): assert (desc := txout.script_descriptor) script_type = self.get_trezor_output_script_type(desc.to_legacy_electrum_script_type()) if multi := desc.get_simple_multisig(): - xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) - multisig = self._make_multisig(multi.thresh, xpubs_and_deriv_suffixes) + multisig = self._make_multisig(multi) else: multisig = None my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) From a80bef8421bf9bc66e5d6de5279dc6b8d4665618 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 1 Mar 2023 18:46:34 +0000 Subject: [PATCH 0271/1143] follow-up descriptor.py: small clean-up --- electrum/bitcoin.py | 2 +- electrum/descriptor.py | 5 ++--- electrum/wallet.py | 4 ---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index d5ca43727..aa73623f9 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -442,7 +442,7 @@ def redeem_script_to_address(txin_type: str, scriptcode: str, *, net=None) -> st raise NotImplementedError(txin_type) -def script_to_address(script: str, *, net=None) -> str: +def script_to_address(script: str, *, net=None) -> Optional[str]: from .transaction import get_address_from_output_script return get_address_from_output_script(bfh(script), net=net) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index bd3b0a4ec..4ad9fd71a 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -54,7 +54,7 @@ class ExpandedScripts: def __init__( self, *, - output_script: Optional[bytes] = None, + output_script: bytes, # "scriptPubKey" redeem_script: Optional[bytes] = None, witness_script: Optional[bytes] = None, scriptcode_for_sighash: Optional[bytes] = None @@ -75,8 +75,7 @@ def scriptcode_for_sighash(self, value: Optional[bytes]): self._scriptcode_for_sighash = value def address(self, *, net=None) -> Optional[str]: - if spk := self.output_script: - return bitcoin.script_to_address(spk.hex(), net=net) + return bitcoin.script_to_address(self.output_script.hex(), net=net) class ScriptSolutionInner(NamedTuple): diff --git a/electrum/wallet.py b/electrum/wallet.py index c1e0198e2..1f43e9270 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -107,8 +107,6 @@ async def _append_utxos_to_inputs( inputs: List[PartialTxInput], network: 'Network', script_descriptor: 'descriptor.Descriptor', - pubkey: str, - txin_type: str, imax: int, ) -> None: script = script_descriptor.expand().output_script.hex() @@ -145,8 +143,6 @@ async def find_utxos_for_privkey(txin_type, privkey, compressed): inputs=inputs, network=network, script_descriptor=desc, - pubkey=pubkey, - txin_type=txin_type, imax=imax) keypairs[pubkey] = privkey, compressed From 2ed71579c39ab7ace9fc108a38394deb769ac98c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 2 Mar 2023 11:29:41 +0100 Subject: [PATCH 0272/1143] privacy analysis: detect address reuse add tx position to get_addr_io --- electrum/address_synchronizer.py | 6 ++--- electrum/gui/qt/utxo_dialog.py | 39 +++++++++++++++++++++----------- electrum/wallet.py | 22 ++++++++++++++---- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 76a0255e2..a435c127d 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -778,13 +778,13 @@ def get_addr_io(self, address): sent = {} for tx_hash, height in h: hh, pos = self.get_txpos(tx_hash) + assert hh == height d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): received[tx_hash + ':%d'%n] = (height, pos, v, is_cb) - for tx_hash, height in h: l = self.db.get_txi_addr(tx_hash, address) for txi, v in l: - sent[txi] = tx_hash, height + sent[txi] = tx_hash, height, pos return received, sent def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: @@ -799,7 +799,7 @@ def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: utxo.block_height = tx_height utxo.block_txpos = tx_pos if prevout_str in sent: - txid, height = sent[prevout_str] + txid, height, pos = sent[prevout_str] utxo.spent_txid = txid utxo.spent_height = height else: diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 30dc80b1c..af8a7a618 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -28,13 +28,14 @@ from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QTextCharFormat, QFont -from PyQt5.QtWidgets import QVBoxLayout, QLabel, QTextBrowser +from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QLabel, QTextBrowser from electrum.i18n import _ from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel from .history_list import HistoryList, HistoryModel from .qrtextedit import ShowQRTextEdit +from .transaction_dialog import TxOutputColoring if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -66,6 +67,10 @@ def __init__(self, window: 'ElectrumWindow', utxo): self.parents_list.setMinimumWidth(900) self.parents_list.setMinimumHeight(400) self.parents_list.setLineWrapMode(QTextBrowser.NoWrap) + self.txo_color_parent = TxOutputColoring( + legend=_("Direct parent"), color=ColorScheme.BLUE, tooltip=_("Direct parent")) + self.txo_color_uncle = TxOutputColoring( + legend=_("Address reuse"), color=ColorScheme.RED, tooltip=_("Address reuse")) cursor = self.parents_list.textCursor() ext = QTextCharFormat() @@ -81,7 +86,7 @@ def __init__(self, window: 'ElectrumWindow', utxo): ASCII_PIPE = '│' ASCII_SPACE = ' ' - def print_ascii_tree(_txid, prefix, is_last): + def print_ascii_tree(_txid, prefix, is_last, is_uncle): if _txid not in parents: return tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) @@ -91,7 +96,10 @@ def print_ascii_tree(_txid, prefix, is_last): label = '[duplicate]' c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH) cursor.insertText(prefix + c, ext) - lnk = QTextCharFormat() + if is_uncle: + lnk = QTextCharFormat(self.txo_color_uncle.text_char_format) + else: + lnk = QTextCharFormat(self.txo_color_parent.text_char_format) lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setAnchorHref(_txid) #lnk.setAnchorNames([a_name]) @@ -102,22 +110,27 @@ def print_ascii_tree(_txid, prefix, is_last): cursor.insertText(label, ext) cursor.insertBlock() next_prefix = '' if txid == _txid else prefix + (ASCII_SPACE if is_last else ASCII_PIPE) - parents_list = parents_copy.pop(_txid, []) - for i, p in enumerate(parents_list): - is_last = i == len(parents_list) - 1 - print_ascii_tree(p, next_prefix, is_last) + parents_list, uncle_list = parents_copy.pop(_txid, ([],[])) + for i, p in enumerate(parents_list + uncle_list): + is_last = (i == len(parents_list) + len(uncle_list)- 1) + is_uncle = (i > len(parents_list) - 1) + print_ascii_tree(p, next_prefix, is_last, is_uncle) + # recursively build the tree - print_ascii_tree(txid, '', False) + print_ascii_tree(txid, '', False, False) vbox = QVBoxLayout() vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(num_parents))) vbox.addWidget(self.parents_list) - msg = ' '.join([ - _("Note: This analysis only shows parent transactions, and does not take address reuse into consideration."), - _("If you reuse addresses, more links can be established between your transactions, that are not displayed here.") - ]) - vbox.addWidget(WWLabel(msg)) + legend_hbox = QHBoxLayout() + legend_hbox.setContentsMargins(0, 0, 0, 0) + legend_hbox.addStretch(2) + legend_hbox.addWidget(self.txo_color_parent.legend_label) + legend_hbox.addWidget(self.txo_color_uncle.legend_label) + vbox.addLayout(legend_hbox) + self.txo_color_parent.legend_label.setVisible(True) + self.txo_color_uncle.legend_label.setVisible(True) vbox.addLayout(Buttons(CloseButton(self))) self.setLayout(vbox) # set cursor to top diff --git a/electrum/wallet.py b/electrum/wallet.py index 0985fa94a..9b0681589 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -871,23 +871,37 @@ def get_tx_parents(self, txid) -> Dict: """ if not self.is_up_to_date(): return {} - if self._last_full_history is None: - self._last_full_history = self.get_full_history(None) - with self.lock, self.transaction_lock: + if self._last_full_history is None: + self._last_full_history = self.get_full_history(None) result = self._tx_parents_cache.get(txid, None) if result is not None: return result result = {} parents = [] + uncles = [] tx = self.adb.get_transaction(txid) assert tx, f"cannot find {txid} in db" for i, txin in enumerate(tx.inputs()): _txid = txin.prevout.txid.hex() parents.append(_txid) + # detect address reuse + addr = self.adb.get_txin_address(txin) + received, sent = self.adb.get_addr_io(addr) + if len(sent) > 1: + my_txid, my_height, my_pos = sent[txin.prevout.to_str()] + assert my_txid == txid + for k, v in sent.items(): + if k != txin.prevout.to_str(): + reuse_txid, reuse_height, reuse_pos = v + if (reuse_height, reuse_pos) < (my_height, my_pos): + uncle_txid, uncle_index = k.split(':') + uncles.append(uncle_txid) + + for _txid in parents + uncles: if _txid in self._last_full_history.keys(): result.update(self.get_tx_parents(_txid)) - result[txid] = parents + result[txid] = parents, uncles self._tx_parents_cache[txid] = result return result From 27ce9d88c35e5d146a4f75a22396b9fe0cf29968 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 4 Mar 2023 09:04:50 +0100 Subject: [PATCH 0273/1143] follow-up 2ed71579c39ab7ace9fc108a38394deb769ac98c: remove wrong assert --- electrum/address_synchronizer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index a435c127d..7bd06e45d 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -778,7 +778,6 @@ def get_addr_io(self, address): sent = {} for tx_hash, height in h: hh, pos = self.get_txpos(tx_hash) - assert hh == height d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): received[tx_hash + ':%d'%n] = (height, pos, v, is_cb) From 2f6d60c715e3c2637a5fe491d375a493ced43bba Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 26 Feb 2023 10:15:25 +0100 Subject: [PATCH 0274/1143] Move transaction related settings into transaction editor. That way, users can see the effects settings directly on their transaction. This changes the API of make_tx: - get_coins is called inside make_tx, so that inputs can be changed dynamically - make_tx takes an optional parameter: unconfirmed_only, passed to get_coins - ConfirmTxDialog detects if we can pay by disabling confirmed_only or lowering fee --- electrum/gui/qt/confirm_tx_dialog.py | 107 ++++++++++++++++++++++---- electrum/gui/qt/main_window.py | 9 +-- electrum/gui/qt/rbf_dialog.py | 6 +- electrum/gui/qt/send_tab.py | 31 ++++---- electrum/gui/qt/settings_dialog.py | 69 +---------------- electrum/gui/qt/transaction_dialog.py | 11 ++- electrum/wallet.py | 2 +- 7 files changed, 124 insertions(+), 111 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index b928cc70a..98be69853 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -374,12 +374,58 @@ def create_buttons_bar(self): def create_top_bar(self, text): self.pref_menu = QMenu() - self.m1 = self.pref_menu.addAction('Show inputs/outputs', self.toggle_io_visibility) - self.m1.setCheckable(True) - self.m2 = self.pref_menu.addAction('Edit fees', self.toggle_fee_details) - self.m2.setCheckable(True) - self.m3 = self.pref_menu.addAction('Edit Locktime', self.toggle_locktime) - self.m3.setCheckable(True) + self.pref_menu.setToolTipsVisible(True) + def add_pref_action(b, action, text, tooltip): + m = self.pref_menu.addAction(text, action) + m.setCheckable(True) + m.setChecked(b) + m.setToolTip(tooltip) + return m + add_pref_action( + self.config.get('show_tx_io', False), + self.toggle_io_visibility, + _('Show inputs and outputs'), '') + add_pref_action( + self.config.get('show_tx_fee_details', False), + self.toggle_fee_details, + _('Edit fees manually'), '') + add_pref_action( + self.config.get('show_tx_locktime', False), + self.toggle_locktime, + _('Edit Locktime'), '') + self.pref_menu.addSeparator() + add_pref_action( + self.wallet.use_change, + self.toggle_use_change, + _('Use change addresses'), + _('Using change addresses makes it more difficult for other people to track your transactions.')) + self.use_multi_change_menu = add_pref_action( + self.wallet.multiple_change, self.toggle_multiple_change, + _('Use multiple change addresses',), + '\n'.join([ + _('In some cases, use up to 3 change addresses in order to break ' + 'up large coin amounts and obfuscate the recipient address.'), + _('This may result in higher transactions fees.') + ])) + self.use_multi_change_menu.setEnabled(self.wallet.use_change) + add_pref_action( + self.config.get('batch_rbf', False), + self.toggle_batch_rbf, + _('Batch unconfirmed transactions'), + _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \ + _('This will save fees, but might have unwanted effects in terms of privacy')) + add_pref_action( + self.config.get('confirmed_only', False), + self.toggle_confirmed_only, + _('Spend only confirmed coins'), + _('Spend only confirmed inputs.')) + add_pref_action( + self.config.get('coin_chooser_output_rounding', True), + self.toggle_confirmed_only, + _('Enable output value rounding'), + _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' + \ + _('This might improve your privacy somewhat.') + '\n' + \ + _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')) self.pref_button = QToolButton() self.pref_button.setIcon(read_QIcon("preferences.png")) self.pref_button.setMenu(self.pref_menu) @@ -397,6 +443,27 @@ def resize_to_fit_content(self): self.resize(size) self.resize(size) + def toggle_use_change(self): + self.wallet.use_change = not self.wallet.use_change + self.wallet.db.put('use_change', self.wallet.use_change) + self.use_multi_change_menu.setEnabled(self.wallet.use_change) + self.trigger_update() + + def toggle_multiple_change(self): + self.wallet.multiple_change = not self.wallet.multiple_change + self.wallet.db.put('multiple_change', self.wallet.multiple_change) + self.trigger_update() + + def toggle_batch_rbf(self): + b = not self.config.get('batch_rbf', False) + self.config.set_key('batch_rbf', b) + self.trigger_update() + + def toggle_confirmed_only(self): + b = not self.config.get('confirmed_only', False) + self.config.set_key('confirmed_only', b) + self.trigger_update() + def toggle_io_visibility(self): b = not self.config.get('show_tx_io', False) self.config.set_key('show_tx_io', b) @@ -417,7 +484,6 @@ def toggle_locktime(self): def set_io_visible(self, b): self.io_widget.setVisible(b) - self.m1.setChecked(b) def set_fee_edit_visible(self, b): detailed = [self.feerounding_icon, self.feerate_e, self.fee_e] @@ -427,14 +493,12 @@ def set_fee_edit_visible(self, b): w.hide() for w in (detailed if b else basic): w.show() - self.m2.setChecked(b) def set_locktime_visible(self, b): for w in [ self.locktime_e, self.locktime_label]: w.setVisible(b) - self.m3.setChecked(b) def run(self): cancelled = not self.exec_() @@ -452,8 +516,19 @@ def on_preview(self): def _update_widgets(self): self._update_amount_label() if self.not_enough_funds: - self.error = self.main_window.send_tab.get_text_not_enough_funds_mentioning_frozen() + self.error = _('Not enough funds.') + confirmed_only = self.config.get('confirmed_only', False) + if confirmed_only and self.can_pay_assuming_zero_fees(confirmed_only=False): + self.error += ' ' + _('Change your settings to allow spending unconfirmed coins.') + elif self.can_pay_assuming_zero_fees(confirmed_only=confirmed_only): + self.error += ' ' + _('You need to set a lower fee.') + else: + self.error += '' + else: + self.error = '' if not self.tx: + if self.not_enough_funds: + self.io_widget.update(None) self.set_feerounding_visibility(False) else: self.check_tx_fee_warning() @@ -467,7 +542,6 @@ def _update_widgets(self): self._update_send_button() self._update_message() - def check_tx_fee_warning(self): # side effects: self.error, self.message fee = self.tx.get_fee() @@ -538,8 +612,9 @@ def _update_amount_label(self): def update_tx(self, *, fallback_to_zero_fee: bool = False): fee_estimator = self.get_fee_estimator() + confirmed_only = self.config.get('confirmed_only', False) try: - self.tx = self.make_tx(fee_estimator) + self.tx = self.make_tx(fee_estimator, confirmed_only=confirmed_only) self.not_enough_funds = False self.no_dynfee_estimates = False error = '' @@ -549,7 +624,7 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): self.tx = None if fallback_to_zero_fee: try: - self.tx = self.make_tx(0) + self.tx = self.make_tx(0, confirmed_only=confirmed_only) except BaseException: return else: @@ -558,7 +633,7 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): self.no_dynfee_estimates = True self.tx = None try: - self.tx = self.make_tx(0) + self.tx = self.make_tx(0, confirmed_only=confirmed_only) except NotEnoughFunds: self.not_enough_funds = True return @@ -570,10 +645,10 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): raise self.tx.set_rbf(True) - def have_enough_funds_assuming_zero_fees(self) -> bool: + def can_pay_assuming_zero_fees(self, confirmed_only) -> bool: # called in send_tab.py try: - tx = self.make_tx(0) + tx = self.make_tx(0, confirmed_only=confirmed_only) except NotEnoughFunds: return False else: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f71d09f82..9393f2f93 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1192,12 +1192,12 @@ def on_event_payment_failed(self, wallet, key, reason): else: self.show_error(_('Payment failed') + '\n\n' + reason) - def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]: + def get_coins(self, **kwargs) -> Sequence[PartialTxInput]: coins = self.get_manually_selected_coins() if coins is not None: return coins else: - return self.wallet.get_spendable_coins(None, nonlocal_only=nonlocal_only) + return self.wallet.get_spendable_coins(None, **kwargs) def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]: """Return a list of selected coins or None. @@ -1242,9 +1242,8 @@ def on_failure(exc_info): WaitingDialog(self, msg, task, on_success, on_failure) def mktx_for_open_channel(self, *, funding_sat, node_id): - coins = self.get_coins(nonlocal_only=True) - make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel( - coins=coins, + make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.lnworker.mktx_for_open_channel( + coins = self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), funding_sat=funding_sat, node_id=node_id, fee_est=fee_est) diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index d3ec31a89..f0de304a9 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -153,12 +153,12 @@ def __init__( txid=txid, title=_('Bump Fee')) - def rbf_func(self, fee_rate): + def rbf_func(self, fee_rate, *, confirmed_only=False): return self.wallet.bump_fee( tx=self.old_tx, txid=self.old_txid, new_fee_rate=fee_rate, - coins=self.main_window.get_coins(), + coins=self.main_window.get_coins(nonlocal_only=True, confirmed_only=confirmed_only), decrease_payment=self.is_decrease_payment()) @@ -183,5 +183,5 @@ def __init__( self.method_label.setVisible(False) self.method_combo.setVisible(False) - def rbf_func(self, fee_rate): + def rbf_func(self, fee_rate, *, confirmed_only=False): return self.wallet.dscancel(tx=self.old_tx, new_fee_rate=fee_rate) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 4145295b1..99dcbc015 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -179,12 +179,11 @@ def spend_max(self): outputs = self.payto_e.get_outputs(True) if not outputs: return - make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( + make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( coins=self.window.get_coins(), outputs=outputs, fee=fee_est, is_sweep=False) - try: try: tx = make_tx(None) @@ -216,32 +215,30 @@ def spend_max(self): QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg) def pay_onchain_dialog( - self, inputs: Sequence[PartialTxInput], + self, outputs: List[PartialTxOutput], *, + nonlocal_only=False, external_keypairs=None) -> None: # trustedcoin requires this if run_hook('abort_send', self): return is_sweep = bool(external_keypairs) - make_tx = lambda fee_est: self.wallet.make_unsigned_transaction( - coins=inputs, + # we call get_coins inside make_tx, so that inputs can be changed dynamically + make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( + coins=self.window.get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only), outputs=outputs, fee=fee_est, is_sweep=is_sweep) output_values = [x.value for x in outputs] - if any(parse_max_spend(outval) for outval in output_values): - output_value = '!' - else: - output_value = sum(output_values) + is_max = any(parse_max_spend(outval) for outval in output_values) + output_value = '!' if is_max else sum(output_values) conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value) if conf_dlg.not_enough_funds: - # Check if we had enough funds excluding fees, - # if so, still provide opportunity to set lower fees. - if not conf_dlg.have_enough_funds_assuming_zero_fees(): + confirmed_only = self.config.get('confirmed_only', False) + if not conf_dlg.can_pay_assuming_zero_fees(confirmed_only=False): text = self.get_text_not_enough_funds_mentioning_frozen() self.show_message(text) return - tx = conf_dlg.run() if tx is None: # user cancelled @@ -250,14 +247,12 @@ def pay_onchain_dialog( if is_preview: self.window.show_transaction(tx) return - self.save_pending_invoice() def sign_done(success): if success: self.window.broadcast_or_show(tx) else: raise - self.window.sign_tx( tx, callback=sign_done, @@ -564,13 +559,13 @@ def pay_multiple_invoices(self, invoices): outputs = [] for invoice in invoices: outputs += invoice.outputs - self.pay_onchain_dialog(self.window.get_coins(), outputs) + self.pay_onchain_dialog(outputs) def do_pay_invoice(self, invoice: 'Invoice'): if invoice.is_lightning(): self.pay_lightning_invoice(invoice) else: - self.pay_onchain_dialog(self.window.get_coins(), invoice.outputs) + self.pay_onchain_dialog(invoice.outputs) def read_outputs(self) -> List[PartialTxOutput]: if self.payment_request: @@ -695,7 +690,7 @@ def pay_lightning_invoice(self, invoice: Invoice): chan, swap_recv_amount_sat = can_pay_with_swap self.window.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) elif r == 3: - self.pay_onchain_dialog(coins, invoice.get_outputs()) + self.pay_onchain_dialog(invoice.get_outputs(), nonlocal_only=True) return assert lnworker is not None diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index e247e3bcb..1dad08386 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -119,15 +119,6 @@ def on_bip21_lightning(x): self.config.set_key('bip21_lightning', bool(x)) bip21_lightning_cb.stateChanged.connect(on_bip21_lightning) - batch_rbf_cb = QCheckBox(_('Batch unconfirmed transactions')) - batch_rbf_cb.setChecked(bool(self.config.get('batch_rbf', False))) - batch_rbf_cb.setToolTip( - _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \ - _('This will save fees.')) - def on_batch_rbf(x): - self.config.set_key('batch_rbf', bool(x)) - batch_rbf_cb.stateChanged.connect(on_batch_rbf) - # lightning help_recov = _(messages.MSG_RECOVERABLE_CHANNELS) recov_cb = QCheckBox(_("Create recoverable channels")) @@ -276,33 +267,6 @@ def on_set_filelogging(v): filelogging_cb.stateChanged.connect(on_set_filelogging) filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.')) - usechange_cb = QCheckBox(_('Use change addresses')) - usechange_cb.setChecked(self.wallet.use_change) - if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False) - def on_usechange(x): - usechange_result = x == Qt.Checked - if self.wallet.use_change != usechange_result: - self.wallet.use_change = usechange_result - self.wallet.db.put('use_change', self.wallet.use_change) - multiple_cb.setEnabled(self.wallet.use_change) - usechange_cb.stateChanged.connect(on_usechange) - usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.')) - - def on_multiple(x): - multiple = x == Qt.Checked - if self.wallet.multiple_change != multiple: - self.wallet.multiple_change = multiple - self.wallet.db.put('multiple_change', multiple) - multiple_change = self.wallet.multiple_change - multiple_cb = QCheckBox(_('Use multiple change addresses')) - multiple_cb.setEnabled(self.wallet.use_change) - multiple_cb.setToolTip('\n'.join([ - _('In some cases, use up to 3 change addresses in order to break ' - 'up large coin amounts and obfuscate the recipient address.'), - _('This may result in higher transactions fees.') - ])) - multiple_cb.setChecked(multiple_change) - multiple_cb.stateChanged.connect(on_multiple) def fmt_docs(key, klass): lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")] @@ -323,25 +287,6 @@ def on_chooser(x): self.config.set_key('coin_chooser', chooser_name) chooser_combo.currentIndexChanged.connect(on_chooser) - def on_unconf(x): - self.config.set_key('confirmed_only', bool(x)) - conf_only = bool(self.config.get('confirmed_only', False)) - unconf_cb = QCheckBox(_('Spend only confirmed coins')) - unconf_cb.setToolTip(_('Spend only confirmed inputs.')) - unconf_cb.setChecked(conf_only) - unconf_cb.stateChanged.connect(on_unconf) - - def on_outrounding(x): - self.config.set_key('coin_chooser_output_rounding', bool(x)) - enable_outrounding = bool(self.config.get('coin_chooser_output_rounding', True)) - outrounding_cb = QCheckBox(_('Enable output value rounding')) - outrounding_cb.setToolTip( - _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' + - _('This might improve your privacy somewhat.') + '\n' + - _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')) - outrounding_cb.setChecked(enable_outrounding) - outrounding_cb.stateChanged.connect(on_outrounding) - block_explorers = sorted(util.block_explorer_info().keys()) BLOCK_EX_CUSTOM_ITEM = _("Custom URL") if BLOCK_EX_CUSTOM_ITEM in block_explorers: # malicious translation? @@ -484,15 +429,7 @@ def on_fiat_address(checked): invoices_widgets = [] invoices_widgets.append((bolt11_fallback_cb, None)) invoices_widgets.append((bip21_lightning_cb, None)) - tx_widgets = [] - tx_widgets.append((usechange_cb, None)) - tx_widgets.append((batch_rbf_cb, None)) - tx_widgets.append((unconf_cb, None)) - tx_widgets.append((multiple_cb, None)) - tx_widgets.append((outrounding_cb, None)) - if len(choosers) > 1: - tx_widgets.append((chooser_label, chooser_combo)) - tx_widgets.append((block_ex_label, block_ex_hbox_w)) + lightning_widgets = [] lightning_widgets.append((recov_cb, None)) lightning_widgets.append((trampoline_cb, None)) @@ -509,10 +446,12 @@ def on_fiat_address(checked): misc_widgets.append((filelogging_cb, None)) misc_widgets.append((alias_label, self.alias_e)) misc_widgets.append((qr_label, qr_combo)) + misc_widgets.append((block_ex_label, block_ex_hbox_w)) + if len(choosers) > 1: + misc_widgets.append((chooser_label, chooser_combo)) tabs_info = [ (gui_widgets, _('Appearance')), - (tx_widgets, _('Transactions')), (invoices_widgets, _('Invoices')), (lightning_widgets, _('Lightning')), (fiat_widgets, _('Fiat')), diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 0c6ee566b..e0a57fa33 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -132,12 +132,17 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): vbox.addWidget(self.outputs_textedit) self.setLayout(vbox) - def update(self, tx): + def update(self, tx: Optional[Transaction]): self.tx = tx - inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs()) + if tx is None: + self.inputs_header.setText('') + self.inputs_textedit.setText('') + self.outputs_header.setText('') + self.outputs_textedit.setText('') + return + inputs_header_text = _("Inputs") + ' (%d)'%len(self.tx.inputs()) self.inputs_header.setText(inputs_header_text) - ext = QTextCharFormat() # "external" lnk = QTextCharFormat() lnk.setToolTip(_('Click to open, right-click for menu')) diff --git a/electrum/wallet.py b/electrum/wallet.py index 9b0681589..48b6f6e37 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -926,8 +926,8 @@ def get_spendable_coins( domain: Optional[Iterable[str]] = None, *, nonlocal_only: bool = False, + confirmed_only: bool = False, ) -> Sequence[PartialTxInput]: - confirmed_only = self.config.get('confirmed_only', False) with self._freeze_lock: frozen_addresses = self._frozen_addresses.copy() utxos = self.get_utxos( From d2883e19ac775c145538ea49990f887d8d1ca9be Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 11:11:08 +0100 Subject: [PATCH 0275/1143] android: qt5 activity inhibit screenshots --- contrib/android/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 04d2e74eb..5edc5119a 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,7 +179,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "254033e750ac844426de18085a4262066c8a34c7^{commit}" \ + && git checkout "b6e43aeb69bb7687a91703b7b8cd9af444e7f9f2^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars From ce5b6499fd28bc320bd52f87c893bd1b6cf82107 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 11:43:32 +0100 Subject: [PATCH 0276/1143] qml: styling fixes --- electrum/gui/qml/components/ReceiveDetailsDialog.qml | 5 ++++- electrum/gui/qml/components/controls/BalanceSummary.qml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 0af10e72d..290e6593e 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -45,7 +45,7 @@ ElDialog { Layout.columnSpan: 4 Layout.fillWidth: true - visible: Daemon.currentWallet.lightningCanReceive + visible: !Daemon.currentWallet.lightningCanReceive.isEmpty RowLayout { width: parent.width @@ -55,6 +55,8 @@ ElDialog { font.pixelSize: constants.fontSizeSmall color: Material.accentColor wrapMode: Text.Wrap + // try to fill/wrap in remaining space + Layout.preferredWidth: Math.min(implicitWidth, parent.width - 2*parent.spacing - constants.iconSizeSmall - lnMaxAmount.implicitWidth) } Image { Layout.preferredWidth: constants.iconSizeSmall @@ -62,6 +64,7 @@ ElDialog { source: '../../icons/lightning.png' } FormattedAmount { + id: lnMaxAmount amount: Daemon.currentWallet.lightningCanReceive } } diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index ecced5d63..6a954ed0f 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -39,6 +39,7 @@ Item { } Label { + Layout.alignment: Qt.AlignRight font.pixelSize: constants.fontSizeXLarge font.family: FixedFont text: formattedTotalBalance From a5485e5f68747c88c13cf67fb2afb6cbad3ad714 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 12:03:25 +0100 Subject: [PATCH 0277/1143] android: use material dark style for splashscreen action bar --- contrib/android/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 5edc5119a..19ad50903 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,7 +179,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "b6e43aeb69bb7687a91703b7b8cd9af444e7f9f2^{commit}" \ + && git checkout "8ac9841dc0d2c825a247583b9ed5a33b7b533e85^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars From 461fcf24437481ab4c76f9ce5e54a8747acde317 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 16:02:08 +0100 Subject: [PATCH 0278/1143] qml: wizard use flatbuttons --- electrum/gui/qml/components/wizard/Wizard.qml | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index d34678c01..77979bea0 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -150,36 +150,42 @@ ElDialog { currentIndex: pages.currentIndex } - RowLayout { - Layout.alignment: Qt.AlignHCenter - Button { + ButtonContainer { + Layout.fillWidth: true + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 visible: pages.currentIndex == 0 text: qsTr("Cancel") onClicked: wizard.reject() } - - Button { + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 visible: pages.currentIndex > 0 text: qsTr('Back') onClicked: pages.prev() } - - Button { + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 text: qsTr("Next") visible: !pages.lastpage enabled: pages.pagevalid onClicked: pages.next() } - - Button { + FlatButton { id: finishButton + Layout.fillWidth: true + Layout.preferredWidth: 1 text: qsTr("Finish") visible: pages.lastpage enabled: pages.pagevalid onClicked: pages.finish() } - } + } } From 00286254f91f4bf8054b3d59bf5b888b04b5d0cc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 16:47:19 +0100 Subject: [PATCH 0279/1143] qml: wizard button padding, wizardcomponent now a Pane --- electrum/gui/qml/components/wizard/Wizard.qml | 2 ++ .../gui/qml/components/wizard/WizardComponent.qml | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index 77979bea0..8db489eca 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -11,6 +11,7 @@ ElDialog { width: parent.width height: parent.height + padding: 0 title: wizardTitle + (pages.currentItem.title ? ' - ' + pages.currentItem.title : '') iconSource: '../../../icons/electrum.png' @@ -103,6 +104,7 @@ ElDialog { id: pages Layout.fillWidth: true Layout.fillHeight: true + interactive: false clip:true diff --git a/electrum/gui/qml/components/wizard/WizardComponent.qml b/electrum/gui/qml/components/wizard/WizardComponent.qml index 3f841f8cb..df633c2f3 100644 --- a/electrum/gui/qml/components/wizard/WizardComponent.qml +++ b/electrum/gui/qml/components/wizard/WizardComponent.qml @@ -1,6 +1,8 @@ import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 -Item { +Pane { id: root signal next signal prev @@ -10,6 +12,13 @@ Item { property bool last: false property string title: '' + leftPadding: constants.paddingXLarge + rightPadding: constants.paddingXLarge + + background: Rectangle { + color: Material.dialogColor + } + onAccept: { apply() } From 9b0ff481b26f8e872ea4bb7d3bcda19313abb58f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 18:05:20 +0100 Subject: [PATCH 0280/1143] qml: txdetails feebump/cancel below mempool position --- electrum/gui/qml/components/TxDetails.qml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index e0227437b..2b349fd41 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -120,6 +120,17 @@ Pane { text: txdetails.status } + Label { + text: qsTr('Mempool depth') + color: Material.accentColor + visible: txdetails.mempoolDepth + } + + Label { + text: txdetails.mempoolDepth + visible: txdetails.mempoolDepth + } + TextHighlightPane { Layout.fillWidth: true Layout.columnSpan: 2 @@ -175,17 +186,6 @@ Pane { } - Label { - text: qsTr('Mempool depth') - color: Material.accentColor - visible: txdetails.mempoolDepth - } - - Label { - text: txdetails.mempoolDepth - visible: txdetails.mempoolDepth - } - Label { visible: txdetails.isMined text: qsTr('Date') From b788f79509567b565469ddedea5a045cb41280b2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 6 Mar 2023 18:05:54 +0100 Subject: [PATCH 0281/1143] qml: styling, padding various wizardcomponents --- electrum/gui/qml/components/wizard/WCConfirmSeed.qml | 1 + electrum/gui/qml/components/wizard/WCCreateSeed.qml | 10 +++++++++- electrum/gui/qml/components/wizard/WCHaveSeed.qml | 7 ++++++- electrum/gui/qml/components/wizard/WCImport.qml | 3 +-- electrum/gui/qml/components/wizard/WCMultisig.qml | 2 -- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml index 64b35f6b7..c81727f1e 100644 --- a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml +++ b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml @@ -34,6 +34,7 @@ WizardComponent { } Label { + Layout.topMargin: constants.paddingMedium text: qsTr('Confirm your seed (re-enter)') } diff --git a/electrum/gui/qml/components/wizard/WCCreateSeed.qml b/electrum/gui/qml/components/wizard/WCCreateSeed.qml index 202209b04..096a016b4 100644 --- a/electrum/gui/qml/components/wizard/WCCreateSeed.qml +++ b/electrum/gui/qml/components/wizard/WCCreateSeed.qml @@ -48,7 +48,12 @@ WizardComponent { Layout.fillWidth: true iconStyle: InfoTextArea.IconStyle.Warn } - Label { text: qsTr('Your wallet generation seed is:') } + + Label { + Layout.topMargin: constants.paddingMedium + text: qsTr('Your wallet generation seed is:') + } + SeedTextArea { id: seedtext readOnly: true @@ -60,16 +65,19 @@ WizardComponent { visible: seedtext.text == '' } } + CheckBox { id: extendcb text: qsTr('Extend seed with custom words') } + TextField { id: customwordstext visible: extendcb.checked Layout.fillWidth: true placeholderText: qsTr('Enter your custom word(s)') } + Component.onCompleted : { setWarningText(12) } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index d8e7ed272..08246665d 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -141,6 +141,7 @@ WizardComponent { visible: !is2fa text: qsTr('Seed Type') } + ComboBox { id: seed_variant_cb visible: !is2fa @@ -157,15 +158,19 @@ WizardComponent { checkValid() } } + InfoTextArea { id: infotext Layout.fillWidth: true Layout.columnSpan: 2 } + Label { - text: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') + Layout.topMargin: constants.paddingMedium Layout.columnSpan: 2 + text: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') } + SeedTextArea { id: seedtext Layout.fillWidth: true diff --git a/electrum/gui/qml/components/wizard/WCImport.qml b/electrum/gui/qml/components/wizard/WCImport.qml index 86bddc5d0..baa0546b6 100644 --- a/electrum/gui/qml/components/wizard/WCImport.qml +++ b/electrum/gui/qml/components/wizard/WCImport.qml @@ -26,14 +26,13 @@ WizardComponent { ColumnLayout { width: parent.width - Label { text: qsTr('Import Bitcoin Addresses') } - InfoTextArea { Layout.preferredWidth: parent.width text: qsTr('Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.') } RowLayout { + Layout.topMargin: constants.paddingMedium TextArea { id: import_ta Layout.fillWidth: true diff --git a/electrum/gui/qml/components/wizard/WCMultisig.qml b/electrum/gui/qml/components/wizard/WCMultisig.qml index 2a1e14406..0ed56ffaf 100644 --- a/electrum/gui/qml/components/wizard/WCMultisig.qml +++ b/electrum/gui/qml/components/wizard/WCMultisig.qml @@ -39,8 +39,6 @@ WizardComponent { id: rootLayout width: parent.width - Label { text: qsTr('Multisig wallet') } - InfoTextArea { Layout.preferredWidth: parent.width text: qsTr('Choose the number of participants, and the number of signatures needed to unlock funds in your wallet.') From 74718e9085000dc835759fb8d643d8843d7ce378 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 6 Mar 2023 19:25:46 +0100 Subject: [PATCH 0282/1143] confirm_tx_dialog: separate messages from warnings. add warnings about tx batching and spending unconfirmed coins --- electrum/gui/qml/qetxfinalizer.py | 2 +- electrum/gui/qt/confirm_tx_dialog.py | 37 +++++++++++++++++++++------- electrum/wallet.py | 4 +-- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index a7ab66fdf..bf5bda24f 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -334,7 +334,7 @@ def update(self): invoice_amt=amount, tx_size=tx.estimated_size(), fee=tx.get_fee()) if fee_warning_tuple: allow_send, long_warning, short_warning = fee_warning_tuple - self.warning = long_warning + self.warning = _('Warning') + ': ' + long_warning else: self.warning = '' diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 98be69853..357a54371 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -67,7 +67,8 @@ def __init__(self, *, title='', self.make_tx = make_tx self.output_value = output_value self.tx = None # type: Optional[PartialTransaction] - self.message = '' # set by side effect + self.message = '' # set by side effect in RBF dialogs + self.warning = '' # set by side effect self.error = '' # set by side effect self.config = window.config @@ -93,9 +94,12 @@ def __init__(self, *, title='', vbox.addLayout(top) vbox.addLayout(grid) - self.message_label = WWLabel('\n') + self.message_label = WWLabel('') vbox.addWidget(self.message_label) vbox.addWidget(self.io_widget) + self.warning_label = WWLabel('') + vbox.addWidget(self.warning_label) + buttons = self.create_buttons_bar() vbox.addStretch(1) vbox.addLayout(buttons) @@ -531,7 +535,7 @@ def _update_widgets(self): self.io_widget.update(None) self.set_feerounding_visibility(False) else: - self.check_tx_fee_warning() + self.check_warnings() self.update_fee_fields() if self.locktime_e.get_locktime() is None: self.locktime_e.set_locktime(self.tx.locktime) @@ -542,8 +546,9 @@ def _update_widgets(self): self._update_send_button() self._update_message() - def check_tx_fee_warning(self): - # side effects: self.error, self.message + def check_warnings(self): + # side effects: self.error, self.warning + warnings = [] fee = self.tx.get_fee() assert fee is not None amount = self.tx.output_value() if self.output_value == '!' else self.output_value @@ -555,8 +560,17 @@ def check_tx_fee_warning(self): if not allow_send: self.error = long_warning else: - # note: this may overrride existing message - self.message = long_warning + warnings.append(long_warning) + # warn if spending unconf + if any(txin.block_height<=0 for txin in self.tx.inputs()): + warnings.append(_('This transaction spends unconfirmed coins.')) + # warn if we merge from mempool + base_tx = self.wallet.get_unconfirmed_base_tx_for_batching() + if self.config.get('batch_rbf', False) and base_tx: + warnings.append(_('This payment was merged with another existing transaction.')) + # TODO: warn if we send change back to input address + self.warning = _('Warning') + ': ' + '\n'.join(warnings) if warnings else '' + self._update_warning() def set_locktime(self): if not self.tx: @@ -572,9 +586,14 @@ def _update_extra_fees(self): pass def _update_message(self): - style = ColorScheme.RED if self.error else ColorScheme.BLUE + style = ColorScheme.BLUE self.message_label.setStyleSheet(style.as_stylesheet()) - self.message_label.setText(self.error or self.message) + self.message_label.setText(self.message) + + def _update_warning(self): + style = ColorScheme.RED if self.error else ColorScheme.BLUE + self.warning_label.setStyleSheet(style.as_stylesheet()) + self.warning_label.setText(self.error or self.warning) def _update_send_button(self): enabled = bool(self.tx) and not self.error diff --git a/electrum/wallet.py b/electrum/wallet.py index 48b6f6e37..bff672744 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2859,12 +2859,12 @@ def get_tx_fee_warning( allow_send = False elif fee_ratio >= FEE_RATIO_HIGH_WARNING: long_warning = ( - _('Warning') + ': ' + _("The fee for this transaction seems unusually high.") + _("The fee for this transaction seems unusually high.") + f' ({fee_ratio*100:.2f}% of amount)') short_warning = _("high fee ratio") + "!" elif feerate > FEERATE_WARNING_HIGH_FEE / 1000: long_warning = ( - _('Warning') + ': ' + _("The fee for this transaction seems unusually high.") + _("The fee for this transaction seems unusually high.") + f' (feerate: {feerate:.2f} sat/byte)') short_warning = _("high fee rate") + "!" if long_warning is None: From 0545edd4c66e63f04871bfc6fbe25dc626f82905 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 7 Mar 2023 08:28:34 +0100 Subject: [PATCH 0283/1143] confirm_tx_dialog: fix warning (follow-up previous commit) --- electrum/gui/qt/confirm_tx_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 357a54371..7f3baaec1 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -534,6 +534,7 @@ def _update_widgets(self): if self.not_enough_funds: self.io_widget.update(None) self.set_feerounding_visibility(False) + self.warning = '' else: self.check_warnings() self.update_fee_fields() @@ -545,6 +546,7 @@ def _update_widgets(self): self._update_send_button() self._update_message() + self._update_warning() def check_warnings(self): # side effects: self.error, self.warning @@ -570,7 +572,6 @@ def check_warnings(self): warnings.append(_('This payment was merged with another existing transaction.')) # TODO: warn if we send change back to input address self.warning = _('Warning') + ': ' + '\n'.join(warnings) if warnings else '' - self._update_warning() def set_locktime(self): if not self.tx: From a5c7cc65eedd405562e8b351fa862b428d99c70e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 7 Mar 2023 18:13:27 +0100 Subject: [PATCH 0284/1143] make_unsigned_transaction: call get_unconfirmed_base_tx_for_batching lazily --- electrum/wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index bff672744..48ebc9e84 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1668,8 +1668,8 @@ def make_unsigned_transaction( # Let the coin chooser select the coins to spend coin_chooser = coinchooser.get_coin_chooser(self.config) # If there is an unconfirmed RBF tx, merge with it - base_tx = self.get_unconfirmed_base_tx_for_batching() - if self.config.get('batch_rbf', False) and base_tx: + base_tx = self.get_unconfirmed_base_tx_for_batching() if self.config.get('batch_rbf', False) else None + if base_tx: # make sure we don't try to spend change from the tx-to-be-replaced: coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()] is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL From 18cf546aab7d1a4d122a85ae2b49935cf64c9510 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 7 Mar 2023 18:27:15 +0000 Subject: [PATCH 0285/1143] fix tests side-effecting each other in test_wallet_vertical.test_rbf --- electrum/tests/test_wallet_vertical.py | 110 ++++++++++++++----------- 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index dac6b64d7..6c5aa1afb 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1035,53 +1035,71 @@ async def test_sending_between_p2sh_1of2_and_p2wpkh_p2sh(self, mock_save_db): @mock.patch.object(wallet.Abstract_Wallet, 'save_db') async def test_rbf(self, mock_save_db): self.maxDiff = None - config = SimpleConfig({'electrum_path': self.electrum_path}) - config.set_key('coin_chooser_output_rounding', False) + + class TmpConfig(tempfile.TemporaryDirectory): # to avoid sub-tests side-effecting each other + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config = SimpleConfig({'electrum_path': self.name}) + self.config.set_key('coin_chooser_output_rounding', False) + def __enter__(self): + return self.config + for simulate_moving_txs in (False, True): - with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2pkh_when_there_is_a_change_address( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_when_there_is_a_change_address( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_user_sends_max( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_new_inputs_need_to_be_added( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs): - self._rbf_batching( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_decrease_payment( - simulate_moving_txs=simulate_moving_txs, - config=config) - with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment_batch", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_decrease_payment_batch( - simulate_moving_txs=simulate_moving_txs, - config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2pkh_when_there_is_a_change_address( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2wpkh_when_there_is_a_change_address( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_when_user_sends_max( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_when_new_inputs_need_to_be_added( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs): + self._rbf_batching( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2wpkh_decrease_payment( + simulate_moving_txs=simulate_moving_txs, + config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment_batch", simulate_moving_txs=simulate_moving_txs): + self._bump_fee_p2wpkh_decrease_payment_batch( + simulate_moving_txs=simulate_moving_txs, + config=config) def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean', From 3253e4904b10eb20e9cf5944ac8c70172ac672a5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 7 Mar 2023 18:09:34 +0100 Subject: [PATCH 0286/1143] Add rbf_merge_txid to PartialTransaction, instead of calling get_unconfirmed_base_tx_for_batching a second time from GUI. --- electrum/gui/qt/confirm_tx_dialog.py | 3 +-- electrum/transaction.py | 1 + electrum/wallet.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 7f3baaec1..80c2f8e98 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -567,8 +567,7 @@ def check_warnings(self): if any(txin.block_height<=0 for txin in self.tx.inputs()): warnings.append(_('This transaction spends unconfirmed coins.')) # warn if we merge from mempool - base_tx = self.wallet.get_unconfirmed_base_tx_for_batching() - if self.config.get('batch_rbf', False) and base_tx: + if self.tx.rbf_merge_txid: warnings.append(_('This payment was merged with another existing transaction.')) # TODO: warn if we send change back to input address self.warning = _('Warning') + ': ' + '\n'.join(warnings) if warnings else '' diff --git a/electrum/transaction.py b/electrum/transaction.py index 3e1ff536b..0a7dccbad 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1651,6 +1651,7 @@ def __init__(self): self._inputs = [] # type: List[PartialTxInput] self._outputs = [] # type: List[PartialTxOutput] self._unknown = {} # type: Dict[bytes, bytes] + self.rbf_merge_txid = None def to_json(self) -> dict: d = super().to_json() diff --git a/electrum/wallet.py b/electrum/wallet.py index 48ebc9e84..095e3453c 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1664,6 +1664,9 @@ def make_unsigned_transaction( else: raise Exception(f'Invalid argument fee: {fee}') + # set if we merge with another transaction + rbf_merge_txid = None + if len(i_max) == 0: # Let the coin chooser select the coins to spend coin_chooser = coinchooser.get_coin_chooser(self.config) @@ -1686,6 +1689,7 @@ def fee_estimator(size: Union[int, float, Decimal]) -> int: txi = base_tx.inputs() txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)] + rbf_merge_txid = base_tx.txid() else: txi = [] txo = [] @@ -1727,6 +1731,7 @@ def fee_estimator(size: Union[int, float, Decimal]) -> int: # Timelock tx to current height. tx.locktime = get_locktime_for_new_transaction(self.network) + tx.rbf_merge_txid = rbf_merge_txid tx.set_rbf(rbf) tx.add_info_from_wallet(self) run_hook('make_unsigned_transaction', self, tx) From dce0615b5de57ea2c3ee93137aa9388d17dcd029 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 7 Mar 2023 19:06:22 +0000 Subject: [PATCH 0287/1143] test_wallet_vertical: add a failing test for rbf-batching --- electrum/tests/test_wallet_vertical.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 6c5aa1afb..9918fa4bb 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1815,6 +1815,41 @@ def _rbf_batching(self, *, simulate_moving_txs, config): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 3_900_000, 0), wallet.get_balance()) + async def test_rbf_batching__cannot_batch_as_would_need_to_use_ismine_outputs_of_basetx(self): + """Wallet history contains unconf tx1 that spends all its coins to two ismine outputs, + one 'recv' address (20k sats) and one 'change' (80k sats). + The user tries to create tx2, that pays an invoice for 90k sats. + Even if batch_rbf==True, no batching should be done. Instead, the outputs of tx1 should be used. + """ + wallet = self.create_standard_wallet_from_seed('cause carbon luggage air humble mistake melt paper supreme sense gravity void', + config=self.config) + + # bootstrap wallet (incoming funding_tx0) + funding_tx = Transaction('020000000001021798e10f8b7220c57ea0d605316a52453ca9b3eed99996b5b7bdf4699548bb520000000000fdffffff277d82678d238ca45dd3490ac9fbb49272f0980b093b9197ff70ec8eb082cfb00100000000fdffffff028c360100000000001600147a9bfd90821be827275023849dd91ee80d494957a08601000000000016001476efaaa243327bf3a2c0f5380cb3914099448cec024730440220354b2a74f5ac039cca3618f7ff98229d243b89ac40550c8b027894f2c5cb88ff022064cb5ab1539b4c5367c2e01a8362e0aa12c2732bc8d08c3fce6eab9e56b7fe19012103e0a1499cb3d8047492c60466722c435dfbcffae8da9b83e758fbd203d12728f502473044022073cef8b0cfb093aed5b8eaacbb58c2fa6a69405a8e266cd65e76b726c9151d7602204d5820b23ab96acc57c272aac96d94740a20a6b89c016aa5aed7c06d1e6b9100012102f09e50a265c6a0dcf7c87153ea73d7b12a0fbe9d7d0bbec5db626b2402c1e85c02fa2400') + funding_txid = funding_tx.txid() + wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # to_self_payment tx1 + toself_tx = Transaction('02000000000101ce05b8ae96fe8d2875fd1efcb591b6fb5c5d924bf05d75d880a0e44498fe14b80100000000fdffffff02204e0000000000001600142266c890fad71396f106319368107d5b2a1146feb837010000000000160014b113a47f3718da3fd161339a6681c150fef2cfe3024730440220197bfea1bc5c86c35d68029422342de97c1e5d9adc12e48d99ae359940211a660220770ddb228ae75698f827e2fddc574f0c8eb2a3e109678a2a2b6bc9cbb9593b1c012102b07ca318381fcef5998f34ee4197e96c17aa19867cbe99c544d321807db95ed2f1f92400') + toself_txid = toself_tx.txid() + wallet.adb.receive_tx_callback(toself_txid, toself_tx, TX_HEIGHT_UNCONFIRMED) + + # create outgoing tx2 + outputs = [PartialTxOutput.from_address_and_value("tb1qkfn0fude7z789uys2u7sf80kd4805zpvs3na0h", 90_000)] + for batch_rbf in (False, True): + with self.subTest(batch_rbf=batch_rbf): + coins = wallet.get_spendable_coins(domain=None) + self.assertEqual(2, len(coins)) + + wallet.config.set_key('batch_rbf', batch_rbf) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000) + tx.set_rbf(True) + tx.locktime = 2423302 + tx.version = 2 + wallet.sign_transaction(tx, password=None) + self.assertEqual('02000000000102bbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0000000000fdffffffbbef0182c2c746bd28517b6fd27ba9eef9c7fb5982efd27bd612cc5a28615a3a0100000000fdffffff02602200000000000016001413fabce9be995554a722fc4e1c5ae53ebfd58164905f010000000000160014b266f4f1b9f0bc72f090573d049df66d4efa082c0247304402205c50b9ddb1b3ead6214d7d9707c74ba29ff547880d017aae2459db156bf85b9b022041134562fffa3dccf1ac05d9b07da62a8d57dd158d25d22d1965a011325e64aa012102c72b815ba00ccb0b469cc61a0ceb843d974e630cf34abcfac178838f1974f68f02473044022049774c32b0ad046b7acdb4acc38107b6b1be57c0d167643a48cbc045850c86c202205189ed61342fc52a377c2865a879c4c2606de98eebd6bf4d73874d62329668c70121033484c8ed83c359d1c3e569accb04b77988daab9408fc82869051c10d0749ac2006fa2400', + str(tx)) + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') async def test_cpfp_p2wpkh(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage') From 364510906fcbecf6bcc855b94f279a278541a2b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 7 Mar 2023 18:09:34 +0100 Subject: [PATCH 0288/1143] Fix edge case of batch_rbf, where we need to spend outputs from the base tx --- electrum/wallet.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 095e3453c..2030fa70d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1513,7 +1513,7 @@ def relayfee(self): def dust_threshold(self): return dust_threshold(self.network) - def get_unconfirmed_base_tx_for_batching(self) -> Optional[Transaction]: + def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Transaction]: candidate = None domain = self.get_addresses() for hist_item in self.adb.get_history(domain): @@ -1545,6 +1545,12 @@ def get_unconfirmed_base_tx_for_batching(self) -> Optional[Transaction]: # tx must have opted-in for RBF (even if local, for consistency) if tx.is_final(): continue + # reject merge if we need to spend outputs from the base tx + remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid()) + change_amount = sum(o.value for o in tx.outputs() if self.is_change(o.address)) + output_amount = sum(o.value for o in outputs) + if output_amount > remaining_amount + change_amount: + continue # prefer txns already in mempool (vs local) if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL: candidate = tx @@ -1671,7 +1677,7 @@ def make_unsigned_transaction( # Let the coin chooser select the coins to spend coin_chooser = coinchooser.get_coin_chooser(self.config) # If there is an unconfirmed RBF tx, merge with it - base_tx = self.get_unconfirmed_base_tx_for_batching() if self.config.get('batch_rbf', False) else None + base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) if self.config.get('batch_rbf', False) else None if base_tx: # make sure we don't try to spend change from the tx-to-be-replaced: coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()] From a244b508aa9759731407b462c57428984752bc0a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 8 Mar 2023 17:52:15 +0100 Subject: [PATCH 0289/1143] Confirm tx dialog: warn if tx has multiple change outputs --- electrum/gui/qt/confirm_tx_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 80c2f8e98..5a9643ef2 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -569,6 +569,10 @@ def check_warnings(self): # warn if we merge from mempool if self.tx.rbf_merge_txid: warnings.append(_('This payment was merged with another existing transaction.')) + # warn if we use multiple change outputs + num_change = sum(int(o.is_change) for o in self.tx.outputs()) + if num_change > 1: + warnings.append(_('This transaction has {} change outputs.'.format(num_change))) # TODO: warn if we send change back to input address self.warning = _('Warning') + ': ' + '\n'.join(warnings) if warnings else '' From 37b29b1f371727d26b50f2b2cd411d343eb7418f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 8 Mar 2023 18:44:29 +0100 Subject: [PATCH 0290/1143] confirm_tx_dialog: fix toggle_output_rounding --- electrum/gui/qt/confirm_tx_dialog.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 5a9643ef2..786f43d02 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -73,6 +73,7 @@ def __init__(self, *, title='', self.config = window.config self.wallet = window.wallet + self.feerounding_sats = 0 self.not_enough_funds = False self.no_dynfee_estimates = False self.needs_update = False @@ -187,7 +188,7 @@ def create_fee_controls(self): self.fee_combo.setFocusPolicy(Qt.NoFocus) def feerounding_onclick(): - text = (self.feerounding_text + '\n\n' + + text = (self.feerounding_text() + '\n\n' + _('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + _('At most 100 satoshis might be lost due to this rounding.') + ' ' + _("You can disable this setting in '{}'.").format(_('Preferences')) + '\n' + @@ -268,9 +269,8 @@ def is_send_feerate_frozen(self): return self.feerate_e.isVisible() and self.feerate_e.isModified() \ and (self.feerate_e.text() or self.feerate_e.hasFocus()) - def set_feerounding_text(self, num_satoshis_added): - self.feerounding_text = (_('Additional {} satoshis are going to be added.') - .format(num_satoshis_added)) + def feerounding_text(self): + return (_('Additional {} satoshis are going to be added.').format(self.feerounding_sats)) def set_feerounding_visibility(self, b:bool): # we do not use setVisible because it affects the layout @@ -360,8 +360,8 @@ def update_fee_fields(self): # set fee rounding icon to empty if there is no rounding feerounding = (fee - displayed_fee) if (fee and displayed_fee is not None) else 0 - self.set_feerounding_text(int(feerounding)) - self.feerounding_icon.setToolTip(self.feerounding_text) + self.feerounding_sats = int(feerounding) + self.feerounding_icon.setToolTip(self.feerounding_text()) self.set_feerounding_visibility(abs(feerounding) >= 1) # feerate_label needs to be updated from feerate_e self.update_feerate_label() @@ -425,7 +425,7 @@ def add_pref_action(b, action, text, tooltip): _('Spend only confirmed inputs.')) add_pref_action( self.config.get('coin_chooser_output_rounding', True), - self.toggle_confirmed_only, + self.toggle_output_rounding, _('Enable output value rounding'), _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' + \ _('This might improve your privacy somewhat.') + '\n' + \ @@ -447,6 +447,11 @@ def resize_to_fit_content(self): self.resize(size) self.resize(size) + def toggle_output_rounding(self): + b = not self.config.get('coin_chooser_output_rounding', True) + self.config.set_key('coin_chooser_output_rounding', b) + self.trigger_update() + def toggle_use_change(self): self.wallet.use_change = not self.wallet.use_change self.wallet.db.put('use_change', self.wallet.use_change) From db467ff1f7c4381562a7470fd459778db6b28111 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 10:01:06 +0100 Subject: [PATCH 0291/1143] qml: remove not implemented new version check option in Preferences --- electrum/gui/qml/components/Preferences.qml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 656f49fa6..bb97c23c4 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -94,22 +94,6 @@ Pane { } } - RowLayout { - Layout.columnSpan: 2 - Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall - spacing: 0 - Switch { - id: checkSoftware - enabled: false - } - Label { - Layout.fillWidth: true - text: qsTr('Automatically check for software updates') - wrapMode: Text.Wrap - } - } - RowLayout { Layout.leftMargin: -constants.paddingSmall spacing: 0 From f7a300b89b8251119202f99fd7c40193b64a3dcd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 10:19:17 +0100 Subject: [PATCH 0292/1143] qml: successful PIN entry stays valid for 5 mins --- electrum/gui/qml/components/main.qml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index f17cdd375..9505a8914 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -485,6 +485,9 @@ ApplicationWindow } } + property var _lastCorrectPinTime: 0 + property int _pinValidSeconds: 5*60 + function handleAuthRequired(qtobject, method) { console.log('auth using method ' + method) if (method == 'wallet') { @@ -510,8 +513,14 @@ ApplicationWindow // no PIN configured qtobject.authProceed() } else { + if (Date.now() - _lastCorrectPinTime > _pinValidSeconds * 1000) { + // correct pin entered recently, accept. + qtobject.authProceed() + return + } var dialog = app.pinDialog.createObject(app, {mode: 'check', pincode: Config.pinCode}) dialog.accepted.connect(function() { + _lastCorrectPinTime = Date.now() qtobject.authProceed() dialog.close() }) @@ -527,21 +536,22 @@ ApplicationWindow } property var _lastActive: 0 // record time of last activity - property int _maxInactive: 30 // seconds property bool _lockDialogShown: false onActiveChanged: { console.log('app active = ' + active) - if (!active) { - // deactivated - _lastActive = Date.now() - } else { + if (active) { + if (!_lastActive) { + _lastActive = Date.now() + return + } // activated - if (_lastActive != 0 && Date.now() - _lastActive > _maxInactive * 1000) { + if (Date.now() - _lastCorrectPinTime > _pinValidSeconds * 1000) { if (_lockDialogShown || Config.pinCode == '') return var dialog = app.pinDialog.createObject(app, {mode: 'check', canCancel: false, pincode: Config.pinCode}) dialog.accepted.connect(function() { + _lastCorrectPinTime = Date.now() dialog.close() _lockDialogShown = false }) From c449c8eda84ef6eddade6e5ada107e06b9f9c4eb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Mar 2023 11:26:40 +0100 Subject: [PATCH 0293/1143] set NoWrap for tx input/outputs. Use QTextBrowserWithDefaultSize in utxo dialog. --- electrum/gui/qt/transaction_dialog.py | 2 ++ electrum/gui/qt/utxo_dialog.py | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index e0a57fa33..0986a1150 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -83,6 +83,8 @@ def __init__(self, width: int = 0, height: int = 0): self._width = width self._height = height QTextBrowser.__init__(self) + self.setLineWrapMode(QTextBrowser.NoWrap) + def sizeHint(self): return QSize(self._width, self._height) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index af8a7a618..5c18bae2e 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -35,7 +35,7 @@ from .util import WindowModalDialog, ButtonsLineEdit, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT, WWLabel from .history_list import HistoryList, HistoryModel from .qrtextedit import ShowQRTextEdit -from .transaction_dialog import TxOutputColoring +from .transaction_dialog import TxOutputColoring, QTextBrowserWithDefaultSize if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -58,15 +58,12 @@ def __init__(self, window: 'ElectrumWindow', utxo): num_parents = len(parents) parents_copy = copy.deepcopy(parents) - self.parents_list = QTextBrowser() + self.parents_list = QTextBrowserWithDefaultSize(800, 400) self.parents_list.setOpenLinks(False) # disable automatic link opening self.parents_list.anchorClicked.connect(self.open_tx) # send links to our handler self.parents_list.setFont(QFont(MONOSPACE_FONT)) self.parents_list.setReadOnly(True) self.parents_list.setTextInteractionFlags(self.parents_list.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) - self.parents_list.setMinimumWidth(900) - self.parents_list.setMinimumHeight(400) - self.parents_list.setLineWrapMode(QTextBrowser.NoWrap) self.txo_color_parent = TxOutputColoring( legend=_("Direct parent"), color=ColorScheme.BLUE, tooltip=_("Direct parent")) self.txo_color_uncle = TxOutputColoring( From b960433c60b129f3f146f4827d97d6b7174f98f9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Mar 2023 14:40:08 +0100 Subject: [PATCH 0294/1143] confirm_tx_dialog: Use future tense in warnings + minor fix --- electrum/gui/qt/confirm_tx_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 786f43d02..540928c9d 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -569,11 +569,11 @@ def check_warnings(self): else: warnings.append(long_warning) # warn if spending unconf - if any(txin.block_height<=0 for txin in self.tx.inputs()): - warnings.append(_('This transaction spends unconfirmed coins.')) + if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()): + warnings.append(_('This transaction will spend unconfirmed coins.')) # warn if we merge from mempool if self.tx.rbf_merge_txid: - warnings.append(_('This payment was merged with another existing transaction.')) + warnings.append(_('This payment will be merged with another existing transaction.')) # warn if we use multiple change outputs num_change = sum(int(o.is_change) for o in self.tx.outputs()) if num_change > 1: From d9f1a21219ed466cee1e686576b90c59f7aff700 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 9 Mar 2023 14:57:43 +0100 Subject: [PATCH 0295/1143] reverse_swap: return as soon as we detect the funding transaction --- electrum/submarine_swaps.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 7b82b739a..0d86207da 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -444,12 +444,18 @@ async def reverse_swap( self._add_or_reindex_swap(swap) # add callback to lnwatcher self.add_lnwatcher_callback(swap) - # initiate payment. + # initiate fee payment. if fee_invoice: self.prepayments[prepay_hash] = preimage_hash asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10)) - # initiate payment. - success, log = await self.lnworker.pay_invoice(invoice, attempts=10, channels=channels) + # we return if we detect funding + async def wait_for_funding(swap): + while swap.spending_txid is None: + await asyncio.sleep(1) + # initiate main payment + tasks = [self.lnworker.pay_invoice(invoice, attempts=10, channels=channels), wait_for_funding(swap)] + await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + success = swap.spending_txid is not None return success def _add_or_reindex_swap(self, swap: SwapData) -> None: From 0423970ae077d3e48b62376ba9b5a85a5fda6dd9 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 15:08:39 +0100 Subject: [PATCH 0296/1143] qml: add word picker to SeedTextArea --- .../qml/components/controls/SeedTextArea.qml | 108 +++++++++++++++--- .../qml/components/wizard/WCConfirmSeed.qml | 1 + .../gui/qml/components/wizard/WCHaveSeed.qml | 9 +- electrum/gui/qml/qebitcoin.py | 13 ++- 4 files changed, 111 insertions(+), 20 deletions(-) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 34bd866f8..3ab6e95d9 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -1,20 +1,102 @@ -import QtQuick 2.6 +import QtQuick 2.15 import QtQuick.Layouts 1.0 -import QtQuick.Controls 2.1 +import QtQuick.Controls 2.15 import QtQuick.Controls.Material 2.0 -TextArea { - id: seedtext - Layout.fillWidth: true - Layout.minimumHeight: 80 - rightPadding: constants.paddingLarge - leftPadding: constants.paddingLarge - wrapMode: TextInput.WordWrap - font.bold: true - font.pixelSize: constants.fontSizeLarge - inputMethodHints: Qt.ImhSensitiveData | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText +import org.electrum 1.0 + +Pane { + id: root + implicitHeight: rootLayout.height + padding: 0 + + property alias readOnly: seedtextarea.readOnly + property alias text: seedtextarea.text + property alias placeholderText: seedtextarea.placeholderText + + property var _suggestions: [] + background: Rectangle { color: "transparent" - border.color: Material.accentColor + } + + ColumnLayout { + id: rootLayout + width: parent.width + spacing: 0 + Flickable { + Layout.preferredWidth: parent.width + Layout.minimumHeight: fontMetrics.lineSpacing + 2*constants.paddingXXSmall + 2*constants.paddingXSmall + 2 + implicitHeight: wordsLayout.height + + visible: !readOnly + flickableDirection: Flickable.HorizontalFlick + contentWidth: wordsLayout.width + interactive: wordsLayout.width > width + + RowLayout { + id: wordsLayout + Repeater { + model: _suggestions + Rectangle { + Layout.margins: constants.paddingXXSmall + width: suggestionLabel.width + height: suggestionLabel.height + color: constants.lighterBackground + radius: constants.paddingXXSmall + Label { + id: suggestionLabel + text: modelData + padding: constants.paddingXSmall + leftPadding: constants.paddingSmall + rightPadding: constants.paddingSmall + } + MouseArea { + anchors.fill: parent + onClicked: { + var words = seedtextarea.text.split(' ') + words.pop() + words.push(modelData) + seedtextarea.text = words.join(' ') + ' ' + } + } + } + } + } + } + + TextArea { + id: seedtextarea + Layout.fillWidth: true + Layout.minimumHeight: fontMetrics.height * 3 + topPadding + bottomPadding + + rightPadding: constants.paddingLarge + leftPadding: constants.paddingLarge + + wrapMode: TextInput.WordWrap + font.bold: true + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText + + background: Rectangle { + color: constants.darkerBackground + } + + onTextChanged: { + _suggestions = bitcoin.mnemonicsFor(seedtextarea.text.split(' ').pop()) + // TODO: cursorPosition only on suggestion apply + cursorPosition = text.length + } + } + } + + FontMetrics { + id: fontMetrics + font: seedtextarea.font + } + + Bitcoin { + id: bitcoin } } diff --git a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml index c81727f1e..ac7a0c8ff 100644 --- a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml +++ b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml @@ -41,6 +41,7 @@ WizardComponent { SeedTextArea { id: confirm Layout.fillWidth: true + placeholderText: qsTr('Enter your seed') onTextChanged: checkValid() } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 08246665d..6fe39f1e8 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -165,16 +165,13 @@ WizardComponent { Layout.columnSpan: 2 } - Label { - Layout.topMargin: constants.paddingMedium - Layout.columnSpan: 2 - text: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') - } - SeedTextArea { id: seedtext Layout.fillWidth: true Layout.columnSpan: 2 + + placeholderText: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') + onTextChanged: { validationTimer.restart() } diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 4ca0f4a88..d0c7a0763 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -10,7 +10,8 @@ from electrum.slip39 import decode_mnemonic, Slip39Error from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop from electrum.transaction import tx_from_any -from electrum.mnemonic import is_any_2fa_seed_type +from electrum.mnemonic import Mnemonic, is_any_2fa_seed_type +from electrum.old_mnemonic import wordlist as old_wordlist from .qetypes import QEAmount @@ -26,6 +27,8 @@ class QEBitcoin(QObject): validationMessageChanged = pyqtSignal() _validationMessage = '' + _words = None + def __init__(self, config, parent=None): super().__init__(parent) self.config = config @@ -165,3 +168,11 @@ def isAddressList(self, csv: str): @pyqtSlot(str, result=bool) def isPrivateKeyList(self, csv: str): return keystore.is_private_key_list(csv) + + @pyqtSlot(str, result='QVariantList') + def mnemonicsFor(self, fragment): + if not fragment: + return [] + if not self._words: + self._words = set(Mnemonic('en').wordlist).union(set(old_wordlist)) + return sorted(filter(lambda x: x.startswith(fragment), self._words)) From c6be2521efc2e6211c5a2e39f85b1e760e94f980 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 15:19:33 +0100 Subject: [PATCH 0297/1143] qml: relabel Export tx to Share --- electrum/gui/qml/components/ExportTxDialog.qml | 2 +- electrum/gui/qml/components/TxDetails.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 72d44fb9b..7bfaec28f 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -15,7 +15,7 @@ ElDialog { // if text_qr is undefined text will be used property string text_help - title: qsTr('Export Transaction') + title: qsTr('Share Transaction') parent: Overlay.overlay modal: true diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 2b349fd41..f4f8cada8 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -377,7 +377,7 @@ Pane { Layout.fillWidth: true Layout.preferredWidth: 1 icon.source: '../../icons/qrcode_white.png' - text: qsTr('Export') + text: qsTr('Share') onClicked: { var dialog = exportTxDialog.createObject(root, { txdetails: txdetails }) dialog.open() From b5e7887fa4c62b6ef2511d9d238a3dab7e3b04af Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 15:37:51 +0100 Subject: [PATCH 0298/1143] qml: move channel backups to WalletDetails --- electrum/gui/qml/components/Channels.qml | 9 ------ electrum/gui/qml/components/WalletDetails.qml | 28 +++++++++++++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index dfc2001ed..4bb7305ed 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -133,15 +133,6 @@ Pane { icon.source: '../../icons/lightning.png' } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Channel backups') - onClicked: { - app.stack.push(Qt.resolvedUrl('ChannelBackups.qml')) - } - icon.source: '../../icons/file.png' - } } } diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 54378d517..75f0ed59c 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -473,12 +473,9 @@ Pane { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 - visible: Daemon.currentWallet.walletType == 'imported' - text: Daemon.currentWallet.isWatchOnly - ? qsTr('Add addresses') - : qsTr('Add keys') - icon.source: '../../icons/add.png' - onClicked: rootItem.importAddressesKeys() + text: qsTr('Delete Wallet') + onClicked: rootItem.deleteWallet() + icon.source: '../../icons/delete.png' } FlatButton { Layout.fillWidth: true @@ -490,9 +487,12 @@ Pane { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 - text: qsTr('Delete Wallet') - onClicked: rootItem.deleteWallet() - icon.source: '../../icons/delete.png' + visible: Daemon.currentWallet.walletType == 'imported' + text: Daemon.currentWallet.isWatchOnly + ? qsTr('Add addresses') + : qsTr('Add keys') + icon.source: '../../icons/add.png' + onClicked: rootItem.importAddressesKeys() } FlatButton { Layout.fillWidth: true @@ -502,6 +502,16 @@ Pane { visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning icon.source: '../../icons/lightning.png' } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Channel backups') + visible: Daemon.currentWallet && Daemon.currentWallet.isLightning + icon.source: '../../icons/lightning.png' + onClicked: { + app.stack.push(Qt.resolvedUrl('ChannelBackups.qml')) + } + } } } From d71747138998a773b4a001ba1c158adff43e0399 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 15:38:29 +0100 Subject: [PATCH 0299/1143] qml: move PIN one section up in Preferences --- electrum/gui/qml/components/Preferences.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index bb97c23c4..575b121ef 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -159,11 +159,6 @@ Pane { } } - PrefsHeading { - Layout.columnSpan: 2 - text: qsTr('Wallet behavior') - } - RowLayout { Layout.fillWidth: true Layout.leftMargin: -constants.paddingSmall @@ -221,6 +216,11 @@ Pane { } } + PrefsHeading { + Layout.columnSpan: 2 + text: qsTr('Wallet behavior') + } + RowLayout { Layout.columnSpan: 2 Layout.leftMargin: -constants.paddingSmall From 7584ba00cec0d3b29d6589dbe54f74538d844350 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 9 Mar 2023 14:59:08 +0000 Subject: [PATCH 0300/1143] wallet: kill negative conf numbers for TxMinedInfo fixes https://github.com/spesmilo/electrum/issues/8240 #8240 was triggering an AssertionError in wallet.get_invoice_status, as code there was assuming conf >= 0. To trigger, force-close a LN channel, and while the sweep is waiting on the CSV, try to make a payment in the Send tab to the ismine change address used for the sweep in the future_tx. (order of events can also be reversed) --- electrum/address_synchronizer.py | 7 +++---- electrum/gui/qt/history_list.py | 11 ++++++++--- electrum/util.py | 3 ++- electrum/wallet.py | 9 ++++++--- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 7bd06e45d..7dcbe5fd7 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -674,10 +674,9 @@ def get_tx_height(self, tx_hash: str) -> TxMinedInfo: elif tx_hash in self.unconfirmed_tx: height = self.unconfirmed_tx[tx_hash] return TxMinedInfo(height=height, conf=0) - elif tx_hash in self.future_tx: - num_blocks_remainining = self.future_tx[tx_hash] - self.get_local_height() - if num_blocks_remainining > 0: - return TxMinedInfo(height=TX_HEIGHT_FUTURE, conf=-num_blocks_remainining) + elif wanted_height := self.future_tx.get(tx_hash): + if wanted_height > self.get_local_height(): + return TxMinedInfo(height=TX_HEIGHT_FUTURE, conf=0, wanted_height=wanted_height) else: return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0) else: diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 59c7dc3ef..26180ddda 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -425,11 +425,16 @@ def flags(self, idx: QModelIndex) -> int: @staticmethod def tx_mined_info_from_tx_item(tx_item): - tx_mined_info = TxMinedInfo(height=tx_item['height'], - conf=tx_item['confirmations'], - timestamp=tx_item['timestamp']) + # FIXME a bit hackish to have to reconstruct the TxMinedInfo... + tx_mined_info = TxMinedInfo( + height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp'], + wanted_height=tx_item.get('wanted_height', None), + ) return tx_mined_info + class HistoryList(MyTreeView, AcceptFileDragDrop): filter_columns = [HistoryColumns.STATUS, HistoryColumns.DESCRIPTION, diff --git a/electrum/util.py b/electrum/util.py index ba7dd690a..a0213a583 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1275,10 +1275,11 @@ def func_wrapper(self, *args, **kwargs): class TxMinedInfo(NamedTuple): height: int # height of block that mined tx - conf: Optional[int] = None # number of confirmations, SPV verified (None means unknown) + conf: Optional[int] = None # number of confirmations, SPV verified. >=0, or None (None means unknown) timestamp: Optional[int] = None # timestamp of block that mined tx txpos: Optional[int] = None # position of tx in serialized block header_hash: Optional[str] = None # hash of block that mined tx + wanted_height: Optional[int] = None # in case of timelock, min abs block height class ShortID(bytes): diff --git a/electrum/wallet.py b/electrum/wallet.py index 2030fa70d..ac5aac422 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1000,7 +1000,7 @@ def get_onchain_history(self, *, domain=None): monotonic_timestamp = 0 for hist_item in self.adb.get_history(domain=domain): monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or 999_999_999_999)) - yield { + d = { 'txid': hist_item.txid, 'fee_sat': hist_item.fee, 'height': hist_item.tx_mined_status.height, @@ -1014,6 +1014,9 @@ def get_onchain_history(self, *, domain=None): 'label': self.get_label_for_txid(hist_item.txid), 'txpos_in_block': hist_item.tx_mined_status.txpos, } + if wanted_height := hist_item.tx_mined_status.wanted_height: + d['wanted_height'] = wanted_height + yield d def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice: height = self.adb.get_local_height() @@ -1473,8 +1476,8 @@ def get_tx_status(self, tx_hash, tx_mined_info: TxMinedInfo): conf = tx_mined_info.conf timestamp = tx_mined_info.timestamp if height == TX_HEIGHT_FUTURE: - assert conf < 0, conf - num_blocks_remainining = -conf + num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height() + num_blocks_remainining = max(0, num_blocks_remainining) return 2, f'in {num_blocks_remainining} blocks' if conf == 0: tx = self.db.get_transaction(tx_hash) From 1210ef5f81c5e2d2f9a0c8a60d12a8c7d783b9ce Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 16:13:24 +0100 Subject: [PATCH 0301/1143] qml: various styling --- electrum/gui/qml/components/OpenChannelDialog.qml | 1 + electrum/gui/qml/components/OpenWalletDialog.qml | 4 +++- .../gui/qml/components/controls/ChannelDelegate.qml | 2 +- .../gui/qml/components/controls/PasswordField.qml | 7 +++++++ .../gui/qml/components/wizard/WCWalletPassword.qml | 11 ++++++++--- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 642e98ca6..f4cb089e3 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -169,6 +169,7 @@ ElDialog { FlatButton { Layout.fillWidth: true text: qsTr('Open Channel') + icon.source: '../../icons/confirmed.png' enabled: channelopener.valid onClicked: channelopener.open_channel() } diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index 9fcd75f84..a299c24e9 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -44,7 +44,9 @@ ElDialog { InfoTextArea { id: notice - text: qsTr("Wallet %1 requires password to unlock").arg(name) + text: Daemon.singlePasswordEnabled + ? qsTr('Please enter password') + : qsTr('Wallet %1 requires password to unlock').arg(name) visible: wallet_db.needsPassword iconStyle: InfoTextArea.IconStyle.Warn Layout.fillWidth: true diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index 54b8a05a2..962fa7164 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -22,7 +22,7 @@ ItemDelegate { left: parent.left right: parent.right leftMargin: constants.paddingSmall - rightMargin: constants.paddingSmall + rightMargin: constants.paddingMedium } columns: 2 diff --git a/electrum/gui/qml/components/controls/PasswordField.qml b/electrum/gui/qml/components/controls/PasswordField.qml index 019a928ba..fed6be43e 100644 --- a/electrum/gui/qml/components/controls/PasswordField.qml +++ b/electrum/gui/qml/components/controls/PasswordField.qml @@ -6,6 +6,9 @@ RowLayout { id: root property alias text: password_tf.text property alias tf: password_tf + property alias echoMode: password_tf.echoMode + property bool showReveal: true + signal accepted TextField { @@ -17,6 +20,10 @@ RowLayout { onAccepted: root.accepted() } ToolButton { + id: revealButton + enabled: root.showReveal + opacity: root.showReveal ? 1 : 0 + icon.source: '../../../icons/eye1.png' onClicked: { password_tf.echoMode = password_tf.echoMode == TextInput.Password ? TextInput.Normal : TextInput.Password diff --git a/electrum/gui/qml/components/wizard/WCWalletPassword.qml b/electrum/gui/qml/components/wizard/WCWalletPassword.qml index 5e4921ac5..3da0cec99 100644 --- a/electrum/gui/qml/components/wizard/WCWalletPassword.qml +++ b/electrum/gui/qml/components/wizard/WCWalletPassword.qml @@ -12,14 +12,19 @@ WizardComponent { wizard_data['encrypt'] = password1.text != '' } - GridLayout { - columns: 1 - Label { text: qsTr('Password protect wallet?') } + ColumnLayout { + Label { + text: Daemon.singlePasswordEnabled + ? qsTr('Enter password') + : qsTr('Enter password for %1').arg(wizard_data['wallet_name']) + } PasswordField { id: password1 } PasswordField { id: password2 + showReveal: false + echoMode: password1.echoMode } } } From 62ab6d970284646f803f975f1d09d7df408a7d81 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 9 Mar 2023 15:18:09 +0000 Subject: [PATCH 0302/1143] (trivial) reduce log spam during ln-channel-open --- electrum/lnchannel.py | 2 +- electrum/lnwatcher.py | 2 +- electrum/transaction.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 5329dd271..1825addc9 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -1647,7 +1647,7 @@ def is_funding_tx_mined(self, funding_height): funding_idx = self.funding_outpoint.output_index conf = funding_height.conf if conf < self.funding_txn_minimum_depth(): - self.logger.info(f"funding tx is still not at sufficient depth. actual depth: {conf}") + #self.logger.info(f"funding tx is still not at sufficient depth. actual depth: {conf}") return False assert conf > 0 # check funding_tx amount and script diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index e2d95a580..109490df2 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -212,7 +212,7 @@ async def check_onchain_situation(self, address, funding_outpoint): if not self.adb.is_mine(address): return spenders = self.inspect_tx_candidate(funding_outpoint, 0) - # inspect_tx_candidate might have added new addresses, in which case we return ealy + # inspect_tx_candidate might have added new addresses, in which case we return early if not self.adb.is_up_to_date(): return funding_txid = funding_outpoint.split(':')[0] diff --git a/electrum/transaction.py b/electrum/transaction.py index 0a7dccbad..854e80b43 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1945,7 +1945,7 @@ def sign(self, keypairs) -> None: break if pubkey not in keypairs: continue - _logger.info(f"adding signature for {pubkey}") + _logger.info(f"adding signature for {pubkey}. spending utxo {txin.prevout.to_str()}") sec, compressed = keypairs[pubkey] sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey, sig=sig) From c0e7fc6dec1e312102e3dcd6a9f23fc7d73411b2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 16:17:39 +0100 Subject: [PATCH 0303/1143] qml: don't show placeholder in History when empty history list and synchronizing --- electrum/gui/qml/components/History.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index f989480df..3b8a339a8 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -82,12 +82,12 @@ Pane { ScrollIndicator.vertical: ScrollIndicator { } Label { - visible: Daemon.currentWallet.historyModel.count == 0 + visible: Daemon.currentWallet.historyModel.count == 0 && !Daemon.currentWallet.synchronizing anchors.centerIn: parent width: listview.width * 4/5 font.pixelSize: constants.fontSizeXXLarge color: constants.mutedForeground - text: qsTr('No transactions yet in this wallet') + text: qsTr('No transactions in this wallet yet') wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter } From aaca7c58ad04a0eb2923616f9b63737ee0f2e219 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 17:16:22 +0100 Subject: [PATCH 0304/1143] qml: BalanceSummary now flipflops between fiat view and btc view --- .../components/controls/BalanceSummary.qml | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 6a954ed0f..363563844 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -9,20 +9,31 @@ Item { implicitWidth: balancePane.implicitWidth implicitHeight: balancePane.implicitHeight + property string formattedConfirmedBalance property string formattedTotalBalance property string formattedTotalBalanceFiat - property string formattedLightningCanSend - property string formattedLightningCanSendFiat + property string formattedLightningBalance function setBalances() { + root.formattedConfirmedBalance = Config.formatSats(Daemon.currentWallet.confirmedBalance) root.formattedTotalBalance = Config.formatSats(Daemon.currentWallet.totalBalance) - root.formattedLightningCanSend = Config.formatSats(Daemon.currentWallet.lightningCanSend) + root.formattedLightningBalance = Config.formatSats(Daemon.currentWallet.lightningBalance) if (Daemon.fx.enabled) { root.formattedTotalBalanceFiat = Daemon.fx.fiatValue(Daemon.currentWallet.totalBalance, false) - root.formattedLightningCanSendFiat = Daemon.fx.fiatValue(Daemon.currentWallet.lightningCanSend, false) } } + state: 'fiat' + + states: [ + State { + name: 'fiat' + }, + State { + name: 'btc' + } + ] + TextHighlightPane { id: balancePane leftPadding: constants.paddingXLarge @@ -51,26 +62,26 @@ Item { } Item { - visible: Daemon.fx.enabled + visible: Daemon.fx.enabled && root.state == 'fiat' Layout.preferredHeight: 1 Layout.preferredWidth: 1 } Label { Layout.alignment: Qt.AlignRight - visible: Daemon.fx.enabled + visible: Daemon.fx.enabled && root.state == 'fiat' font.pixelSize: constants.fontSizeLarge color: constants.mutedForeground text: formattedTotalBalanceFiat } Label { - visible: Daemon.fx.enabled + visible: Daemon.fx.enabled && root.state == 'fiat' font.pixelSize: constants.fontSizeLarge color: constants.mutedForeground text: Daemon.fx.fiatCurrency } RowLayout { - visible: Daemon.currentWallet.isLightning + visible: Daemon.currentWallet.isLightning && root.state == 'btc' Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall @@ -83,34 +94,42 @@ Item { } } Label { - visible: Daemon.currentWallet.isLightning + visible: Daemon.currentWallet.isLightning && root.state == 'btc' Layout.alignment: Qt.AlignRight - text: formattedLightningCanSend + text: formattedLightningBalance font.family: FixedFont } Label { - visible: Daemon.currentWallet.isLightning + visible: Daemon.currentWallet.isLightning && root.state == 'btc' font.pixelSize: constants.fontSizeSmall color: Material.accentColor text: Config.baseUnit } - Item { - visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled - Layout.preferredHeight: 1 - Layout.preferredWidth: 1 + + RowLayout { + visible: root.state == 'btc' + Image { + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + source: '../../../icons/bitcoin.png' + } + Label { + text: qsTr('On-chain:') + font.pixelSize: constants.fontSizeSmall + color: Material.accentColor + } } Label { + visible: root.state == 'btc' Layout.alignment: Qt.AlignRight - visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled - font.pixelSize: constants.fontSizeSmall - color: constants.mutedForeground - text: formattedLightningCanSendFiat + text: formattedConfirmedBalance + font.family: FixedFont } Label { - visible: Daemon.currentWallet.isLightning && Daemon.fx.enabled + visible: root.state == 'btc' font.pixelSize: constants.fontSizeSmall - color: constants.mutedForeground - text: Daemon.fx.fiatCurrency + color: Material.accentColor + text: Config.baseUnit } } @@ -124,6 +143,13 @@ Item { font.pixelSize: constants.fontSizeLarge } + MouseArea { + anchors.fill: parent + onClicked: { + root.state = root.state == 'fiat' ? 'btc' : 'fiat' + } + } + // instead of all these explicit connections, we should expose // formatted balances directly as a property Connections { From 016eea2c042312f6d5a1038abd7984ac6e7ee117 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 9 Mar 2023 21:49:42 +0100 Subject: [PATCH 0305/1143] qml: more robust keystore properties builder, support imported type --- electrum/gui/qml/components/WalletDetails.qml | 25 +++++++++++++++++++ electrum/gui/qml/qewallet.py | 10 +++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 75f0ed59c..100ee77a9 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -404,6 +404,30 @@ Pane { width: parent.width columns: 2 + Label { + text: qsTr('Keystore type') + visible: modelData.keystore_type + color: Material.accentColor + } + Label { + Layout.fillWidth: true + text: modelData.keystore_type + visible: modelData.keystore_type + } + + Label { + text: modelData.watch_only + ? qsTr('Imported addresses') + : qsTr('Imported keys') + visible: modelData.num_imported + color: Material.accentColor + } + Label { + Layout.fillWidth: true + text: modelData.num_imported + visible: modelData.num_imported + } + Label { text: qsTr('Derivation prefix') visible: modelData.derivation_prefix @@ -438,6 +462,7 @@ Pane { Layout.fillWidth: true Layout.columnSpan: 2 Layout.leftMargin: constants.paddingLarge + visible: modelData.master_pubkey Label { text: modelData.master_pubkey wrapMode: Text.Wrap diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index c41627239..450d65c85 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -362,10 +362,12 @@ def keystores(self): result = [] for k in self.wallet.get_keystores(): result.append({ - 'derivation_prefix': k.get_derivation_prefix() or '', - 'master_pubkey': k.get_master_public_key() or '', - 'fingerprint': k.get_root_fingerprint() or '', - 'watch_only': k.is_watching_only() + 'keystore_type': k.type, + 'watch_only': k.is_watching_only(), + 'derivation_prefix': (k.get_derivation_prefix() if k.is_deterministic() else '') or '', + 'master_pubkey': (k.get_master_public_key() if k.is_deterministic() else '') or '', + 'fingerprint': (k.get_root_fingerprint() if k.is_deterministic() else '') or '', + 'num_imported': len(k.keypairs) if k.can_import() else 0, }) return result From ffb899871f4ddb58f2c1b7d2c08567179f2f918a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Mar 2023 10:25:14 +0100 Subject: [PATCH 0306/1143] qml: don't explicitly add new channel to listmodel. It is updated automatically (and the model refreshed) through the channels_updated callback --- electrum/gui/qml/components/OpenChannelDialog.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index f4cb089e3..05ae0049c 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -236,7 +236,6 @@ ElDialog { app.channelOpenProgressDialog.channelBackup = channelopener.channelBackup(cid) } // TODO: handle incomplete TX - channelopener.wallet.channelModel.new_channel(cid) root.close() } } From df94bc0d60b5339377849a944f9279bdde79dc5d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 10 Mar 2023 10:52:32 +0100 Subject: [PATCH 0307/1143] TxInOutWidget: reduce size of displayed addresses --- electrum/gui/qt/transaction_dialog.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 0986a1150..14c53809e 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -70,6 +70,7 @@ dialogs = [] # Otherwise python randomly garbage collects the dialogs... + class TxSizeLabel(QLabel): def setAmount(self, byte_size): self.setText(('x %s bytes =' % byte_size) if byte_size else '') @@ -97,7 +98,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.main_window = main_window self.tx = None # type: Optional[Transaction] self.inputs_header = QLabel() - self.inputs_textedit = QTextBrowserWithDefaultSize(950, 100) + self.inputs_textedit = QTextBrowserWithDefaultSize(750, 100) self.inputs_textedit.setOpenLinks(False) # disable automatic link opening self.inputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler self.inputs_textedit.setTextInteractionFlags( @@ -111,7 +112,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.txo_color_2fa = TxOutputColoring( legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) self.outputs_header = QLabel() - self.outputs_textedit = QTextBrowserWithDefaultSize(950, 100) + self.outputs_textedit = QTextBrowserWithDefaultSize(750, 100) self.outputs_textedit.setOpenLinks(False) # disable automatic link opening self.outputs_textedit.anchorClicked.connect(self._open_internal_link) # send links to our handler self.outputs_textedit.setTextInteractionFlags( @@ -195,9 +196,14 @@ def insert_tx_io( cursor.insertText(" " * max(0, 15 - len(short_id)), tcf_ext) # padding cursor.insertText('\t', tcf_ext) # addr - address_str = addr or '
' + if addr is None: + address_str = '
' + elif len(addr) <= 42: + address_str = addr + else: + address_str = addr[0:30] + '…' + addr[-11:] cursor.insertText(address_str, tcf_addr) - cursor.insertText(" " * max(0, 62 - len(address_str)), tcf_ext) # padding + cursor.insertText(" " * max(0, 42 - len(address_str)), tcf_ext) # padding cursor.insertText('\t', tcf_ext) # value value_str = self.main_window.format_amount(value, whitespaces=True) From d0b2c66550a12fc9c60acb031bd50277af11f970 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 10 Mar 2023 11:46:36 +0100 Subject: [PATCH 0308/1143] confirm_tx_dialog: simplify messages and warnings. reduce the use of side effects --- electrum/gui/qt/confirm_tx_dialog.py | 45 +++++++++++----------------- electrum/gui/qt/rbf_dialog.py | 10 +++++-- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 540928c9d..0845d5d66 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -67,8 +67,7 @@ def __init__(self, *, title='', self.make_tx = make_tx self.output_value = output_value self.tx = None # type: Optional[PartialTransaction] - self.message = '' # set by side effect in RBF dialogs - self.warning = '' # set by side effect + self.messages = [] self.error = '' # set by side effect self.config = window.config @@ -95,11 +94,9 @@ def __init__(self, *, title='', vbox.addLayout(top) vbox.addLayout(grid) + vbox.addWidget(self.io_widget) self.message_label = WWLabel('') vbox.addWidget(self.message_label) - vbox.addWidget(self.io_widget) - self.warning_label = WWLabel('') - vbox.addWidget(self.warning_label) buttons = self.create_buttons_bar() vbox.addStretch(1) @@ -227,7 +224,7 @@ def feerounding_onclick(): def trigger_update(self): # set tx to None so that the ok button is disabled while we compute the new tx self.tx = None - self.message = '' + self.messages = [] self.error = '' self._update_widgets() self.needs_update = True @@ -523,6 +520,7 @@ def on_preview(self): self.accept() def _update_widgets(self): + # side effect: self.error self._update_amount_label() if self.not_enough_funds: self.error = _('Not enough funds.') @@ -533,15 +531,13 @@ def _update_widgets(self): self.error += ' ' + _('You need to set a lower fee.') else: self.error += '' - else: - self.error = '' if not self.tx: if self.not_enough_funds: self.io_widget.update(None) self.set_feerounding_visibility(False) - self.warning = '' + self.messages = [] else: - self.check_warnings() + self.messages = self.get_messages() self.update_fee_fields() if self.locktime_e.get_locktime() is None: self.locktime_e.set_locktime(self.tx.locktime) @@ -551,11 +547,10 @@ def _update_widgets(self): self._update_send_button() self._update_message() - self._update_warning() - def check_warnings(self): - # side effects: self.error, self.warning - warnings = [] + def get_messages(self): + # side effect: self.error + messages = [] fee = self.tx.get_fee() assert fee is not None amount = self.tx.output_value() if self.output_value == '!' else self.output_value @@ -567,19 +562,19 @@ def check_warnings(self): if not allow_send: self.error = long_warning else: - warnings.append(long_warning) + messages.append(long_warning) # warn if spending unconf if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()): - warnings.append(_('This transaction will spend unconfirmed coins.')) + messages.append(_('This transaction will spend unconfirmed coins.')) # warn if we merge from mempool if self.tx.rbf_merge_txid: - warnings.append(_('This payment will be merged with another existing transaction.')) + messages.append(_('This payment will be merged with another existing transaction.')) # warn if we use multiple change outputs num_change = sum(int(o.is_change) for o in self.tx.outputs()) if num_change > 1: - warnings.append(_('This transaction has {} change outputs.'.format(num_change))) + messages.append(_('This transaction has {} change outputs.'.format(num_change))) # TODO: warn if we send change back to input address - self.warning = _('Warning') + ': ' + '\n'.join(warnings) if warnings else '' + return messages def set_locktime(self): if not self.tx: @@ -595,14 +590,10 @@ def _update_extra_fees(self): pass def _update_message(self): - style = ColorScheme.BLUE - self.message_label.setStyleSheet(style.as_stylesheet()) - self.message_label.setText(self.message) - - def _update_warning(self): style = ColorScheme.RED if self.error else ColorScheme.BLUE - self.warning_label.setStyleSheet(style.as_stylesheet()) - self.warning_label.setText(self.error or self.warning) + message_str = '\n'.join(self.messages) if self.messages else '' + self.message_label.setStyleSheet(style.as_stylesheet()) + self.message_label.setText(self.error or message_str) def _update_send_button(self): enabled = bool(self.tx) and not self.error @@ -645,8 +636,6 @@ def update_tx(self, *, fallback_to_zero_fee: bool = False): self.tx = self.make_tx(fee_estimator, confirmed_only=confirmed_only) self.not_enough_funds = False self.no_dynfee_estimates = False - error = '' - message = '' except NotEnoughFunds: self.not_enough_funds = True self.tx = None diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index f0de304a9..045413e9b 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -126,14 +126,18 @@ def update_tx(self): except CannotBumpFee as e: self.tx = None self.error = str(e) + + def get_messages(self): + messages = super().get_messages() if not self.tx: return delta = self.tx.get_fee() - self.old_tx.get_fee() if not self.is_decrease_payment(): - self.message = _("You will pay {} more.").format(self.main_window.format_amount_and_units(delta)) + msg = _("You will pay {} more.").format(self.main_window.format_amount_and_units(delta)) else: - self.message = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta)) - + msg = _("The recipient will receive {} less.").format(self.main_window.format_amount_and_units(delta)) + messages.insert(0, msg) + return messages class BumpFeeDialog(_BaseRBFDialog): From cb5a9e499fe4d8730c292a1b04f36dddaf139dbd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Mar 2023 12:52:04 +0100 Subject: [PATCH 0309/1143] qml: SwapDialog ok button --- electrum/gui/qml/components/SwapDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 6239e6a06..f421924e0 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -186,8 +186,8 @@ ElDialog { FlatButton { Layout.columnSpan: 2 Layout.fillWidth: true - text: qsTr('Swap') - icon.source: Qt.resolvedUrl('../../icons/update.png') + text: qsTr('Ok') + icon.source: Qt.resolvedUrl('../../icons/confirmed.png') enabled: swaphelper.valid onClicked: swaphelper.executeSwap() } From 15a3c2d3443a83b1d2561459fbba31e2a0d5cb40 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Mar 2023 13:31:41 +0100 Subject: [PATCH 0310/1143] qml: auto sign & broadcast fee bump transactions if wallet can sign without cosigners. Show a dialog otherwise --- electrum/gui/qml/components/TxDetails.qml | 20 +++++++- .../gui/qml/components/WalletMainView.qml | 10 ++++ electrum/gui/qml/qetxdetails.py | 47 ++++++++++++++----- electrum/gui/qml/qewallet.py | 4 +- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index f4f8cada8..4965d5841 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -447,7 +447,15 @@ Pane { onTxaccepted: { root.rawtx = rbffeebumper.getNewTx() - // TODO: sign & send when possible? + if (Daemon.currentWallet.canSignWithoutCosigner) { + txdetails.sign(true) + // close txdetails? + } else { + var dialog = app.messageDialog.createObject(app, { + text: qsTr('Transaction fee updated.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') + }) + dialog.open() + } } onClosed: destroy() } @@ -466,7 +474,15 @@ Pane { onTxaccepted: { // replaces parent tx with cpfp tx root.rawtx = cpfpfeebumper.getNewTx() - // TODO: sign & send when possible? + if (Daemon.currentWallet.canSignWithoutCosigner) { + txdetails.sign(true) + // close txdetails? + } else { + var dialog = app.messageDialog.createObject(app, { + text: qsTr('CPFP fee bump transaction created.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') + }) + dialog.open() + } } onClosed: destroy() } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 15170538b..96bad9a02 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -238,6 +238,16 @@ Item { } } + Connections { + target: Daemon.currentWallet + function onBroadcastFailed(txid, code, message) { + var dialog = app.messageDialog.createObject(app, { + text: message + }) + dialog.open() + } + } + Component { id: invoiceDialog InvoiceDialog { diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 7457b6a01..8da45ca64 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -67,6 +67,12 @@ def on_event_verified(self, wallet, txid, info): self._logger.debug('verified event for our txid %s' % txid) self.update() + @event_listener + def on_event_new_transaction(self, wallet, tx): + if wallet == self._wallet.wallet and tx.txid() == self._txid: + self._logger.debug('new_transaction event for our txid %s' % self._txid) + self.update() + walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) def wallet(self): @@ -293,25 +299,24 @@ def update_mined_status(self, tx_mined_info): self._header_hash = tx_mined_info.header_hash @pyqtSlot() - def sign(self): + @pyqtSlot(bool) + def sign(self, broadcast = False): + # TODO: connecting/disconnecting signal handlers here is hmm try: self._wallet.transactionSigned.disconnect(self.onSigned) + self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) + if broadcast: + self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) except: pass self._wallet.transactionSigned.connect(self.onSigned) - self._wallet.sign(self._tx) + self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded) + if broadcast: + self._wallet.broadcastFailed.connect(self.onBroadcastFailed) + self._wallet.sign(self._tx, broadcast=broadcast) # side-effect: signing updates self._tx # we rely on this for broadcast - @pyqtSlot(str) - def onSigned(self, txid): - if txid != self._txid: - return - - self._logger.debug('onSigned') - self._wallet.transactionSigned.disconnect(self.onSigned) - self.update() - @pyqtSlot() def broadcast(self): assert self._tx.is_complete() @@ -327,6 +332,26 @@ def broadcast(self): self._wallet.broadcast(self._tx) + @pyqtSlot(str) + def onSigned(self, txid): + if txid != self._txid: + return + + self._logger.debug('onSigned') + self._wallet.transactionSigned.disconnect(self.onSigned) + self.update() + + @pyqtSlot(str) + def onBroadcastSucceeded(self, txid): + if txid != self._txid: + return + + self._logger.debug('onBroadcastSucceeded') + self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) + + self._can_broadcast = False + self.detailsChanged.emit() + @pyqtSlot(str,str,str) def onBroadcastFailed(self, txid, code, reason): if txid != self._txid: diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 450d65c85..cdfcf7000 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -541,10 +541,10 @@ def broadcast_thread(): self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx)) except TxBroadcastError as e: self._logger.error(repr(e)) - self.broadcastFailed.emit(tx.txid(),'',repr(e)) + self.broadcastFailed.emit(tx.txid(),'',str(e)) except BestEffortRequestFailed as e: self._logger.error(repr(e)) - self.broadcastFailed.emit(tx.txid(),'',repr(e)) + self.broadcastFailed.emit(tx.txid(),'',str(e)) else: self._logger.info('broadcast success') self.broadcastSucceeded.emit(tx.txid()) From f2dc651c9b0176a06dcaa53e354784a963ebcf65 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 10 Mar 2023 14:03:57 +0000 Subject: [PATCH 0311/1143] Qt history list: Ctrl+F filter to work for "Short ID" (scid) --- electrum/gui/qt/history_list.py | 22 ++++++++++++++++++---- electrum/gui/qt/utxo_dialog.py | 3 ++- electrum/lnworker.py | 6 ++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 26180ddda..9f38e70fc 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -88,6 +88,8 @@ class HistoryColumns(IntEnum): FIAT_ACQ_PRICE = 5 FIAT_CAP_GAINS = 6 TXID = 7 + SHORT_ID = 8 # ~SCID + class HistorySortModel(QSortFilterProxyModel): def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): @@ -121,6 +123,7 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria tx_item = self.get_data() is_lightning = tx_item.get('lightning', False) timestamp = tx_item['timestamp'] + short_id = None if is_lightning: status = 0 if timestamp is None: @@ -129,6 +132,9 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria status_str = format_time(int(timestamp)) else: tx_hash = tx_item['txid'] + txpos_in_block = tx_item.get('txpos_in_block') + if txpos_in_block is not None and txpos_in_block >= 0: + short_id = f"{tx_item['height']}x{txpos_in_block}" conf = tx_item['confirmations'] try: status, status_str = self.model.tx_status_cache[tx_hash] @@ -155,6 +161,7 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria HistoryColumns.FIAT_CAP_GAINS: tx_item['capital_gain'].value if 'capital_gain' in tx_item else None, HistoryColumns.TXID: tx_hash if not is_lightning else None, + HistoryColumns.SHORT_ID: short_id, } return QVariant(d[col]) if role == MyTreeView.ROLE_EDIT_KEY: @@ -219,6 +226,8 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria return QVariant(window.fx.format_fiat(cg)) elif col == HistoryColumns.TXID: return QVariant(tx_hash) if not is_lightning else QVariant('') + elif col == HistoryColumns.SHORT_ID: + return QVariant(short_id or "") return QVariant() @@ -350,6 +359,7 @@ def set_visible(col: int, b: bool): self.view.showColumn(col) if b else self.view.hideColumn(col) # txid set_visible(HistoryColumns.TXID, False) + set_visible(HistoryColumns.SHORT_ID, False) # fiat history = self.window.fx.show_history() cap_gains = self.window.fx.get_history_capital_gains_config() @@ -415,6 +425,7 @@ def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDat HistoryColumns.FIAT_ACQ_PRICE: fiat_acq_title, HistoryColumns.FIAT_CAP_GAINS: fiat_cg_title, HistoryColumns.TXID: 'TXID', + HistoryColumns.SHORT_ID: 'Short ID', }[section] def flags(self, idx: QModelIndex) -> int: @@ -436,10 +447,13 @@ def tx_mined_info_from_tx_item(tx_item): class HistoryList(MyTreeView, AcceptFileDragDrop): - filter_columns = [HistoryColumns.STATUS, - HistoryColumns.DESCRIPTION, - HistoryColumns.AMOUNT, - HistoryColumns.TXID] + filter_columns = [ + HistoryColumns.STATUS, + HistoryColumns.DESCRIPTION, + HistoryColumns.AMOUNT, + HistoryColumns.TXID, + HistoryColumns.SHORT_ID, + ] def tx_item_from_proxy_row(self, proxy_row): hm_idx = self.model().mapToSource(self.model().index(proxy_row, 0)) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 5c18bae2e..8d539e419 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -38,6 +38,7 @@ from .transaction_dialog import TxOutputColoring, QTextBrowserWithDefaultSize if TYPE_CHECKING: + from electrum.transaction import PartialTxInput from .main_window import ElectrumWindow # todo: @@ -46,7 +47,7 @@ class UTXODialog(WindowModalDialog): - def __init__(self, window: 'ElectrumWindow', utxo): + def __init__(self, window: 'ElectrumWindow', utxo: 'PartialTxInput'): WindowModalDialog.__init__(self, window, _("Coin Privacy Analysis")) self.main_window = window self.config = window.config diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 9b69156a6..fe86970e8 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -905,7 +905,8 @@ def get_onchain_history(self): 'fee_msat': None, 'height': tx_height.height, 'confirmations': tx_height.conf, - } + 'txpos_in_block': tx_height.txpos, + } # FIXME this data structure needs to be kept in ~sync with wallet.get_onchain_history out[funding_txid] = item item = chan.get_closing_height() if item is None: @@ -926,7 +927,8 @@ def get_onchain_history(self): 'fee_msat': None, 'height': tx_height.height, 'confirmations': tx_height.conf, - } + 'txpos_in_block': tx_height.txpos, + } # FIXME this data structure needs to be kept in ~sync with wallet.get_onchain_history out[closing_txid] = item # add info about submarine swaps settled_payments = self.get_payments(status='settled') From 7746cc8e6072b703e49e14216374489ff204198f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 10 Mar 2023 14:23:17 +0000 Subject: [PATCH 0312/1143] bip32: (trivial) rename method strpath_to_intpath, for symmetry Required a much higher mental load to parse the name "convert_bip32_path_to_list_of_uint32" than to parse "convert_bip32_strpath_to_intpath". And we already have the ~inverse: "convert_bip32_intpath_to_strpath". --- electrum/bip32.py | 18 +++++++++--------- electrum/bip39_recovery.py | 2 +- electrum/descriptor.py | 6 +++--- electrum/keystore.py | 12 ++++++------ electrum/plugins/bitbox02/bitbox02.py | 8 ++++---- electrum/plugins/jade/jade.py | 8 ++++---- electrum/plugins/keepkey/clientbase.py | 4 ++-- electrum/plugins/ledger/ledger.py | 2 +- electrum/plugins/safe_t/clientbase.py | 4 ++-- electrum/plugins/trezor/clientbase.py | 2 +- electrum/plugins/trezor/trezor.py | 2 +- electrum/tests/test_bitcoin.py | 12 ++++++------ electrum/wallet.py | 4 ++-- 13 files changed, 42 insertions(+), 42 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 225427f78..f3ea60f95 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -236,7 +236,7 @@ def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP3 if path is None: raise Exception("derivation path must not be None") if isinstance(path, str): - path = convert_bip32_path_to_list_of_uint32(path) + path = convert_bip32_strpath_to_intpath(path) if not self.is_private(): raise Exception("cannot do bip32 private derivation; private key missing") if not path: @@ -262,7 +262,7 @@ def subkey_at_public_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32 if path is None: raise Exception("derivation path must not be None") if isinstance(path, str): - path = convert_bip32_path_to_list_of_uint32(path) + path = convert_bip32_strpath_to_intpath(path) if not path: return self.convert_to_public() depth = self.depth @@ -313,8 +313,8 @@ def xpub_from_xprv(xprv): return BIP32Node.from_xkey(xprv).to_xpub() -def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: - """Convert bip32 path to list of uint32 integers with prime flags +def convert_bip32_strpath_to_intpath(n: str) -> List[int]: + """Convert bip32 path str to list of uint32 integers with prime flags m/0/-1/1' -> [0, 0x80000001, 0x80000001] based on code in trezorlib @@ -373,7 +373,7 @@ def is_bip32_derivation(s: str) -> bool: try: if not (s == 'm' or s.startswith('m/')): return False - convert_bip32_path_to_list_of_uint32(s) + convert_bip32_strpath_to_intpath(s) except: return False else: @@ -385,14 +385,14 @@ def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]: return None if not is_bip32_derivation(s): raise ValueError(f"invalid bip32 derivation: {s}") - ints = convert_bip32_path_to_list_of_uint32(s) + ints = convert_bip32_strpath_to_intpath(s) return convert_bip32_intpath_to_strpath(ints) def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool: """Returns whether all levels in path use non-hardened derivation.""" if isinstance(path, str): - path = convert_bip32_path_to_list_of_uint32(path) + path = convert_bip32_strpath_to_intpath(path) for child_index in path: if child_index < 0: raise ValueError('the bip32 index needs to be non-negative') @@ -425,7 +425,7 @@ def is_xkey_consistent_with_key_origin_info(xkey: str, *, bip32node = BIP32Node.from_xkey(xkey) int_path = None if derivation_prefix is not None: - int_path = convert_bip32_path_to_list_of_uint32(derivation_prefix) + int_path = convert_bip32_strpath_to_intpath(derivation_prefix) if int_path is not None and len(int_path) != bip32node.depth: return False if bip32node.depth == 0: @@ -503,7 +503,7 @@ def from_string(cls, s: str) -> 'KeyOriginInfo': fingerprint = binascii.unhexlify(s[0:8]) path: Sequence[int] = [] if len(entries) > 1: - path = convert_bip32_path_to_list_of_uint32(s[9:]) + path = convert_bip32_strpath_to_intpath(s[9:]) return cls(fingerprint, path) def get_derivation_path(self) -> str: diff --git a/electrum/bip39_recovery.py b/electrum/bip39_recovery.py index e6c7c1fa4..bf9fa5375 100644 --- a/electrum/bip39_recovery.py +++ b/electrum/bip39_recovery.py @@ -8,7 +8,7 @@ from . import bitcoin from .constants import BIP39_WALLET_FORMATS from .bip32 import BIP32_PRIME, BIP32Node -from .bip32 import convert_bip32_path_to_list_of_uint32 as bip32_str_to_ints +from .bip32 import convert_bip32_strpath_to_intpath as bip32_str_to_ints from .bip32 import convert_bip32_intpath_to_strpath as bip32_ints_to_str from .util import OldTaskGroup diff --git a/electrum/descriptor.py b/electrum/descriptor.py index 4ad9fd71a..3bce2da20 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -16,7 +16,7 @@ import enum -from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo, BIP32_PRIME +from .bip32 import convert_bip32_strpath_to_intpath, BIP32Node, KeyOriginInfo, BIP32_PRIME from . import bitcoin from .bitcoin import construct_script, opcodes, construct_witness from . import constants @@ -250,7 +250,7 @@ def get_pubkey_bytes(self, *, pos: Optional[int] = None) -> bytes: if self.is_range(): assert path_str[-1] == "*" path_str = path_str[:-1] + str(pos) - path = convert_bip32_path_to_list_of_uint32(path_str) + path = convert_bip32_strpath_to_intpath(path_str) child_key = self.extkey.subkey_at_public_derivation(path) return child_key.eckey.get_public_key_bytes(compressed=compressed) else: @@ -286,7 +286,7 @@ def get_der_suffix_int_list(self, *, pos: Optional[int] = None) -> List[int]: der_suffix = self.deriv_path assert (wc_count := der_suffix.count("*")) <= 1, wc_count der_suffix = der_suffix.replace("*", str(pos)) - return convert_bip32_path_to_list_of_uint32(der_suffix) + return convert_bip32_strpath_to_intpath(der_suffix) def __lt__(self, other: 'PubkeyProvider') -> bool: return self.pubkey < other.pubkey diff --git a/electrum/keystore.py b/electrum/keystore.py index 9ccf2ab30..c1c5638a6 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -34,7 +34,7 @@ from . import bitcoin, ecc, constants, bip32 from .bitcoin import deserialize_privkey, serialize_privkey, BaseDecodeError from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput -from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, +from .bip32 import (convert_bip32_strpath_to_intpath, BIP32_PRIME, is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info, KeyOriginInfo) @@ -454,7 +454,7 @@ def test_der_suffix_against_pubkey(der_suffix: Sequence[int], pubkey: bytes) -> # 1. try fp against our root ks_root_fingerprint_hex = self.get_root_fingerprint() ks_der_prefix_str = self.get_derivation_prefix() - ks_der_prefix = convert_bip32_path_to_list_of_uint32(ks_der_prefix_str) if ks_der_prefix_str else None + ks_der_prefix = convert_bip32_strpath_to_intpath(ks_der_prefix_str) if ks_der_prefix_str else None if (ks_root_fingerprint_hex is not None and ks_der_prefix is not None and fp_found.hex() == ks_root_fingerprint_hex): if path_found[:len(ks_der_prefix)] == ks_der_prefix: @@ -524,11 +524,11 @@ def get_fp_and_derivation_to_be_used_in_partial_tx( if not only_der_suffix and fingerprint_hex is not None and der_prefix_str is not None: # use root fp, and true full path fingerprint_bytes = bfh(fingerprint_hex) - der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str) + der_prefix_ints = convert_bip32_strpath_to_intpath(der_prefix_str) else: # use intermediate fp, and claim der suffix is the full path fingerprint_bytes = self.get_bip32_node_for_xpub().calc_fingerprint_of_this_node() - der_prefix_ints = convert_bip32_path_to_list_of_uint32('m') + der_prefix_ints = convert_bip32_strpath_to_intpath('m') der_full = der_prefix_ints + list(der_suffix) return fingerprint_bytes, der_full @@ -832,7 +832,7 @@ def get_fp_and_derivation_to_be_used_in_partial_tx( fingerprint_hex = self.get_root_fingerprint() der_prefix_str = self.get_derivation_prefix() fingerprint_bytes = bfh(fingerprint_hex) - der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str) + der_prefix_ints = convert_bip32_strpath_to_intpath(der_prefix_str) der_full = der_prefix_ints + list(der_suffix) return fingerprint_bytes, der_full @@ -1030,7 +1030,7 @@ def from_bip43_rootseed(root_seed, derivation, xtype=None): def xtype_from_derivation(derivation: str) -> str: """Returns the script type to be used for this derivation.""" - bip32_indices = convert_bip32_path_to_list_of_uint32(derivation) + bip32_indices = convert_bip32_strpath_to_intpath(derivation) if len(bip32_indices) >= 1: if bip32_indices[0] == 84 + BIP32_PRIME: return 'p2wpkh' diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 4b328dc6b..ccba7c126 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -201,7 +201,7 @@ def coin_network_from_electrum_network(self) -> int: @runs_in_hwd_thread def get_password_for_storage_encryption(self) -> str: derivation = get_derivation_used_for_hw_device_encryption() - derivation_list = bip32.convert_bip32_path_to_list_of_uint32(derivation) + derivation_list = bip32.convert_bip32_strpath_to_intpath(derivation) xpub = self.bitbox02_device.electrum_encryption_key(derivation_list) node = bip32.BIP32Node.from_xkey(xpub, net = constants.BitcoinMainnet()).subkey_at_public_derivation(()) return node.eckey.get_public_key_bytes(compressed=True).hex() @@ -218,7 +218,7 @@ def get_xpub(self, bip32_path: str, xtype: str, *, display: bool = False) -> str self.fail_if_not_initialized() - xpub_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + xpub_keypath = bip32.convert_bip32_strpath_to_intpath(bip32_path) coin_network = self.coin_network_from_electrum_network() if xtype == "p2wpkh": @@ -341,7 +341,7 @@ def show_address( "Need to setup communication first before attempting any BitBox02 calls" ) - address_keypath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + address_keypath = bip32.convert_bip32_strpath_to_intpath(bip32_path) coin_network = self.coin_network_from_electrum_network() if address_type == "p2wpkh": @@ -548,7 +548,7 @@ def sign_message(self, keypath: str, message: bytes, script_type: str) -> bytes: script_config=bitbox02.btc.BTCScriptConfig( simple_type=simple_type, ), - keypath=bip32.convert_bip32_path_to_list_of_uint32(keypath), + keypath=bip32.convert_bip32_strpath_to_intpath(keypath), ), message, ) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 135713d6d..2a68e0ad4 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -55,7 +55,7 @@ def _register_multisig_wallet(wallet, keystore, address): for kstore in wallet.get_keystores(): fingerprint = kstore.get_root_fingerprint() bip32_path_prefix = kstore.get_derivation_prefix() - derivation_path = bip32.convert_bip32_path_to_list_of_uint32(bip32_path_prefix) + derivation_path = bip32.convert_bip32_strpath_to_intpath(bip32_path_prefix) # Jade only understands standard xtypes, so convert here node = bip32.BIP32Node.from_xkey(kstore.xpub) @@ -169,7 +169,7 @@ def get_xpub(self, bip32_path, xtype): self.authenticate() # Jade only provides traditional xpubs ... - path = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + path = bip32.convert_bip32_strpath_to_intpath(bip32_path) xpub = self.jade.get_xpub(self._network(), path) # ... so convert to relevant xtype locally @@ -180,7 +180,7 @@ def get_xpub(self, bip32_path, xtype): def sign_message(self, bip32_path_prefix, sequence, message): self.authenticate() - path = bip32.convert_bip32_path_to_list_of_uint32(bip32_path_prefix) + path = bip32.convert_bip32_strpath_to_intpath(bip32_path_prefix) path.extend(sequence) if isinstance(message, bytes) or isinstance(message, bytearray): @@ -214,7 +214,7 @@ def sign_tx(self, txn_bytes, inputs, change): @runs_in_hwd_thread def show_address(self, bip32_path_prefix, sequence, txin_type): self.authenticate() - path = bip32.convert_bip32_path_to_list_of_uint32(bip32_path_prefix) + path = bip32.convert_bip32_strpath_to_intpath(bip32_path_prefix) path.extend(sequence) script_variant = self._convertAddrType(txin_type, multisig=False) address = self.jade.get_receive_address(self._network(), path, variant=script_variant) diff --git a/electrum/plugins/keepkey/clientbase.py b/electrum/plugins/keepkey/clientbase.py index 5fa2339d7..4c0257860 100644 --- a/electrum/plugins/keepkey/clientbase.py +++ b/electrum/plugins/keepkey/clientbase.py @@ -6,7 +6,7 @@ from electrum.i18n import _ from electrum.util import UserCancelled from electrum.keystore import bip39_normalize_passphrase -from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 +from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath from electrum.logging import Logger from electrum.plugin import runs_in_hwd_thread from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase @@ -154,7 +154,7 @@ def timeout(self, cutoff): @staticmethod def expand_path(n): - return convert_bip32_path_to_list_of_uint32(n) + return convert_bip32_strpath_to_intpath(n) @runs_in_hwd_thread def cancel(self): diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 5b1d213db..9ccf950f4 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -431,7 +431,7 @@ def get_xpub(self, bip32_path, xtype): if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) bip32_path = bip32.normalize_bip32_derivation(bip32_path) - bip32_intpath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + bip32_intpath = bip32.convert_bip32_strpath_to_intpath(bip32_path) bip32_path = bip32_path[2:] # cut off "m/" if len(bip32_intpath) >= 1: prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:] diff --git a/electrum/plugins/safe_t/clientbase.py b/electrum/plugins/safe_t/clientbase.py index 4878a7e63..9ff5361d3 100644 --- a/electrum/plugins/safe_t/clientbase.py +++ b/electrum/plugins/safe_t/clientbase.py @@ -6,7 +6,7 @@ from electrum.i18n import _ from electrum.util import UserCancelled from electrum.keystore import bip39_normalize_passphrase -from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 +from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath from electrum.logging import Logger from electrum.plugin import runs_in_hwd_thread from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase @@ -156,7 +156,7 @@ def timeout(self, cutoff): @staticmethod def expand_path(n): - return convert_bip32_path_to_list_of_uint32(n) + return convert_bip32_strpath_to_intpath(n) @runs_in_hwd_thread def cancel(self): diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index d3297bcfb..30ee4fdd0 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -5,7 +5,7 @@ from electrum.i18n import _ from electrum.util import UserCancelled, UserFacingException from electrum.keystore import bip39_normalize_passphrase -from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path +from electrum.bip32 import BIP32Node, convert_bip32_strpath_to_intpath as parse_path from electrum.logging import Logger from electrum.plugin import runs_in_hwd_thread from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 390a812fa..6170ea2d3 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -3,7 +3,7 @@ from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException -from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path +from electrum.bip32 import BIP32Node from electrum import descriptor from electrum import constants from electrum.i18n import _ diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index c3026a0d2..5699902c4 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -15,7 +15,7 @@ from electrum.segwit_addr import DecodedBech32 from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, - is_xpub, convert_bip32_path_to_list_of_uint32, + is_xpub, convert_bip32_strpath_to_intpath, normalize_bip32_derivation, is_all_public_derivation) from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS from electrum import ecc, crypto, constants @@ -760,7 +760,7 @@ class Test_xprv_xpub(ElectrumTestCase): def _do_test_bip32(self, seed: str, sequence: str): node = BIP32Node.from_rootseed(bfh(seed), xtype='standard') xprv, xpub = node.to_xprv(), node.to_xpub() - int_path = convert_bip32_path_to_list_of_uint32(sequence) + int_path = convert_bip32_strpath_to_intpath(sequence) for n in int_path: if n & bip32.BIP32_PRIME == 0: xpub2 = BIP32Node.from_xkey(xpub).subkey_at_public_derivation([n]).to_xpub() @@ -852,10 +852,10 @@ def test_is_bip32_derivation(self): self.assertFalse(is_bip32_derivation("m/q8462")) self.assertFalse(is_bip32_derivation("m/-8h")) - def test_convert_bip32_path_to_list_of_uint32(self): - self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_path_to_list_of_uint32("m/0/-1/1'")) - self.assertEqual([], convert_bip32_path_to_list_of_uint32("m/")) - self.assertEqual([2147483692, 2147488889, 221], convert_bip32_path_to_list_of_uint32("m/44'/5241h/221")) + def test_convert_bip32_strpath_to_intpath(self): + self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_strpath_to_intpath("m/0/-1/1'")) + self.assertEqual([], convert_bip32_strpath_to_intpath("m/")) + self.assertEqual([2147483692, 2147488889, 221], convert_bip32_strpath_to_intpath("m/44'/5241h/221")) def test_convert_bip32_intpath_to_strpath(self): self.assertEqual("m/0/1h/1h", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001])) diff --git a/electrum/wallet.py b/electrum/wallet.py index ac5aac422..17f083f21 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -50,7 +50,7 @@ from aiorpcx import timeout_after, TaskTimeout, ignore_after, run_in_thread from .i18n import _ -from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_path_to_list_of_uint32 +from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath from .crypto import sha256 from . import util from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore_exceptions, @@ -3298,7 +3298,7 @@ def derive_address(self, for_change: int, n: int) -> str: def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str: if isinstance(path, str): - path = convert_bip32_path_to_list_of_uint32(path) + path = convert_bip32_strpath_to_intpath(path) pk, compressed = self.keystore.get_private_key(path, password) txin_type = self.get_txin_type() # assumes no mixed-scripts in wallet return bitcoin.serialize_privkey(pk, compressed, txin_type) From a595102d5f33c1afe651fb26a9fb69c83da4332d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Mar 2023 15:26:07 +0100 Subject: [PATCH 0313/1143] qml: auto sign and broadcast for cancel txs too --- electrum/gui/qml/components/TxDetails.qml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 4965d5841..8c40c56fc 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -133,6 +133,7 @@ Pane { TextHighlightPane { Layout.fillWidth: true + Layout.topMargin: constants.paddingSmall Layout.columnSpan: 2 borderColor: constants.colorWarning visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel @@ -447,7 +448,7 @@ Pane { onTxaccepted: { root.rawtx = rbffeebumper.getNewTx() - if (Daemon.currentWallet.canSignWithoutCosigner) { + if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign(true) // close txdetails? } else { @@ -474,7 +475,7 @@ Pane { onTxaccepted: { // replaces parent tx with cpfp tx root.rawtx = cpfpfeebumper.getNewTx() - if (Daemon.currentWallet.canSignWithoutCosigner) { + if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign(true) // close txdetails? } else { @@ -500,7 +501,15 @@ Pane { onTxaccepted: { root.rawtx = txcanceller.getNewTx() - // TODO: sign & send when possible? + if (txdetails.wallet.canSignWithoutCosigner) { + txdetails.sign(true) + // close txdetails? + } else { + var dialog = app.messageDialog.createObject(app, { + text: qsTr('Cancel transaction created.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') + }) + dialog.open() + } } onClosed: destroy() } From 7e5ebf0484ceefc16420b5ac5951ac96e245084f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 10 Mar 2023 15:27:39 +0100 Subject: [PATCH 0314/1143] swap: wrap coros in tasks (req since python3.11) --- electrum/submarine_swaps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 0d86207da..d1c653ca3 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -453,7 +453,7 @@ async def wait_for_funding(swap): while swap.spending_txid is None: await asyncio.sleep(1) # initiate main payment - tasks = [self.lnworker.pay_invoice(invoice, attempts=10, channels=channels), wait_for_funding(swap)] + tasks = [asyncio.create_task(self.lnworker.pay_invoice(invoice, attempts=10, channels=channels)), asyncio.create_task(wait_for_funding(swap))] await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) success = swap.spending_txid is not None return success From f89a466d61af4580e6986f5f2cb7423621355741 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 10 Mar 2023 18:02:47 +0100 Subject: [PATCH 0315/1143] minor fix --- electrum/gui/qt/utxo_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index f605f6077..dc23805a0 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -239,7 +239,7 @@ def pay_to_clipboard_address(self, coins): outputs = [PartialTxOutput.from_address_and_value(addr, '!')] #self.clear_coincontrol() self.add_to_coincontrol(coins) - self.parent.send_tab.pay_onchain_dialog(coins, outputs) + self.parent.send_tab.pay_onchain_dialog(outputs) self.clear_coincontrol() def create_menu(self, position): From 1a0a52f9b62beba5f62af10530d0e07d0087a650 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Mar 2023 15:15:16 +0100 Subject: [PATCH 0316/1143] invoices and requests lists: move import/export menus into local toolbars --- electrum/gui/qt/invoice_list.py | 1 - electrum/gui/qt/main_window.py | 6 ------ electrum/gui/qt/receive_tab.py | 12 +++++++----- electrum/gui/qt/request_list.py | 1 - electrum/gui/qt/send_tab.py | 9 +++++++-- electrum/gui/qt/util.py | 16 ++++++++++++++++ 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 6a5375ae9..fa0251632 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -134,7 +134,6 @@ def update(self): def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) - self.send_tab.invoices_label.setVisible(b) def create_menu(self, position): wallet = self.wallet diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 9393f2f93..374e9a782 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -705,12 +705,6 @@ def init_menubar(self): contacts_menu.addAction(_("&New"), self.new_contact_dialog) contacts_menu.addAction(_("Import"), lambda: self.import_contacts()) contacts_menu.addAction(_("Export"), lambda: self.export_contacts()) - invoices_menu = wallet_menu.addMenu(_("Invoices")) - invoices_menu.addAction(_("Import"), lambda: self.import_invoices()) - invoices_menu.addAction(_("Export"), lambda: self.export_invoices()) - requests_menu = wallet_menu.addMenu(_("Requests")) - requests_menu.addAction(_("Import"), lambda: self.import_requests()) - requests_menu.addAction(_("Export"), lambda: self.export_requests()) wallet_menu.addSeparator() wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index fa012c0d4..8b660702c 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -168,12 +168,14 @@ def on_receive_swap(): self.receive_tabs.setSizePolicy(receive_tabs_sp) self.receive_tabs.setVisible(False) - self.receive_requests_label = QLabel(_('Receive queue')) - # with QDarkStyle, this label may partially cover the qrcode widget. - # setMaximumWidth prevents that - self.receive_requests_label.setMaximumWidth(400) from .request_list import RequestList self.request_list = RequestList(self) + self.toolbar = self.request_list.create_toolbar_with_menu( + _('Requests'), + [ + (_("Import requests"), self.window.import_requests), + (_("Export requests"), self.window.export_requests), + ]) # layout vbox_g = QVBoxLayout() @@ -188,7 +190,7 @@ def on_receive_swap(): vbox = QVBoxLayout(self) vbox.addLayout(hbox) vbox.addStretch() - vbox.addWidget(self.receive_requests_label) + vbox.addLayout(self.toolbar) vbox.addWidget(self.request_list) vbox.setStretchFactor(hbox, 40) vbox.setStretchFactor(self.request_list, 60) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 49cead064..dff11de31 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -167,7 +167,6 @@ def update(self): def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) - self.receive_tab.receive_requests_label.setVisible(b) if not b: # list got hidden, so selected item should also be cleared: self.item_changed(None) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 99dcbc015..7bfe1902a 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -146,9 +146,14 @@ def reset_max(text): self.set_onchain(False) - self.invoices_label = QLabel(_('Send queue')) from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) + self.toolbar = self.invoice_list.create_toolbar_with_menu( + _('Invoices'), + [ + (_("Import invoices"), self.window.import_invoices), + (_("Export invoices"), self.window.export_invoices), + ]) vbox0 = QVBoxLayout() vbox0.addLayout(grid) @@ -159,7 +164,7 @@ def reset_max(text): vbox = QVBoxLayout(self) vbox.addLayout(hbox) vbox.addStretch(1) - vbox.addWidget(self.invoices_label) + vbox.addLayout(self.toolbar) vbox.addWidget(self.invoice_list) vbox.setStretchFactor(self.invoice_list, 1000) self.searchable_list = self.invoice_list diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 677e4f2b7..259e97c6e 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -756,6 +756,22 @@ def create_toolbar(self, config=None): hbox.addWidget(hide_button) return hbox + def create_toolbar_with_menu(self, title, menu_items): + menu = QMenu() + menu.setToolTipsVisible(True) + for k, v in menu_items: + menu.addAction(k, v) + toolbar_button = QToolButton() + toolbar_button.setIcon(read_QIcon("preferences.png")) + toolbar_button.setMenu(menu) + toolbar_button.setPopupMode(QToolButton.InstantPopup) + toolbar_button.setFocusPolicy(Qt.NoFocus) + toolbar = QHBoxLayout() + toolbar.addWidget(QLabel(title)) + toolbar.addStretch() + toolbar.addWidget(toolbar_button) + return toolbar + def save_toolbar_state(self, state, config): pass # implemented in subclasses From c595df39721dfd3f6be118bcbc823039510f2067 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Mar 2023 15:41:13 +0100 Subject: [PATCH 0317/1143] Qt: call create_toolbar in create_list_tab --- electrum/gui/qt/address_list.py | 3 +++ electrum/gui/qt/channels_list.py | 2 +- electrum/gui/qt/history_list.py | 3 +++ electrum/gui/qt/main_window.py | 16 +++++++--------- electrum/gui/qt/util.py | 5 ++++- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 7d200758e..79e63a1ae 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -108,6 +108,9 @@ def __init__(self, parent): self.update() self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder) + def create_toolbar(self, config): + return self.create_toolbar_with_buttons(config) + def get_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index de3a5ba66..100e37b01 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -353,7 +353,7 @@ def update_swap_button(self, lnworker: LNWallet): else: self.swap_button.setEnabled(False) - def get_toolbar(self): + def create_toolbar(self, config): h = QHBoxLayout() self.can_send_label = QLabel('') h.addWidget(self.can_send_label) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 9f38e70fc..b1d7dd239 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -519,6 +519,9 @@ def on_combo(self, x): self.end_button.setText(_('To') + ' ' + self.format_date(self.end_date)) self.hide_rows() + def create_toolbar(self, config): + return self.create_toolbar_with_buttons(config) + def create_toolbar_buttons(self): self.period_combo = QComboBox() self.start_button = QPushButton('-') diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 374e9a782..5dbf657e6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1040,16 +1040,14 @@ def refresh_tabs(self, wallet=None): def create_channels_tab(self): self.channels_list = ChannelsList(self) - t = self.channels_list.get_toolbar() - return self.create_list_tab(self.channels_list, t) + return self.create_list_tab(self.channels_list) def create_history_tab(self): self.history_model = HistoryModel(self) self.history_list = l = HistoryList(self, self.history_model) self.history_model.set_view(self.history_list) l.searchable_list = l - toolbar = l.create_toolbar(self.config) - tab = self.create_list_tab(l, toolbar) + tab = self.create_list_tab(self.history_list) toolbar_shown = bool(self.config.get('show_toolbar_history', False)) l.show_toolbar(toolbar_shown) return tab @@ -1332,13 +1330,14 @@ def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: boo self.utxo_list.refresh_all() self.utxo_list.selectionModel().clearSelection() - def create_list_tab(self, l, toolbar=None): + def create_list_tab(self, l): w = QWidget() w.searchable_list = l vbox = QVBoxLayout() w.setLayout(vbox) #vbox.setContentsMargins(0, 0, 0, 0) #vbox.setSpacing(0) + toolbar = l.create_toolbar(self.config) if toolbar: vbox.addLayout(toolbar) vbox.addWidget(l) @@ -1346,11 +1345,10 @@ def create_list_tab(self, l, toolbar=None): def create_addresses_tab(self): from .address_list import AddressList - self.address_list = l = AddressList(self) - toolbar = l.create_toolbar(self.config) - tab = self.create_list_tab(l, toolbar) + self.address_list = AddressList(self) + tab = self.create_list_tab(self.address_list) toolbar_shown = bool(self.config.get('show_toolbar_addresses', False)) - l.show_toolbar(toolbar_shown) + self.address_list.show_toolbar(toolbar_shown) return tab def create_utxo_tab(self): diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 259e97c6e..5b09741a8 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -742,7 +742,10 @@ def hide_rows(self): for row in range(self.model().rowCount()): self.hide_row(row) - def create_toolbar(self, config=None): + def create_toolbar(self, config): + return + + def create_toolbar_with_buttons(self, config=None): hbox = QHBoxLayout() buttons = self.get_toolbar_buttons() for b in buttons: From d6a65a06a701935a185b37dfe7a1db062cc003e6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Mar 2023 17:47:01 +0100 Subject: [PATCH 0318/1143] Qt: move remaining menu items that are tab specific to tab toolbars: history, addresses, contacts --- electrum/gui/qt/address_list.py | 10 +++++---- electrum/gui/qt/contact_list.py | 14 ++++++++----- electrum/gui/qt/history_list.py | 37 +++++++++++++++++++-------------- electrum/gui/qt/main_window.py | 15 ------------- electrum/gui/qt/util.py | 14 ++----------- 5 files changed, 38 insertions(+), 52 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 79e63a1ae..ab9c5ca65 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -109,7 +109,12 @@ def __init__(self, parent): self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder) def create_toolbar(self, config): - return self.create_toolbar_with_buttons(config) + toolbar = self.create_toolbar_with_menu('', [ + (_("&Filter"), lambda: self.toggle_toolbar(self.config)), + ]) + hbox = self.create_toolbar_buttons() + toolbar.insertLayout(1, hbox) + return toolbar def get_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button @@ -119,9 +124,6 @@ def on_hide_toolbar(self): self.show_used = AddressUsageStateFilter.ALL # type: AddressUsageStateFilter self.update() - def save_toolbar_state(self, state, config): - config.set_key('show_toolbar_addresses', state) - def refresh_headers(self): fx = self.parent.fx if fx and fx.get_fiat_address_config(): diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 02fdc621d..33cc23029 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -75,11 +75,7 @@ def create_menu(self, position): for s_idx in self.selected_in_column(self.Columns.NAME): sel_key = self.model().itemFromIndex(s_idx).data(self.ROLE_CONTACT_KEY) selected_keys.append(sel_key) - if not selected_keys or not idx.isValid(): - menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) - menu.addAction(_("Import file"), lambda: self.parent.import_contacts()) - menu.addAction(_("Export file"), lambda: self.parent.export_contacts()) - else: + if selected_keys and idx.isValid(): column_title = self.model().horizontalHeaderItem(column).text() column_data = '\n'.join(self.model().itemFromIndex(s_idx).text() for s_idx in self.selected_in_column(column)) @@ -131,3 +127,11 @@ def get_edit_key_from_coordinate(self, row, col): if col != self.Columns.NAME: return None return self.get_role_data_from_coordinate(row, col, role=self.ROLE_CONTACT_KEY) + + def create_toolbar(self, config): + toolbar = self.create_toolbar_with_menu('', [ + (_("&New contact"), self.parent.new_contact_dialog), + (_("Import"), lambda: self.parent.import_contacts()), + (_("Export"), lambda: self.parent.export_contacts()), + ]) + return toolbar diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index b1d7dd239..eff2d7a40 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -483,7 +483,15 @@ def __init__(self, parent, model: HistoryModel): self.start_date = None self.end_date = None self.years = [] - self.create_toolbar_buttons() + self.period_combo = QComboBox() + self.start_button = QPushButton('-') + self.start_button.pressed.connect(self.select_start_date) + self.start_button.setEnabled(False) + self.end_button = QPushButton('-') + self.end_button.pressed.connect(self.select_end_date) + self.end_button.setEnabled(False) + self.period_combo.addItems([_('All'), _('Custom')]) + self.period_combo.activated.connect(self.on_combo) self.wallet = self.parent.wallet # type: Abstract_Wallet self.sortByColumn(HistoryColumns.STATUS, Qt.AscendingOrder) self.setRootIsDecorated(True) @@ -520,18 +528,18 @@ def on_combo(self, x): self.hide_rows() def create_toolbar(self, config): - return self.create_toolbar_with_buttons(config) - - def create_toolbar_buttons(self): - self.period_combo = QComboBox() - self.start_button = QPushButton('-') - self.start_button.pressed.connect(self.select_start_date) - self.start_button.setEnabled(False) - self.end_button = QPushButton('-') - self.end_button.pressed.connect(self.select_end_date) - self.end_button.setEnabled(False) - self.period_combo.addItems([_('All'), _('Custom')]) - self.period_combo.activated.connect(self.on_combo) + toolbar = self.create_toolbar_with_menu('', [ + (_("&Filter Period"), lambda: self.toggle_toolbar(self.config)), + (_("&Summary"), self.show_summary), + (_("&Plot"), self.plot_history_dialog), + (_("&Export"), self.export_history_dialog), + ]) + hbox = self.create_toolbar_buttons() + toolbar.insertLayout(1, hbox) + return toolbar + + def toggle_filter(self): + pass def get_toolbar_buttons(self): return self.period_combo, self.start_button, self.end_button @@ -541,9 +549,6 @@ def on_hide_toolbar(self): self.end_date = None self.hide_rows() - def save_toolbar_state(self, state, config): - config.set_key('show_toolbar_history', state) - def select_start_date(self): self.start_date = self.select_date(self.start_button) self.hide_rows() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 5dbf657e6..8b7e6ed64 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -691,20 +691,9 @@ def init_menubar(self): self.import_address_menu = wallet_menu.addAction(_("Import addresses"), self.import_addresses) wallet_menu.addSeparator() - addresses_menu = wallet_menu.addMenu(_("&Addresses")) - addresses_menu.addAction(_("&Filter"), lambda: self.address_list.toggle_toolbar(self.config)) labels_menu = wallet_menu.addMenu(_("&Labels")) labels_menu.addAction(_("&Import"), self.do_import_labels) labels_menu.addAction(_("&Export"), self.do_export_labels) - history_menu = wallet_menu.addMenu(_("&History")) - history_menu.addAction(_("&Filter"), lambda: self.history_list.toggle_toolbar(self.config)) - history_menu.addAction(_("&Summary"), self.history_list.show_summary) - history_menu.addAction(_("&Plot"), self.history_list.plot_history_dialog) - history_menu.addAction(_("&Export"), self.history_list.export_history_dialog) - contacts_menu = wallet_menu.addMenu(_("Contacts")) - contacts_menu.addAction(_("&New"), self.new_contact_dialog) - contacts_menu.addAction(_("Import"), lambda: self.import_contacts()) - contacts_menu.addAction(_("Export"), lambda: self.export_contacts()) wallet_menu.addSeparator() wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) @@ -1048,8 +1037,6 @@ def create_history_tab(self): self.history_model.set_view(self.history_list) l.searchable_list = l tab = self.create_list_tab(self.history_list) - toolbar_shown = bool(self.config.get('show_toolbar_history', False)) - l.show_toolbar(toolbar_shown) return tab def show_address(self, addr: str, *, parent: QWidget = None): @@ -1347,8 +1334,6 @@ def create_addresses_tab(self): from .address_list import AddressList self.address_list = AddressList(self) tab = self.create_list_tab(self.address_list) - toolbar_shown = bool(self.config.get('show_toolbar_addresses', False)) - self.address_list.show_toolbar(toolbar_shown) return tab def create_utxo_tab(self): diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 5b09741a8..ddcadadfa 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -745,18 +745,13 @@ def hide_rows(self): def create_toolbar(self, config): return - def create_toolbar_with_buttons(self, config=None): + def create_toolbar_buttons(self): hbox = QHBoxLayout() buttons = self.get_toolbar_buttons() for b in buttons: b.setVisible(False) hbox.addWidget(b) - hide_button = QPushButton('x') - hide_button.setVisible(False) - hide_button.pressed.connect(lambda: self.show_toolbar(False, config)) - self.toolbar_buttons = buttons + (hide_button,) - hbox.addStretch() - hbox.addWidget(hide_button) + self.toolbar_buttons = buttons return hbox def create_toolbar_with_menu(self, title, menu_items): @@ -775,15 +770,10 @@ def create_toolbar_with_menu(self, title, menu_items): toolbar.addWidget(toolbar_button) return toolbar - def save_toolbar_state(self, state, config): - pass # implemented in subclasses - def show_toolbar(self, state, config=None): if state == self.toolbar_shown: return self.toolbar_shown = state - if config: - self.save_toolbar_state(state, config) for b in self.toolbar_buttons: b.setVisible(state) if not state: From 5ad4023e7d24bc8e24609145e52c6f7223047d41 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 11 Mar 2023 18:08:00 +0100 Subject: [PATCH 0319/1143] restore invoices_label and requests_label. move paytomany and toggle_qr_window from main menu to toolbar --- electrum/gui/qt/invoice_list.py | 1 + electrum/gui/qt/main_window.py | 3 --- electrum/gui/qt/receive_tab.py | 10 ++++++++-- electrum/gui/qt/request_list.py | 1 + electrum/gui/qt/send_tab.py | 7 +++++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index fa0251632..6a5375ae9 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -134,6 +134,7 @@ def update(self): def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) + self.send_tab.invoices_label.setVisible(b) def create_menu(self, position): wallet = self.wallet diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8b7e6ed64..4545b2353 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -730,9 +730,6 @@ def add_toggle_action(view_menu, tab): tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message) tools_menu.addSeparator() - paytomany_menu = tools_menu.addAction(_("&Pay to many"), self.send_tab.paytomany) - tools_menu.addAction(_("&Show QR code in separate window"), self.toggle_qr_window) - raw_transaction_menu = tools_menu.addMenu(_("&Load transaction")) raw_transaction_menu.addAction(_("&From file"), self.do_process_from_file) raw_transaction_menu.addAction(_("&From text"), self.do_process_from_text) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 8b660702c..ed28ac19f 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -168,11 +168,16 @@ def on_receive_swap(): self.receive_tabs.setSizePolicy(receive_tabs_sp) self.receive_tabs.setVisible(False) + self.receive_requests_label = QLabel(_('Requests')) + # with QDarkStyle, this label may partially cover the qrcode widget. + # setMaximumWidth prevents that + self.receive_requests_label.setMaximumWidth(400) from .request_list import RequestList self.request_list = RequestList(self) self.toolbar = self.request_list.create_toolbar_with_menu( - _('Requests'), + '', [ + (_("Toggle QR code window"), self.window.toggle_qr_window), (_("Import requests"), self.window.import_requests), (_("Export requests"), self.window.export_requests), ]) @@ -188,9 +193,10 @@ def on_receive_swap(): self.searchable_list = self.request_list vbox = QVBoxLayout(self) + vbox.addLayout(self.toolbar) vbox.addLayout(hbox) vbox.addStretch() - vbox.addLayout(self.toolbar) + vbox.addWidget(self.receive_requests_label) vbox.addWidget(self.request_list) vbox.setStretchFactor(hbox, 40) vbox.setStretchFactor(self.request_list, 60) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index dff11de31..49cead064 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -167,6 +167,7 @@ def update(self): def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) + self.receive_tab.receive_requests_label.setVisible(b) if not b: # list got hidden, so selected item should also be cleared: self.item_changed(None) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 7bfe1902a..1280c6e80 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -146,11 +146,13 @@ def reset_max(text): self.set_onchain(False) + self.invoices_label = QLabel(_('Invoices')) from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) self.toolbar = self.invoice_list.create_toolbar_with_menu( - _('Invoices'), + '', [ + (_("&Pay to many"), self.paytomany), (_("Import invoices"), self.window.import_invoices), (_("Export invoices"), self.window.export_invoices), ]) @@ -162,9 +164,10 @@ def reset_max(text): hbox.addStretch(1) vbox = QVBoxLayout(self) + vbox.addLayout(self.toolbar) vbox.addLayout(hbox) vbox.addStretch(1) - vbox.addLayout(self.toolbar) + vbox.addWidget(self.invoices_label) vbox.addWidget(self.invoice_list) vbox.setStretchFactor(self.invoice_list, 1000) self.searchable_list = self.invoice_list From 6a049a335aa923e89b9963f4fcc5f6a0cfc5235e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 11 Mar 2023 22:57:45 +0000 Subject: [PATCH 0320/1143] transaction: run validate_data before setting .utxo, not after Feels safer. --- electrum/transaction.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 854e80b43..d9ad81fa0 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1207,8 +1207,8 @@ def utxo(self, tx: Optional[Transaction]): # 'utxo' field in PSBT cannot be another PSBT: if not tx.is_complete(): return + self.validate_data(utxo=tx) self._utxo = tx - self.validate_data() @property def witness_utxo(self): @@ -1216,8 +1216,8 @@ def witness_utxo(self): @witness_utxo.setter def witness_utxo(self, value: Optional[TxOutput]): + self.validate_data(witness_utxo=value) self._witness_utxo = value - self.validate_data() @property def pubkeys(self) -> Set[bytes]: @@ -1270,20 +1270,29 @@ def from_txin(cls, txin: TxInput, *, strip_witness: bool = True) -> 'PartialTxIn is_coinbase_output=txin.is_coinbase_output()) return res - def validate_data(self, *, for_signing=False) -> None: - if self.utxo: - if self.prevout.txid.hex() != self.utxo.txid(): + def validate_data( + self, + *, + for_signing=False, + # allow passing provisional fields for 'self', before setting them: + utxo: Optional[Transaction] = None, + witness_utxo: Optional[TxOutput] = None, + ) -> None: + utxo = utxo or self.utxo + witness_utxo = witness_utxo or self.witness_utxo + if utxo: + if self.prevout.txid.hex() != utxo.txid(): raise PSBTInputConsistencyFailure(f"PSBT input validation: " f"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout") - if self.witness_utxo: - if self.utxo.outputs()[self.prevout.out_idx] != self.witness_utxo: + if witness_utxo: + if utxo.outputs()[self.prevout.out_idx] != witness_utxo: raise PSBTInputConsistencyFailure(f"PSBT input validation: " f"If both non-witness UTXO and witness UTXO are provided, they must be consistent") # The following test is disabled, so we are willing to sign non-segwit inputs # without verifying the input amount. This means, given a maliciously modified PSBT, # for non-segwit inputs, we might end up burning coins as miner fees. if for_signing and False: - if not self.is_segwit() and self.witness_utxo: + if not self.is_segwit() and witness_utxo: raise PSBTInputConsistencyFailure(f"PSBT input validation: " f"If a witness UTXO is provided, no non-witness signature may be created") if self.redeem_script and self.address: From 9439261e42eb9efaa35db16735b8421ff17e7b0b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 11 Mar 2023 18:32:38 +0000 Subject: [PATCH 0321/1143] network: fix bug in best_effort_reliable self.interface might get set to None after decorator checks it but before func gets scheduled: 125.04 | E | asyncio | Task exception was never retrieved future: .add_info_to_txin() done, defined at ...\electrum\electrum\transaction.py:976> exception=AttributeError("'NoneType' object has no attribute 'get_transaction'")> Traceback (most recent call last): File "...\electrum\electrum\transaction.py", line 980, in add_info_to_txin await txin.add_info_from_network(network=network, ignore_network_issues=ignore_network_issues) File "...\electrum\electrum\transaction.py", line 375, in add_info_from_network self.utxo = await fetch_from_network(txid=self.prevout.txid.hex()) File "...\electrum\electrum\transaction.py", line 362, in fetch_from_network raw_tx = await network.get_transaction(txid, timeout=10) File "...\electrum\electrum\network.py", line 866, in make_reliable_wrapper async with OldTaskGroup(wait=any) as group: File "...\aiorpcX\aiorpcx\curio.py", line 304, in __aexit__ await self.join() File "...\electrum\electrum\util.py", line 1410, in join self.completed.result() File "...\electrum\electrum\network.py", line 889, in wrapper return await func(self, *args, **kwargs) File "...\electrum\electrum\network.py", line 1114, in get_transaction return await self.interface.get_transaction(tx_hash=tx_hash, timeout=timeout) AttributeError: 'NoneType' object has no attribute 'get_transaction' --- electrum/network.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/electrum/network.py b/electrum/network.py index 5e531208d..bc2057062 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -894,10 +894,14 @@ async def wrapper(self, *args, **kwargs): @best_effort_reliable @catch_server_exceptions async def get_merkle_for_transaction(self, tx_hash: str, tx_height: int) -> dict: + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.get_merkle_for_transaction(tx_hash=tx_hash, tx_height=tx_height) @best_effort_reliable async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None: + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() if timeout is None: timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent) try: @@ -1106,31 +1110,43 @@ def sanitize_tx_broadcast_response(server_msg) -> str: @best_effort_reliable @catch_server_exceptions async def request_chunk(self, height: int, tip=None, *, can_return_early=False): + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.request_chunk(height, tip=tip, can_return_early=can_return_early) @best_effort_reliable @catch_server_exceptions async def get_transaction(self, tx_hash: str, *, timeout=None) -> str: + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.get_transaction(tx_hash=tx_hash, timeout=timeout) @best_effort_reliable @catch_server_exceptions async def get_history_for_scripthash(self, sh: str) -> List[dict]: + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.get_history_for_scripthash(sh) @best_effort_reliable @catch_server_exceptions async def listunspent_for_scripthash(self, sh: str) -> List[dict]: + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.listunspent_for_scripthash(sh) @best_effort_reliable @catch_server_exceptions async def get_balance_for_scripthash(self, sh: str) -> dict: + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.get_balance_for_scripthash(sh) @best_effort_reliable @catch_server_exceptions async def get_txid_from_txpos(self, tx_height, tx_pos, merkle): + if self.interface is None: # handled by best_effort_reliable + raise RequestTimedOut() return await self.interface.get_txid_from_txpos(tx_height, tx_pos, merkle) def blockchain(self) -> Blockchain: From 81772faf6c5a1938cb95e327e84389635500162a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 10 Mar 2023 21:17:05 +0000 Subject: [PATCH 0322/1143] wallet: add_input_info to no longer do network requests - wallet.add_input_info() previously had a fallback to download parent prev txs from the network (after a lookup in wallet.db failed). wallet.add_input_info() is not async, so the network request cannot be done cleanly there and was really just a hack. - tx.add_info_from_wallet() calls wallet.add_input_info() on each txin, in which case these network requests were done sequentially, not concurrently - the network part of wallet.add_input_info() is now split out into new method: txin.add_info_from_network() - in addition to tx.add_info_from_wallet(), there is now also tx.add_info_from_network() - callers of old tx.add_info_from_wallet() should now called either - tx.add_info_from_wallet(), then tx.add_info_from_network(), preferably in that order - tx.add_info_from_wallet() alone is sufficient if the tx is complete, or typically when not in a signing context - callers of wallet.bump_fee and wallet.dscancel are now expected to have already called tx.add_info_from_network(), as it cannot be done in a non-async context (but for the common case of all-inputs-are-ismine, bump_fee/dscancel should work regardless) - PartialTxInput.utxo was moved to the baseclass, TxInput.utxo --- electrum/address_synchronizer.py | 5 +- electrum/commands.py | 2 + electrum/gui/kivy/uix/dialogs/tx_dialog.py | 27 +--- electrum/gui/qml/qetransactionlistmodel.py | 10 +- electrum/gui/qml/qetxdetails.py | 12 +- electrum/gui/qml/qetxfinalizer.py | 40 +---- electrum/gui/qt/main_window.py | 20 +-- electrum/gui/qt/transaction_dialog.py | 17 +- electrum/tests/test_wallet_vertical.py | 93 ++++++----- electrum/transaction.py | 174 +++++++++++++++------ electrum/wallet.py | 65 +++----- 11 files changed, 243 insertions(+), 222 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 7dcbe5fd7..c8c261bdd 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -138,9 +138,8 @@ def get_address_history_len(self, addr: str) -> int: return len(self._history_local.get(addr, ())) def get_txin_address(self, txin: TxInput) -> Optional[str]: - if isinstance(txin, PartialTxInput): - if txin.address: - return txin.address + if txin.address: + return txin.address prevout_hash = txin.prevout.txid.hex() prevout_n = txin.prevout.out_idx for addr in self.db.get_txo_addresses(prevout_hash): diff --git a/electrum/commands.py b/electrum/commands.py index 33073bb39..91c42a2c8 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -762,6 +762,8 @@ async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=Fals coins = wallet.get_spendable_coins(None) if domain_coins is not None: coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] + tx.add_info_from_wallet(wallet) + await tx.add_info_from_network(self.network) new_tx = wallet.bump_fee( tx=tx, txid=tx.txid(), diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index c10a9aac8..f61d2685b 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -16,7 +16,7 @@ from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.wallet import CannotBumpFee, CannotCPFP, CannotDoubleSpendTx from electrum.transaction import Transaction, PartialTransaction -from electrum.network import NetworkException +from electrum.network import NetworkException, Network from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.util import address_colors @@ -120,19 +120,21 @@ class TxDialog(Factory.Popup): - def __init__(self, app, tx): + def __init__(self, app, tx: Transaction): Factory.Popup.__init__(self) self.app = app # type: ElectrumWindow self.wallet = self.app.wallet - self.tx = tx # type: Transaction + self.tx = tx self.config = self.app.electrum_config # If the wallet can populate the inputs with more info, do it now. # As a result, e.g. we might learn an imported address tx is segwit, # or that a beyond-gap-limit address is is_mine. # note: this might fetch prev txs over the network. - # note: this is a no-op for complete txs tx.add_info_from_wallet(self.wallet) + if not tx.is_complete() and tx.is_missing_info_from_network(): + Network.run_from_another_thread( + tx.add_info_from_network(self.wallet.network)) # FIXME is this needed?... def on_open(self): self.update() @@ -201,19 +203,6 @@ def update_action_dropdown(self): ) action_dropdown.update(options=options) - def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool: - """Returns whether successful.""" - # note side-effect: tx is being mutated - assert isinstance(tx, PartialTransaction) - try: - # note: this might download input utxos over network - # FIXME network code in gui thread... - tx.add_info_from_wallet(self.wallet, ignore_network_issues=False) - except NetworkException as e: - self.app.show_error(repr(e)) - return False - return True - def do_rbf(self): from .bump_fee_dialog import BumpFeeDialog tx = self.tx @@ -221,7 +210,7 @@ def do_rbf(self): assert txid if not isinstance(tx, PartialTransaction): tx = PartialTransaction.from_tx(tx) - if not self._add_info_to_tx_from_wallet_and_network(tx): + if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.app.show_error): return fee = tx.get_fee() assert fee is not None @@ -295,7 +284,7 @@ def do_dscancel(self): assert txid if not isinstance(tx, PartialTransaction): tx = PartialTransaction.from_tx(tx) - if not self._add_info_to_tx_from_wallet_and_network(tx): + if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.app.show_error): return fee = tx.get_fee() assert fee is not None diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 33f2f4bde..9081af66d 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex @@ -9,6 +10,10 @@ from .qetypes import QEAmount from .util import QtEventListener, qt_event_listener +if TYPE_CHECKING: + from electrum.wallet import Abstract_Wallet + + class QETransactionListModel(QAbstractListModel, QtEventListener): _logger = get_logger(__name__) @@ -22,7 +27,7 @@ class QETransactionListModel(QAbstractListModel, QtEventListener): requestRefresh = pyqtSignal() - def __init__(self, wallet, parent=None, *, onchain_domain=None, include_lightning=True): + def __init__(self, wallet: 'Abstract_Wallet', parent=None, *, onchain_domain=None, include_lightning=True): super().__init__(parent) self.wallet = wallet self.onchain_domain = onchain_domain @@ -101,7 +106,8 @@ def tx_to_model(self, tx): item['balance'] = QEAmount(amount_sat=item['balance'].value) if 'txid' in item: - tx = self.wallet.get_input_tx(item['txid']) + tx = self.wallet.db.get_transaction(item['txid']) + assert tx is not None item['complete'] = tx.is_complete() # newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 8da45ca64..6a3775598 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -1,9 +1,12 @@ +from typing import Optional + from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.i18n import _ from electrum.logging import get_logger from electrum.util import format_time, AddTransactionException from electrum.transaction import tx_from_any +from electrum.network import Network from .qewallet import QEWallet from .qetypes import QEAmount @@ -23,7 +26,7 @@ def __init__(self, parent=None): self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) - self._wallet = None + self._wallet = None # type: Optional[QEWallet] self._txid = '' self._rawtx = '' self._label = '' @@ -229,13 +232,16 @@ def update(self): return if not self._rawtx: - # abusing get_input_tx to get tx from txid - self._tx = self._wallet.wallet.get_input_tx(self._txid) + self._tx = self._wallet.wallet.db.get_transaction(self._txid) + assert self._tx is not None #self._logger.debug(repr(self._tx.to_json())) self._logger.debug('adding info from wallet') self._tx.add_info_from_wallet(self._wallet.wallet) + if not self._tx.is_complete() and self._tx.is_missing_info_from_network(): + Network.run_from_another_thread( + self._tx.add_info_from_network(self._wallet.wallet.network)) # FIXME is this needed?... self._inputs = list(map(lambda x: x.to_json(), self._tx.inputs())) self._outputs = list(map(lambda x: { diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index bf5bda24f..2a53f2416 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -494,7 +494,7 @@ def bumpMethod(self, bumpmethod): def get_tx(self): assert self._txid - self._orig_tx = self._wallet.wallet.get_input_tx(self._txid) + self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid) assert self._orig_tx if self._wallet.wallet.get_swap_by_funding_tx(self._orig_tx): @@ -504,7 +504,7 @@ def get_tx(self): if not isinstance(self._orig_tx, PartialTransaction): self._orig_tx = PartialTransaction.from_tx(self._orig_tx) - if not self._add_info_to_tx_from_wallet_and_network(self._orig_tx): + if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error): return self.update_from_tx(self._orig_tx) @@ -513,21 +513,6 @@ def get_tx(self): self.oldfeeRate = self.feeRate self.update() - # TODO: duplicated from kivy gui, candidate for moving into backend wallet - def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool: - """Returns whether successful.""" - # note side-effect: tx is being mutated - assert isinstance(tx, PartialTransaction) - try: - # note: this might download input utxos over network - # FIXME network code in gui thread... - tx.add_info_from_wallet(self._wallet.wallet, ignore_network_issues=False) - except NetworkException as e: - # self.app.show_error(repr(e)) - self._logger.error(repr(e)) - return False - return True - def update(self): if not self._txid: # not initialized yet @@ -616,13 +601,13 @@ def oldfeeRate(self, oldfeerate): def get_tx(self): assert self._txid - self._orig_tx = self._wallet.wallet.get_input_tx(self._txid) + self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid) assert self._orig_tx if not isinstance(self._orig_tx, PartialTransaction): self._orig_tx = PartialTransaction.from_tx(self._orig_tx) - if not self._add_info_to_tx_from_wallet_and_network(self._orig_tx): + if not self._orig_tx.add_info_from_wallet_and_network(wallet=self._wallet.wallet, show_error=self._logger.error): return self.update_from_tx(self._orig_tx) @@ -631,21 +616,6 @@ def get_tx(self): self.oldfeeRate = self.feeRate self.update() - # TODO: duplicated from kivy gui, candidate for moving into backend wallet - def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool: - """Returns whether successful.""" - # note side-effect: tx is being mutated - assert isinstance(tx, PartialTransaction) - try: - # note: this might download input utxos over network - # FIXME network code in gui thread... - tx.add_info_from_wallet(self._wallet.wallet, ignore_network_issues=False) - except NetworkException as e: - # self.app.show_error(repr(e)) - self._logger.error(repr(e)) - return False - return True - def update(self): if not self._txid: # not initialized yet @@ -757,7 +727,7 @@ def totalSize(self): def get_tx(self): assert self._txid - self._parent_tx = self._wallet.wallet.get_input_tx(self._txid) + self._parent_tx = self._wallet.wallet.db.get_transaction(self._txid) assert self._parent_tx if isinstance(self._parent_tx, PartialTransaction): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 4545b2353..0de92a984 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2668,27 +2668,11 @@ def on_rate(dyn, pos, fee_rate): return self.show_transaction(new_tx) - def _add_info_to_tx_from_wallet_and_network(self, tx: PartialTransaction) -> bool: - """Returns whether successful.""" - # note side-effect: tx is being mutated - assert isinstance(tx, PartialTransaction) - try: - # note: this might download input utxos over network - BlockingWaitingDialog( - self, - _("Adding info to tx, from wallet and network..."), - lambda: tx.add_info_from_wallet(self.wallet, ignore_network_issues=False), - ) - except NetworkException as e: - self.show_error(repr(e)) - return False - return True - def bump_fee_dialog(self, tx: Transaction): txid = tx.txid() if not isinstance(tx, PartialTransaction): tx = PartialTransaction.from_tx(tx) - if not self._add_info_to_tx_from_wallet_and_network(tx): + if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.show_error): return d = BumpFeeDialog(main_window=self, tx=tx, txid=txid) d.run() @@ -2697,7 +2681,7 @@ def dscancel_dialog(self, tx: Transaction): txid = tx.txid() if not isinstance(tx, PartialTransaction): tx = PartialTransaction.from_tx(tx) - if not self._add_info_to_tx_from_wallet_and_network(tx): + if not tx.add_info_from_wallet_and_network(wallet=self.wallet, show_error=self.show_error): return d = DSCancelDialog(main_window=self, tx=tx, txid=txid) d.run() diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 14c53809e..d893165ff 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -51,6 +51,7 @@ from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint from electrum.logging import get_logger from electrum.util import ShortID +from electrum.network import Network from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog, @@ -477,11 +478,14 @@ def set_tx(self, tx: 'Transaction'): # As a result, e.g. we might learn an imported address tx is segwit, # or that a beyond-gap-limit address is is_mine. # note: this might fetch prev txs over the network. - BlockingWaitingDialog( - self, - _("Adding info to tx, from wallet and network..."), - lambda: tx.add_info_from_wallet(self.wallet), - ) + tx.add_info_from_wallet(self.wallet) + # TODO fetch prev txs for any tx; guarded with a config key + if not tx.is_complete() and tx.is_missing_info_from_network(): + BlockingWaitingDialog( + self, + _("Adding info to tx, from network..."), + lambda: Network.run_from_another_thread(tx.add_info_from_network(self.wallet.network)), + ) def do_broadcast(self): self.main_window.push_top_level_window(self) @@ -535,7 +539,8 @@ def _gettx_for_hardware_device(self) -> PartialTransaction: if not isinstance(self.tx, PartialTransaction): raise Exception("Can only export partial transactions for hardware device.") tx = copy.deepcopy(self.tx) - tx.prepare_for_export_for_hardware_device(self.wallet) + Network.run_from_another_thread( + tx.prepare_for_export_for_hardware_device(self.wallet)) return tx def copy_to_clipboard(self, *, tx: Transaction = None): diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 9918fa4bb..54c547359 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1047,61 +1047,61 @@ def __enter__(self): for simulate_moving_txs in (False, True): with TmpConfig() as config: with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2pkh_when_there_is_a_change_address( + await self._bump_fee_p2pkh_when_there_is_a_change_address( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_when_there_is_a_change_address( + await self._bump_fee_p2wpkh_when_there_is_a_change_address( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv( + await self._bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_user_sends_max( + await self._bump_fee_when_user_sends_max( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_when_new_inputs_need_to_be_added", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_new_inputs_need_to_be_added( + await self._bump_fee_when_new_inputs_need_to_be_added( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address( + await self._bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_rbf_batching", simulate_moving_txs=simulate_moving_txs): - self._rbf_batching( + await self._rbf_batching( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all( + await self._bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine( + await self._bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_decrease_payment( + await self._bump_fee_p2wpkh_decrease_payment( simulate_moving_txs=simulate_moving_txs, config=config) with TmpConfig() as config: with self.subTest(msg="_bump_fee_p2wpkh_decrease_payment_batch", simulate_moving_txs=simulate_moving_txs): - self._bump_fee_p2wpkh_decrease_payment_batch( + await self._bump_fee_p2wpkh_decrease_payment_batch( simulate_moving_txs=simulate_moving_txs, config=config) - def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): + async def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean', config=config) @@ -1165,7 +1165,7 @@ def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 7484320, 0), wallet.get_balance()) - def _bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(self, *, simulate_moving_txs, config): + async def _bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(self, *, simulate_moving_txs, config): """This tests a regression where sometimes we created a replacement tx that spent from the original (which is clearly invalid). """ @@ -1207,7 +1207,7 @@ def _bump_fee_p2pkh_when_there_are_two_ismine_outs_one_change_one_recv(self, *, wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 461600, 0), wallet.get_balance()) - def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config): + async def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label', config=config) @@ -1249,7 +1249,7 @@ def _bump_fee_p2wpkh_decrease_payment(self, *, simulate_moving_txs, config): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 45000, 0), wallet.get_balance()) - def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config): + async def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label', config=config) @@ -1324,7 +1324,7 @@ async def test_cpfp_p2pkh(self, mock_save_db): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, funding_output_value - 50000, 0), wallet.get_balance()) - def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): + async def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) @@ -1388,13 +1388,10 @@ def _bump_fee_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 7490060, 0), wallet.get_balance()) - def _bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(self, *, simulate_moving_txs, config): + async def _bump_fee_when_not_all_inputs_are_ismine_subcase_some_outputs_are_ismine_but_not_all(self, *, simulate_moving_txs, config): class NetworkMock: relay_fee = 1000 async def get_transaction(self, txid, timeout=None): - return self._gettx(txid) - @staticmethod - def _gettx(txid): if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd": return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00" else: @@ -1413,7 +1410,6 @@ def is_tip_stale(self): wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve', config=config) wallet.network = NetworkMock() - wallet._get_rawtx_from_network = NetworkMock._gettx # bootstrap wallet funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00') @@ -1427,7 +1423,10 @@ def is_tip_stale(self): wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) # bump tx - tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=70) + orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize()) + orig_rbf_tx.add_info_from_wallet(wallet=wallet) + await orig_rbf_tx.add_info_from_network(network=wallet.network) + tx = wallet.bump_fee(tx=orig_rbf_tx, new_fee_rate=70) tx.locktime = 1898268 tx.version = 2 if simulate_moving_txs: @@ -1445,13 +1444,10 @@ def is_tip_stale(self): tx_copy.serialize_as_bytes().hex()) self.assertEqual('6a8ed07cd97a10ace851b67a65035f04ff477d67cde62bb8679007e87b214e79', tx_copy.txid()) - def _bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(self, *, simulate_moving_txs, config): + async def _bump_fee_when_not_all_inputs_are_ismine_subcase_all_outputs_are_ismine(self, *, simulate_moving_txs, config): class NetworkMock: relay_fee = 1000 async def get_transaction(self, txid, timeout=None): - return self._gettx(txid) - @staticmethod - def _gettx(txid): if txid == "08557327673db61cc921e1a30826608599b86457836be3021105c13940d9a9a3": return "02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00" else: @@ -1473,7 +1469,6 @@ def is_tip_stale(self): gap_limit=4, ) wallet.network = NetworkMock() - wallet._get_rawtx_from_network = NetworkMock._gettx # bootstrap wallet funding_tx = Transaction('02000000000102c247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0000000000fdffffffc247447533b530cacc3e716aae84621857f04a483252374cbdccfdf8b4ef816b0100000000fdffffff01d63f0f00000000001600141ef4658adb12ec745a1a1fef6ab8897f04bade060247304402201dc5be86749d8ce33571a6f1a2f8bbfceba89b9dbf2b4683e66c8c17cf7df6090220729199516cb894569ebbe3e998d47fc74030231ed30f110c9babd8a9dc361115012102728251a5f5f55375eef3c14fe59ab0755ba4d5f388619895238033ac9b51aad20247304402202e5d416489c20810e96e931b98a84b0c0c4fc32d2d34d3470b7ee16810246a4c022040f86cf8030d2117d6487bbe6e23d68d6d70408b002d8055de1f33d038d3a0550121039c009e7e7dad07e74ec5a8ac9f9e3499420dd9fe9709995525c714170152512620f71c00') @@ -1487,7 +1482,10 @@ def is_tip_stale(self): wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) # bump tx - tx = wallet.bump_fee(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=50) + orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize()) + orig_rbf_tx.add_info_from_wallet(wallet=wallet) + await orig_rbf_tx.add_info_from_network(network=wallet.network) + tx = wallet.bump_fee(tx=orig_rbf_tx, new_fee_rate=50) tx.locktime = 1898273 tx.version = 2 if simulate_moving_txs: @@ -1506,7 +1504,7 @@ def is_tip_stale(self): self.assertEqual('b46cdce7e7564dfd09618ab9008ec3a921c6372f3dcdab2f6094735b024485f0', tx_copy.txid()) - def _bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address(self, *, simulate_moving_txs, config): + async def _bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_address(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) @@ -1568,7 +1566,7 @@ def _bump_fee_p2wpkh_when_there_is_only_a_single_output_and_that_is_a_change_add wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 9991750, 0), wallet.get_balance()) - def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config): + async def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) @@ -1631,7 +1629,7 @@ def _bump_fee_when_user_sends_max(self, *, simulate_moving_txs, config): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 0, 0), wallet.get_balance()) - def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs, config): + async def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) @@ -1703,7 +1701,7 @@ def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_txs, con wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 4_990_300, 0), wallet.get_balance()) - def _rbf_batching(self, *, simulate_moving_txs, config): + async def _rbf_batching(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) wallet.config.set_key('batch_rbf', True) @@ -2177,23 +2175,23 @@ async def test_dscancel(self, mock_save_db): for simulate_moving_txs in (False, True): with self.subTest(msg="_dscancel_when_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): - self._dscancel_when_all_outputs_are_ismine( + await self._dscancel_when_all_outputs_are_ismine( simulate_moving_txs=simulate_moving_txs, config=config) with self.subTest(msg="_dscancel_p2wpkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): - self._dscancel_p2wpkh_when_there_is_a_change_address( + await self._dscancel_p2wpkh_when_there_is_a_change_address( simulate_moving_txs=simulate_moving_txs, config=config) with self.subTest(msg="_dscancel_when_user_sends_max", simulate_moving_txs=simulate_moving_txs): - self._dscancel_when_user_sends_max( + await self._dscancel_when_user_sends_max( simulate_moving_txs=simulate_moving_txs, config=config) with self.subTest(msg="_dscancel_when_not_all_inputs_are_ismine", simulate_moving_txs=simulate_moving_txs): - self._dscancel_when_not_all_inputs_are_ismine( + await self._dscancel_when_not_all_inputs_are_ismine( simulate_moving_txs=simulate_moving_txs, config=config) - def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, config): + async def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean', config=config) @@ -2238,7 +2236,7 @@ def _dscancel_when_all_outputs_are_ismine(self, *, simulate_moving_txs, config): tx_details = wallet.get_tx_info(tx_from_any(tx.serialize())) self.assertFalse(tx_details.can_dscancel) - def _dscancel_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): + async def _dscancel_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) @@ -2304,7 +2302,7 @@ def _dscancel_p2wpkh_when_there_is_a_change_address(self, *, simulate_moving_txs wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 9992300, 0), wallet.get_balance()) - def _dscancel_when_user_sends_max(self, *, simulate_moving_txs, config): + async def _dscancel_when_user_sends_max(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) @@ -2369,13 +2367,10 @@ def _dscancel_when_user_sends_max(self, *, simulate_moving_txs, config): wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 9992300, 0), wallet.get_balance()) - def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, config): + async def _dscancel_when_not_all_inputs_are_ismine(self, *, simulate_moving_txs, config): class NetworkMock: relay_fee = 1000 async def get_transaction(self, txid, timeout=None): - return self._gettx(txid) - @staticmethod - def _gettx(txid): if txid == "597098f9077cd2a7bf5bb2a03c9ae5fcd9d1f07c0891cb42cbb129cf9eaf57fd": return "02000000000102a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540000000000fdffffffbdeb0175b1c51c96843d1952f7e1c49c1703717d7d020048d4de0a8eed94dad50000000000fdffffff03b2a00700000000001600140cd6c9f8ce0aa73d77fcf7f156c74f5cbec6906bb2a00700000000001600146435504ddc95e6019a90bb7dfc7ca81a88a8633106d790000000000016001444bd3017ee214370abf683abaa7f6204c9f40210024730440220652a04a2a301d9a031a034f3ae48174e204e17acf7bfc27f0dcab14243f73e2202207b29e964c434dfb2c515232d36566a40dccd4dd93ccb7fd15260ecbda10f0d9801210231994e564a0530068d17a9b0f85bec58d1352517a2861ea99e5b3070d2c5dbda02473044022072186473874919019da0e3d92b6e0aa4f88cba448ed5434615e5a3c8e2b7c42a02203ec05cef66960d5bc45d0f3d25675190cf8035b11a05ed4b719fd9c3a894899b012102f5fdca8c4e30ba0a1babf9cf9ebe62519b08aead351c349ed1ffc8316c24f542d7f61c00" else: @@ -2394,7 +2389,6 @@ def is_tip_stale(self): wallet = self.create_standard_wallet_from_seed('mix total present junior leader live state athlete mistake crack wall valve', config=config) wallet.network = NetworkMock() - wallet._get_rawtx_from_network = NetworkMock._gettx # bootstrap wallet funding_tx = Transaction('02000000000101a5883f3de780d260e6f26cf85144403c7744a65a44cd38f9ff45aecadf010c540100000000fdffffff0220a1070000000000160014db44724ac632ae47ee5765954d64796dd5fec72708de3c000000000016001424b32aadb42a89016c4de8f11741c3b29b15f21c02473044022045cc6c1cc875cbb0c0d8fe323dc1de9716e49ed5659741b0fb3dd9a196894066022077c242640071d12ec5763c5870f482a4823d8713e4bd14353dd621ed29a7f96d012102aea8d439a0f79d8b58e8d7bda83009f587e1f3da350adaa484329bf47cd03465fef61c00') @@ -2408,7 +2402,10 @@ def is_tip_stale(self): wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) # bump tx - tx = wallet.dscancel(tx=tx_from_any(orig_rbf_tx.serialize()), new_fee_rate=70) + orig_rbf_tx = tx_from_any(orig_rbf_tx.serialize()) + orig_rbf_tx.add_info_from_wallet(wallet=wallet) + await orig_rbf_tx.add_info_from_network(network=wallet.network) + tx = wallet.dscancel(tx=orig_rbf_tx, new_fee_rate=70) tx.locktime = 1898278 tx.version = 2 if simulate_moving_txs: @@ -2686,7 +2683,7 @@ async def test_export_psbt_with_xpubs__multisig(self, mock_save_db): tx.inputs()[0].to_json()['bip32_paths']) self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca0c015148ee000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd0c015148ee010000000000000000", tx.serialize_as_bytes().hex()) - tx.prepare_for_export_for_hardware_device(wallet) + await tx.prepare_for_export_for_hardware_device(wallet) # As the keystores were created from just xpubs, they are missing key origin information # (derivation prefix and root fingerprint). # Note that info for ks1 contains the expected bip32 path (m/9999') and fingerprint, but not ks0. @@ -2716,7 +2713,7 @@ async def test_export_psbt_with_xpubs__multisig(self, mock_save_db): tx.inputs()[0].to_json()['bip32_paths']) self.assertEqual("70736274ff01007d020000000122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff058ab05000000000022002014d2823afee4d75f0f83b91a9d625972df41be222c1373d28e068c3eaae9e00a7b4a24000001012b20a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0100fd7e0102000000000102deab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0000000000fdffffffdeab5844de4aadc177d992696fda2aa6e4692403633d31a4b4073710594d2fca0100000000fdffffff02f49f070000000000160014473b34b7da0aa9f7add803019f649e0729fd39d220a10700000000002200207f50b9d6eb4d899c710d8c48903de33d966ff52445d5a57b5210d02a5dd7e3bf0247304402202a4ec3df7bf2b82505bcd4833eeb32875784b4e93d09ac3cf4a8981dc89a049b02205239bad290877fb810a12538a275d5467f3f6afc88d1e0be3d8f6dc4876e6793012103e48cae7f140e15440f4ad6b3d96cb0deb471bbb45daf527e6eb4d5f6c5e26ec802473044022031028192a8307e52829ad1428941000629de73726306ca71d18c5bcfcb98a4a602205ad0240f7dd6c83686ea257f3146ba595b787d7f68b514569962fd5d3692b07c0121033c8af340bd9abf4a56c7cf7554f52e84a1128e5206ffe5da166ca18a57a260077b4a24000105475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae2206022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049109559fbd10f2700800000000000000000220603cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca1c30cf1be530000080010000800000008002000080000000000000000000000101475221027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e221028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd52ae2202027f7f2eaf9a44316c2cd98b67584d1e71ccaced29a347673f3364efe16f5919e2109559fbd10f27008001000000000000002202028d9b8ff374e0f60fbc698c5a494c12d9a31a3ce364b1f81ae4a46f48ae45acdd1c30cf1be530000080010000800000008002000080010000000000000000", tx.serialize_as_bytes().hex()) - tx.prepare_for_export_for_hardware_device(wallet) + await tx.prepare_for_export_for_hardware_device(wallet) self.assertEqual( {'tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg': ('30cf1be5', "m/48h/1h/0h/2h"), 'tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK': ('9559fbd1', "m/9999h")}, @@ -2754,7 +2751,7 @@ async def test_export_psbt_with_xpubs__singlesig(self, mock_save_db): self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000", tx.serialize_as_bytes().hex()) # if there are no multisig inputs, we never include xpubs in the psbt: - tx.prepare_for_export_for_hardware_device(wallet) + await tx.prepare_for_export_for_hardware_device(wallet) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000", tx.serialize_as_bytes().hex()) diff --git a/electrum/transaction.py b/electrum/transaction.py index d9ad81fa0..52f764ac6 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -51,11 +51,12 @@ base_encode, construct_witness, construct_script) from .crypto import sha256d from .logging import get_logger -from .util import ShortID +from .util import ShortID, OldTaskGroup from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address if TYPE_CHECKING: from .wallet import Abstract_Wallet + from .network import Network _logger = get_logger(__name__) @@ -256,6 +257,7 @@ def __init__(self, *, self.block_txpos = None self.spent_height = None # type: Optional[int] # height at which the TXO got spent self.spent_txid = None # type: Optional[str] # txid of the spender + self._utxo = None # type: Optional[Transaction] @property def short_id(self): @@ -264,6 +266,30 @@ def short_id(self): else: return self.prevout.short_name() + @property + def utxo(self): + return self._utxo + + @utxo.setter + def utxo(self, tx: Optional['Transaction']): + if tx is None: + return + # note that tx might be a PartialTransaction + # serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx + tx = tx_from_any(str(tx)) + # 'utxo' field should not be a PSBT: + if not tx.is_complete(): + return + self.validate_data(utxo=tx) + self._utxo = tx + + def validate_data(self, *, utxo: Optional['Transaction'] = None, **kwargs) -> None: + utxo = utxo or self.utxo + if utxo: + if self.prevout.txid.hex() != utxo.txid(): + raise PSBTInputConsistencyFailure(f"PSBT input validation: " + f"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout") + def is_coinbase_input(self) -> bool: """Whether this is the input of a coinbase tx.""" return self.prevout.is_coinbase() @@ -275,6 +301,22 @@ def is_coinbase_output(self) -> bool: return self._is_coinbase_output def value_sats(self) -> Optional[int]: + if self.utxo: + out_idx = self.prevout.out_idx + return self.utxo.outputs()[out_idx].value + return None + + @property + def address(self) -> Optional[str]: + if self.scriptpubkey: + return get_address_from_output_script(self.scriptpubkey) + return None + + @property + def scriptpubkey(self) -> Optional[bytes]: + if self.utxo: + out_idx = self.prevout.out_idx + return self.utxo.outputs()[out_idx].scriptpubkey return None def to_json(self): @@ -314,6 +356,32 @@ def is_segwit(self, *, guess_for_address=False) -> bool: return True return False + async def add_info_from_network( + self, + network: Optional['Network'], + *, + ignore_network_issues: bool = True, + ) -> None: + from .network import NetworkException + async def fetch_from_network(txid) -> Optional[Transaction]: + tx = None + if network and network.has_internet_connection(): + try: + raw_tx = await network.get_transaction(txid, timeout=10) + except NetworkException as e: + _logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {txid}. ' + f'if you are intentionally offline, consider using the --offline flag') + if not ignore_network_issues: + raise e + else: + tx = Transaction(raw_tx) + if not tx and not ignore_network_issues: + raise NetworkException('failed to get prev tx from network') + return tx + + if self.utxo is None: + self.utxo = await fetch_from_network(txid=self.prevout.txid.hex()) + class BCDataStream(object): """Workalike python implementation of Bitcoin's CDataStream class.""" @@ -895,7 +963,40 @@ def wtxid(self) -> Optional[str]: return sha256d(bfh(ser))[::-1].hex() def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None: - return # no-op + # populate prev_txs + for txin in self.inputs(): + wallet.add_input_info(txin) + + async def add_info_from_network(self, network: Optional['Network'], *, ignore_network_issues: bool = True) -> None: + """note: it is recommended to call add_info_from_wallet first, as this can save some network requests""" + if not self.is_missing_info_from_network(): + return + async with OldTaskGroup() as group: + for txin in self.inputs(): + if txin.utxo is None: + await group.spawn(txin.add_info_from_network(network=network, ignore_network_issues=ignore_network_issues)) + + def is_missing_info_from_network(self) -> bool: + return any(txin.utxo is None for txin in self.inputs()) + + def add_info_from_wallet_and_network( + self, *, wallet: 'Abstract_Wallet', show_error: Callable[[str], None], + ) -> bool: + """Returns whether successful. + note: This is sort of a legacy hack... doing network requests in non-async code. + Relatedly, this should *not* be called from the network thread. + """ + # note side-effect: tx is being mutated + from .network import NetworkException + self.add_info_from_wallet(wallet) + try: + if self.is_missing_info_from_network(): + Network.run_from_another_thread( + self.add_info_from_network(wallet.network, ignore_network_issues=False)) + except NetworkException as e: + show_error(repr(e)) + return False + return True def is_final(self) -> bool: """Whether RBF is disabled.""" @@ -1004,6 +1105,21 @@ def output_value_for_address(self, addr): else: raise Exception('output not found', addr) + def input_value(self) -> int: + input_values = [txin.value_sats() for txin in self.inputs()] + if any([val is None for val in input_values]): + raise MissingTxInputAmount() + return sum(input_values) + + def output_value(self) -> int: + return sum(o.value for o in self.outputs()) + + def get_fee(self) -> Optional[int]: + try: + return self.input_value() - self.output_value() + except MissingTxInputAmount: + return None + def get_input_idx_that_spent_prevout(self, prevout: TxOutpoint) -> Optional[int]: # build cache if there isn't one yet # note: can become stale and return incorrect data @@ -1177,7 +1293,6 @@ def serialize_psbt_section_kvs(self, wr) -> None: class PartialTxInput(TxInput, PSBTSection): def __init__(self, *args, **kwargs): TxInput.__init__(self, *args, **kwargs) - self._utxo = None # type: Optional[Transaction] self._witness_utxo = None # type: Optional[TxOutput] self.part_sigs = {} # type: Dict[bytes, bytes] # pubkey -> sig self.sighash = None # type: Optional[int] @@ -1193,23 +1308,6 @@ def __init__(self, *args, **kwargs): self._is_native_segwit = None # type: Optional[bool] # None means unknown self.witness_sizehint = None # type: Optional[int] # byte size of serialized complete witness, for tx size est - @property - def utxo(self): - return self._utxo - - @utxo.setter - def utxo(self, tx: Optional[Transaction]): - if tx is None: - return - # note that tx might be a PartialTransaction - # serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx - tx = tx_from_any(str(tx)) - # 'utxo' field in PSBT cannot be another PSBT: - if not tx.is_complete(): - return - self.validate_data(utxo=tx) - self._utxo = tx - @property def witness_utxo(self): return self._witness_utxo @@ -1268,6 +1366,7 @@ def from_txin(cls, txin: TxInput, *, strip_witness: bool = True) -> 'PartialTxIn nsequence=txin.nsequence, witness=None if strip_witness else txin.witness, is_coinbase_output=txin.is_coinbase_output()) + res.utxo = txin.utxo return res def validate_data( @@ -1397,31 +1496,28 @@ def serialize_psbt_section_kvs(self, wr): wr(key_type, val, key=key) def value_sats(self) -> Optional[int]: + if (val := super().value_sats()) is not None: + return val if self._trusted_value_sats is not None: return self._trusted_value_sats - if self.utxo: - out_idx = self.prevout.out_idx - return self.utxo.outputs()[out_idx].value if self.witness_utxo: return self.witness_utxo.value return None @property def address(self) -> Optional[str]: + if (addr := super().address) is not None: + return addr if self._trusted_address is not None: return self._trusted_address - scriptpubkey = self.scriptpubkey - if scriptpubkey: - return get_address_from_output_script(scriptpubkey) return None @property def scriptpubkey(self) -> Optional[bytes]: + if (spk := super().scriptpubkey) is not None: + return spk if self._trusted_address is not None: return bfh(bitcoin.address_to_script(self._trusted_address)) - if self.utxo: - out_idx = self.prevout.out_idx - return self.utxo.outputs()[out_idx].scriptpubkey if self.witness_utxo: return self.witness_utxo.scriptpubkey return None @@ -1886,21 +1982,6 @@ def BIP69_sort(self, inputs=True, outputs=True): self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey)) self.invalidate_ser_cache() - def input_value(self) -> int: - input_values = [txin.value_sats() for txin in self.inputs()] - if any([val is None for val in input_values]): - raise MissingTxInputAmount() - return sum(input_values) - - def output_value(self) -> int: - return sum(o.value for o in self.outputs()) - - def get_fee(self) -> Optional[int]: - try: - return self.input_value() - self.output_value() - except MissingTxInputAmount: - return None - def serialize_preimage(self, txin_index: int, *, bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: nVersion = int_to_hex(self.version, 4) @@ -2052,7 +2133,6 @@ def add_info_from_wallet( wallet: 'Abstract_Wallet', *, include_xpubs: bool = False, - ignore_network_issues: bool = True, ) -> None: if self.is_complete(): return @@ -2074,7 +2154,6 @@ def add_info_from_wallet( wallet.add_input_info( txin, only_der_suffix=False, - ignore_network_issues=ignore_network_issues, ) for txout in self.outputs(): wallet.add_output_info( @@ -2104,8 +2183,9 @@ def prepare_for_export_for_coinjoin(self) -> None: txout.bip32_paths.clear() txout._unknown.clear() - def prepare_for_export_for_hardware_device(self, wallet: 'Abstract_Wallet') -> None: + async def prepare_for_export_for_hardware_device(self, wallet: 'Abstract_Wallet') -> None: self.add_info_from_wallet(wallet, include_xpubs=True) + await self.add_info_from_network(wallet.network) # log warning if PSBT_*_BIP32_DERIVATION fields cannot be filled with full path due to missing info from .keystore import Xpub def is_ks_missing_info(ks): diff --git a/electrum/wallet.py b/electrum/wallet.py index 17f083f21..8b003a2a1 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1861,6 +1861,9 @@ def bump_fee( """Increase the miner fee of 'tx'. 'new_fee_rate' is the target min rate in sat/vbyte 'coins' is a list of UTXOs we can choose from as potential new inputs to be added + + note: it is the caller's responsibility to have already called tx.add_info_from_network(). + Without that, all txins must be ismine. """ txid = txid or tx.txid() assert txid @@ -1872,11 +1875,9 @@ def bump_fee( if tx.is_final(): raise CannotBumpFee(_('Transaction is final')) new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision - try: - # note: this might download input utxos over network - tx.add_info_from_wallet(self, ignore_network_issues=False) - except NetworkException as e: - raise CannotBumpFee(repr(e)) + tx.add_info_from_wallet(self) + if tx.is_missing_info_from_network(): + raise Exception("tx missing info from network") old_tx_size = tx.estimated_size() old_fee = tx.get_fee() assert old_fee is not None @@ -2123,6 +2124,9 @@ def dscancel( """Double-Spend-Cancel: cancel an unconfirmed tx by double-spending its inputs, paying ourselves. 'new_fee_rate' is the target min rate in sat/vbyte + + note: it is the caller's responsibility to have already called tx.add_info_from_network(). + Without that, all txins must be ismine. """ if not isinstance(tx, PartialTransaction): tx = PartialTransaction.from_tx(tx) @@ -2132,11 +2136,9 @@ def dscancel( if tx.is_final(): raise CannotDoubleSpendTx(_('Transaction is final')) new_fee_rate = quantize_feerate(new_fee_rate) # strip excess precision - try: - # note: this might download input utxos over network - tx.add_info_from_wallet(self, ignore_network_issues=False) - except NetworkException as e: - raise CannotDoubleSpendTx(repr(e)) + tx.add_info_from_wallet(self) + if tx.is_missing_info_from_network(): + raise Exception("tx missing info from network") old_tx_size = tx.estimated_size() old_fee = tx.get_fee() assert old_fee is not None @@ -2178,7 +2180,6 @@ def _add_input_utxo_info( txin: PartialTxInput, *, address: str = None, - ignore_network_issues: bool = True, ) -> None: # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198). # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset, @@ -2194,7 +2195,7 @@ def _add_input_utxo_info( txin_value = item[2] txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value) if txin.utxo is None: - txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=ignore_network_issues) + txin.utxo = self.db.get_transaction(txin.prevout.txid.hex()) def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput], address: str) -> bool: @@ -2206,14 +2207,21 @@ def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[Partial def add_input_info( self, - txin: PartialTxInput, + txin: TxInput, *, only_der_suffix: bool = False, - ignore_network_issues: bool = True, ) -> None: - address = self.adb.get_txin_address(txin) + """Populates the txin, using info the wallet already has. + That is, network requests are *not* done to fetch missing prev txs! + For that, use txin.add_info_from_network. + """ # note: we add input utxos regardless of is_mine - self._add_input_utxo_info(txin, ignore_network_issues=ignore_network_issues, address=address) + if txin.utxo is None: + txin.utxo = self.db.get_transaction(txin.prevout.txid.hex()) + if not isinstance(txin, PartialTxInput): + return + address = self.adb.get_txin_address(txin) + self._add_input_utxo_info(txin, address=address) is_mine = self.is_mine(address) if not is_mine: is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address) @@ -2279,31 +2287,6 @@ def can_sign(self, tx: Transaction) -> bool: return True return False - def _get_rawtx_from_network(self, txid: str) -> str: - """legacy hack. do not use in new code.""" - assert self.network - return self.network.run_from_another_thread( - self.network.get_transaction(txid, timeout=10)) - - def get_input_tx(self, tx_hash: str, *, ignore_network_issues=False) -> Optional[Transaction]: - # First look up an input transaction in the wallet where it - # will likely be. If co-signing a transaction it may not have - # all the input txs, in which case we ask the network. - tx = self.db.get_transaction(tx_hash) - if not tx and self.network and self.network.has_internet_connection(): - try: - raw_tx = self._get_rawtx_from_network(tx_hash) - except NetworkException as e: - _logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {tx_hash}. ' - f'if you are intentionally offline, consider using the --offline flag') - if not ignore_network_issues: - raise e - else: - tx = Transaction(raw_tx) - if not tx and not ignore_network_issues: - raise NetworkException('failed to get prev tx from network') - return tx - def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None: address = txout.address if not self.is_mine(address): From c79074c4d8114189b3fd85bcec55a4ccacecafd6 Mon Sep 17 00:00:00 2001 From: Calin Culianu Date: Sat, 11 Mar 2023 18:37:28 +0000 Subject: [PATCH 0323/1143] qt: port "rate_limiter" function decorator utility from Electron-Cash ported from https://github.com/Electron-Cash/Electron-Cash/blob/e8bbf8280ccaef95c10cdb1c1cb182cd4b504937/electroncash_gui/qt/util.py (originally added in https://github.com/Electron-Cash/Electron-Cash/commit/8b8d8a56900c42197ed8bb3b48909c2a2ab92826 ) Co-authored-by: Calin Culianu Co-authored-by: SomberNight --- electrum/gui/qt/rate_limiter.py | 234 ++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 electrum/gui/qt/rate_limiter.py diff --git a/electrum/gui/qt/rate_limiter.py b/electrum/gui/qt/rate_limiter.py new file mode 100644 index 000000000..c90d595ee --- /dev/null +++ b/electrum/gui/qt/rate_limiter.py @@ -0,0 +1,234 @@ +# Copyright (c) 2019 Calin Culianu +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +from functools import wraps +import threading +import time +import weakref + +from PyQt5.QtCore import QObject, QTimer + +from electrum.logging import Logger, get_logger + + +_logger = get_logger(__name__) + + +class RateLimiter(Logger): + ''' Manages the state of a @rate_limited decorated function, collating + multiple invocations. This class is not intented to be used directly. Instead, + use the @rate_limited decorator (for instance methods). + This state instance gets inserted into the instance attributes of the target + object wherever a @rate_limited decorator appears. + The inserted attribute is named "__FUNCNAME__RateLimiter". ''' + # some defaults + last_ts = 0.0 + timer = None + saved_args = (tuple(),dict()) + ctr = 0 + + def __init__(self, rate, ts_after, obj, func): + self.n = func.__name__ + self.qn = func.__qualname__ + self.rate = rate + self.ts_after = ts_after + self.obj = weakref.ref(obj) # keep a weak reference to the object to prevent cycles + self.func = func + Logger.__init__(self) + #self.logger.debug(f"*** Created: {func=},{obj=},{rate=}") + + def diagnostic_name(self): + return "{}:{}".format("rate_limited",self.qn) + + def kill_timer(self): + if self.timer: + #self.logger.debug("deleting timer") + try: + self.timer.stop() + self.timer.deleteLater() + except RuntimeError as e: + if 'c++ object' in str(e).lower(): + # This can happen if the attached object which actually owns + # QTimer is deleted by Qt before this call path executes. + # This call path may be executed from a queued connection in + # some circumstances, hence the crazyness (I think). + self.logger.debug("advisory: QTimer was already deleted by Qt, ignoring...") + else: + raise + finally: + self.timer = None + + @classmethod + def attr_name(cls, func): return "__{}__{}".format(func.__name__, cls.__name__) + + @classmethod + def invoke(cls, rate, ts_after, func, args, kwargs): + ''' Calls _invoke() on an existing RateLimiter object (or creates a new + one for the given function on first run per target object instance). ''' + assert args and isinstance(args[0], object), "@rate_limited decorator may only be used with object instance methods" + assert threading.current_thread() is threading.main_thread(), "@rate_limited decorator may only be used with functions called in the main thread" + obj = args[0] + a_name = cls.attr_name(func) + #_logger.debug(f"*** {a_name=}, {obj=}") + rl = getattr(obj, a_name, None) # we hide the RateLimiter state object in an attribute (name based on the wrapped function name) in the target object + if rl is None: + # must be the first invocation, create a new RateLimiter state instance. + rl = cls(rate, ts_after, obj, func) + setattr(obj, a_name, rl) + return rl._invoke(args, kwargs) + + def _invoke(self, args, kwargs): + self._push_args(args, kwargs) # since we're collating, save latest invocation's args unconditionally. any future invocation will use the latest saved args. + self.ctr += 1 # increment call counter + #self.logger.debug(f"args_saved={args}, kwarg_saved={kwargs}") + if not self.timer: # check if there's a pending invocation already + now = time.time() + diff = float(self.rate) - (now - self.last_ts) + if diff <= 0: + # Time since last invocation was greater than self.rate, so call the function directly now. + #self.logger.debug("calling directly") + return self._doIt() + else: + # Time since last invocation was less than self.rate, so defer to the future with a timer. + self.timer = QTimer(self.obj() if isinstance(self.obj(), QObject) else None) + self.timer.timeout.connect(self._doIt) + #self.timer.destroyed.connect(lambda x=None,qn=self.qn: print(qn,"Timer deallocated")) + self.timer.setSingleShot(True) + self.timer.start(int(diff*1e3)) + #self.logger.debug("deferring") + else: + # We had a timer active, which means as future call will occur. So return early and let that call happenin the future. + # Note that a side-effect of this aborted invocation was to update self.saved_args. + pass + #self.logger.debug("ignoring (already scheduled)") + + def _pop_args(self): + args, kwargs = self.saved_args # grab the latest collated invocation's args. this attribute is always defined. + self.saved_args = (tuple(),dict()) # clear saved args immediately + return args, kwargs + + def _push_args(self, args, kwargs): + self.saved_args = (args, kwargs) + + def _doIt(self): + #self.logger.debug("called!") + t0 = time.time() + args, kwargs = self._pop_args() + #self.logger.debug(f"args_actually_used={args}, kwarg_actually_used={kwargs}") + ctr0 = self.ctr # read back current call counter to compare later for reentrancy detection + retval = self.func(*args, **kwargs) # and.. call the function. use latest invocation's args + was_reentrant = self.ctr != ctr0 # if ctr is not the same, func() led to a call this function! + del args, kwargs # deref args right away (allow them to get gc'd) + tf = time.time() + time_taken = tf-t0 + if self.ts_after: + self.last_ts = tf + else: + if time_taken > float(self.rate): + self.logger.debug(f"method took too long: {time_taken} > {self.rate}. Fudging timestamps to compensate.") + self.last_ts = tf # Hmm. This function takes longer than its rate to complete. so mark its last run time as 'now'. This breaks the rate but at least prevents this function from starving the CPU (benforces a delay). + else: + self.last_ts = t0 # Function takes less than rate to complete, so mark its t0 as when we entered to keep the rate constant. + + if self.timer: # timer is not None if and only if we were a delayed (collated) invocation. + if was_reentrant: + # we got a reentrant call to this function as a result of calling func() above! re-schedule the timer. + self.logger.debug("*** detected a re-entrant call, re-starting timer") + time_left = float(self.rate) - (tf - self.last_ts) + self.timer.start(time_left*1e3) + else: + # We did not get a reentrant call, so kill the timer so subsequent calls can schedule the timer and/or call func() immediately. + self.kill_timer() + elif was_reentrant: + self.logger.debug("*** detected a re-entrant call") + + return retval + + +class RateLimiterClassLvl(RateLimiter): + ''' This RateLimiter object is used if classlevel=True is specified to the + @rate_limited decorator. It inserts the __RateLimiterClassLvl state object + on the class level and collates calls for all instances to not exceed rate. + Each instance is guaranteed to receive at least 1 call and to have multiple + calls updated with the latest args for the final call. So for instance: + a.foo(1) + a.foo(2) + b.foo(10) + b.foo(3) + Would collate to a single 'class-level' call using 'rate': + a.foo(2) # latest arg taken, collapsed to 1 call + b.foo(3) # latest arg taken, collapsed to 1 call + ''' + + @classmethod + def invoke(cls, rate, ts_after, func, args, kwargs): + assert args and not isinstance(args[0], type), "@rate_limited decorator may not be used with static or class methods" + obj = args[0] + objcls = obj.__class__ + args = list(args) + args.insert(0, objcls) # prepend obj class to trick super.invoke() into making this state object be class-level. + return super(RateLimiterClassLvl, cls).invoke(rate, ts_after, func, args, kwargs) + + def _push_args(self, args, kwargs): + objcls, obj = args[0:2] + args = args[2:] + self.saved_args[obj] = (args, kwargs) + + def _pop_args(self): + weak_dict = self.saved_args + self.saved_args = weakref.WeakKeyDictionary() + return (weak_dict,),dict() + + def _call_func_for_all(self, weak_dict): + for ref in weak_dict.keyrefs(): + obj = ref() + if obj: + args,kwargs = weak_dict[obj] + obj_name = obj.diagnostic_name() if hasattr(obj, "diagnostic_name") else obj + #self.logger.debug(f"calling for {obj_name}, timer={bool(self.timer)}") + self.func_target(obj, *args, **kwargs) + + def __init__(self, rate, ts_after, obj, func): + # note: obj here is really the __class__ of the obj because we prepended the class in our custom invoke() above. + super().__init__(rate, ts_after, obj, func) + self.func_target = func + self.func = self._call_func_for_all + self.saved_args = weakref.WeakKeyDictionary() # we don't use a simple arg tuple, but instead an instance -> args,kwargs dictionary to store collated calls, per instance collated + + +def rate_limited(rate, *, classlevel=False, ts_after=False): + """ A Function decorator for rate-limiting GUI event callbacks. Argument + rate in seconds is the minimum allowed time between subsequent calls of + this instance of the function. Calls that arrive more frequently than + rate seconds will be collated into a single call that is deferred onto + a QTimer. It is preferable to use this decorator on QObject subclass + instance methods. This decorator is particularly useful in limiting + frequent calls to GUI update functions. + params: + rate - calls are collated to not exceed rate (in seconds) + classlevel - if True, specify that the calls should be collated at + 1 per `rate` secs. for *all* instances of a class, otherwise + calls will be collated on a per-instance basis. + ts_after - if True, mark the timestamp of the 'last call' AFTER the + target method completes. That is, the collation of calls will + ensure at least `rate` seconds will always elapse between + subsequent calls. If False, the timestamp is taken right before + the collated calls execute (thus ensuring a fixed period for + collated calls). + TL;DR: ts_after=True : `rate` defines the time interval you want + from last call's exit to entry into next + call. + ts_adter=False: `rate` defines the time between each + call's entry. + (See on_fx_quotes & on_fx_history in main_window.py for example usages + of this decorator). """ + def wrapper0(func): + @wraps(func) + def wrapper(*args, **kwargs): + if classlevel: + return RateLimiterClassLvl.invoke(rate, ts_after, func, args, kwargs) + return RateLimiter.invoke(rate, ts_after, func, args, kwargs) + return wrapper + return wrapper0 + From d83863cc528e87c152c5b31f38cc86ab619bc72d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 12 Mar 2023 00:19:39 +0000 Subject: [PATCH 0324/1143] qt tx dialog: add checkbox "Download input data" If checked, we download prev (parent) txs from the network, asynchronously. This allows calculating the fee and showing "input addresses". We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp, short ids), but this is not done currently. Note that there is no clean way to do this with electrum protocol 1.4: `blockchain.transaction.get_merkle(tx_hash, height)` requires knowledge of the block height. Loosely based on https://github.com/Electron-Cash/Electron-Cash/commit/6112fe0e51e48e9ceaaecf47a014e6f4a7b41703 --- electrum/gui/kivy/uix/dialogs/tx_dialog.py | 2 +- electrum/gui/qml/qetxdetails.py | 2 +- electrum/gui/qt/transaction_dialog.py | 99 ++++++++++++++++++---- electrum/transaction.py | 59 +++++++++++-- electrum/wallet.py | 2 + 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index f61d2685b..0c8406c22 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -134,7 +134,7 @@ def __init__(self, app, tx: Transaction): tx.add_info_from_wallet(self.wallet) if not tx.is_complete() and tx.is_missing_info_from_network(): Network.run_from_another_thread( - tx.add_info_from_network(self.wallet.network)) # FIXME is this needed?... + tx.add_info_from_network(self.wallet.network, timeout=10)) # FIXME is this needed?... def on_open(self): self.update() diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 6a3775598..54e3cab05 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -241,7 +241,7 @@ def update(self): self._tx.add_info_from_wallet(self._wallet.wallet) if not self._tx.is_complete() and self._tx.is_missing_info_from_network(): Network.run_from_another_thread( - self._tx.add_info_from_network(self._wallet.wallet.network)) # FIXME is this needed?... + self._tx.add_info_from_network(self._wallet.wallet.network, timeout=10)) # FIXME is this needed?... self._inputs = list(map(lambda x: x.to_json(), self._tx.inputs())) self._outputs = list(map(lambda x: { diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index d893165ff..1fb6ad5ce 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -23,7 +23,9 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import asyncio import sys +import concurrent.futures import copy import datetime import traceback @@ -32,7 +34,7 @@ from functools import partial from decimal import Decimal -from PyQt5.QtCore import QSize, Qt, QUrl, QPoint +from PyQt5.QtCore import QSize, Qt, QUrl, QPoint, pyqtSignal from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap, QCursor from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QGridLayout, QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox, QTextBrowser, QToolTip, @@ -49,8 +51,9 @@ from electrum.plugin import run_hook from electrum import simple_config from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, TxOutpoint +from electrum.transaction import TxinDataFetchProgress from electrum.logging import get_logger -from electrum.util import ShortID +from electrum.util import ShortID, get_asyncio_loop from electrum.network import Network from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, @@ -60,6 +63,7 @@ TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX, BlockingWaitingDialog, getSaveFileName, ColorSchemeItem, get_iconname_qrcode) +from .rate_limiter import rate_limited if TYPE_CHECKING: @@ -106,6 +110,11 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.inputs_textedit.textInteractionFlags() | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard) self.inputs_textedit.setContextMenuPolicy(Qt.CustomContextMenu) self.inputs_textedit.customContextMenuRequested.connect(self.on_context_menu_for_inputs) + + self.inheader_hbox = QHBoxLayout() + self.inheader_hbox.setContentsMargins(0, 0, 0, 0) + self.inheader_hbox.addWidget(self.inputs_header) + self.txo_color_recv = TxOutputColoring( legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address")) self.txo_color_change = TxOutputColoring( @@ -130,7 +139,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): outheader_hbox.addWidget(self.txo_color_2fa.legend_label) vbox = QVBoxLayout() - vbox.addWidget(self.inputs_header) + vbox.addLayout(self.inheader_hbox) vbox.addWidget(self.inputs_textedit) vbox.addLayout(outheader_hbox) vbox.addWidget(self.outputs_textedit) @@ -374,6 +383,8 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_uns class TxDialog(QDialog, MessageBoxMixin): + throttled_update_sig = pyqtSignal() # emit from thread to do update in main thread + def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved, external_keypairs=None): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. @@ -408,6 +419,20 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav self.io_widget = TxInOutWidget(self.main_window, self.wallet) vbox.addWidget(self.io_widget) + # add "fetch_txin_data" checkbox to io_widget + fetch_txin_data_cb = QCheckBox(_('Download input data')) + fetch_txin_data_cb.setChecked(bool(self.config.get('tx_dialog_fetch_txin_data', False))) + fetch_txin_data_cb.setToolTip(_('Download parent transactions from the network.\n' + 'Allows filling in missing fee and address details.')) + def on_fetch_txin_data_cb(x): + self.config.set_key('tx_dialog_fetch_txin_data', bool(x)) + if x: + self.initiate_fetch_txin_data() + fetch_txin_data_cb.stateChanged.connect(on_fetch_txin_data_cb) + self.io_widget.inheader_hbox.addStretch(1) + self.io_widget.inheader_hbox.addWidget(fetch_txin_data_cb) + self.io_widget.inheader_hbox.addStretch(10) + self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) @@ -461,6 +486,10 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav vbox.addLayout(hbox) dialogs.append(self) + self._fetch_txin_data_fut = None # type: Optional[concurrent.futures.Future] + self._fetch_txin_data_progress = None # type: Optional[TxinDataFetchProgress] + self.throttled_update_sig.connect(self._throttled_update, Qt.QueuedConnection) + self.set_tx(tx) self.update() self.set_title() @@ -479,13 +508,17 @@ def set_tx(self, tx: 'Transaction'): # or that a beyond-gap-limit address is is_mine. # note: this might fetch prev txs over the network. tx.add_info_from_wallet(self.wallet) - # TODO fetch prev txs for any tx; guarded with a config key + # FIXME for PSBTs, we do a blocking fetch, as the missing data might be needed for e.g. signing + # - otherwise, the missing data is for display-completeness only, e.g. fee, input addresses (we do it async) if not tx.is_complete() and tx.is_missing_info_from_network(): BlockingWaitingDialog( self, _("Adding info to tx, from network..."), - lambda: Network.run_from_another_thread(tx.add_info_from_network(self.wallet.network)), + lambda: Network.run_from_another_thread( + tx.add_info_from_network(self.wallet.network, timeout=10)), ) + elif self.config.get('tx_dialog_fetch_txin_data', False): + self.initiate_fetch_txin_data() def do_broadcast(self): self.main_window.push_top_level_window(self) @@ -507,6 +540,9 @@ def closeEvent(self, event): dialogs.remove(self) except ValueError: pass # was not in list already + if self._fetch_txin_data_fut: + self._fetch_txin_data_fut.cancel() + self._fetch_txin_data_fut = None def reject(self): # Override escape-key to close normally (and invoke closeEvent) @@ -660,6 +696,10 @@ def join_tx_with_another(self): return self.update() + @rate_limited(0.5, ts_after=True) + def _throttled_update(self): + self.update() + def update(self): if self.tx is None: return @@ -742,25 +782,30 @@ def update(self): else: amount_str = _("Amount sent:") + ' %s' % format_amount(-amount) + ' ' + base_unit if fx.is_enabled(): - if tx_item_fiat: - amount_str += ' (%s)' % tx_item_fiat['fiat_value'].to_ui_string() - else: - amount_str += ' (%s)' % format_fiat_and_units(abs(amount)) + if tx_item_fiat: # historical tx -> using historical price + amount_str += ' ({})'.format(tx_item_fiat['fiat_value'].to_ui_string()) + elif tx_details.is_related_to_wallet: # probably "tx preview" -> using current price + amount_str += ' ({})'.format(format_fiat_and_units(abs(amount))) if amount_str: self.amount_label.setText(amount_str) else: self.amount_label.hide() size_str = _("Size:") + ' %d bytes'% size if fee is None: - fee_str = _("Fee") + ': ' + _("unknown") + if prog := self._fetch_txin_data_progress: + if not prog.has_errored: + fee_str = _("Downloading input data...") + f" ({prog.num_tasks_done}/{prog.num_tasks_total})" + else: + fee_str = _("Downloading input data...") + f" error." + else: + fee_str = _("Fee") + ': ' + _("unknown") else: fee_str = _("Fee") + f': {format_amount(fee)} {base_unit}' if fx.is_enabled(): - if tx_item_fiat: - fiat_fee_str = tx_item_fiat['fiat_fee'].to_ui_string() - else: - fiat_fee_str = format_fiat_and_units(fee) - fee_str += f' ({fiat_fee_str})' + if tx_item_fiat: # historical tx -> using historical price + fee_str += ' ({})'.format(tx_item_fiat['fiat_fee'].to_ui_string()) + elif tx_details.is_related_to_wallet: # probably "tx preview" -> using current price + fee_str += ' ({})'.format(format_fiat_and_units(fee)) if fee is not None: fee_rate = Decimal(fee) / size # sat/byte fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000) @@ -887,6 +932,30 @@ def on_finalize(self): def update_fee_fields(self): pass # overridden in subclass + def initiate_fetch_txin_data(self): + """Download missing input data from the network, asynchronously. + Note: we fetch the prev txs, which allows calculating the fee and showing "input addresses". + We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp), + but this is not done currently. + """ + tx = self.tx + if not tx: + return + if self._fetch_txin_data_fut is not None: + return + network = self.wallet.network + def progress_cb(prog: TxinDataFetchProgress): + self._fetch_txin_data_progress = prog + self.throttled_update_sig.emit() + async def wrapper(): + try: + await tx.add_info_from_network(network, progress_cb=progress_cb) + finally: + self._fetch_txin_data_fut = None + + self._fetch_txin_data_progress = None + self._fetch_txin_data_fut = asyncio.run_coroutine_threadsafe(wrapper(), get_asyncio_loop()) + class TxDetailLabel(QLabel): def __init__(self, *, word_wrap=None): diff --git a/electrum/transaction.py b/electrum/transaction.py index 52f764ac6..5d6090e6b 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -91,6 +91,13 @@ class MissingTxInputAmount(Exception): pass +class TxinDataFetchProgress(NamedTuple): + num_tasks_done: int + num_tasks_total: int + has_errored: bool + has_finished: bool + + class Sighash(IntEnum): # note: this is not an IntFlag, as ALL|NONE != SINGLE @@ -361,13 +368,15 @@ async def add_info_from_network( network: Optional['Network'], *, ignore_network_issues: bool = True, - ) -> None: + timeout=None, + ) -> bool: + """Returns True iff successful.""" from .network import NetworkException async def fetch_from_network(txid) -> Optional[Transaction]: tx = None if network and network.has_internet_connection(): try: - raw_tx = await network.get_transaction(txid, timeout=10) + raw_tx = await network.get_transaction(txid, timeout=timeout) except NetworkException as e: _logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {txid}. ' f'if you are intentionally offline, consider using the --offline flag') @@ -381,6 +390,7 @@ async def fetch_from_network(txid) -> Optional[Transaction]: if self.utxo is None: self.utxo = await fetch_from_network(txid=self.prevout.txid.hex()) + return self.utxo is not None class BCDataStream(object): @@ -967,14 +977,49 @@ def add_info_from_wallet(self, wallet: 'Abstract_Wallet', **kwargs) -> None: for txin in self.inputs(): wallet.add_input_info(txin) - async def add_info_from_network(self, network: Optional['Network'], *, ignore_network_issues: bool = True) -> None: + async def add_info_from_network( + self, + network: Optional['Network'], + *, + ignore_network_issues: bool = True, + progress_cb: Callable[[TxinDataFetchProgress], None] = None, + timeout=None, + ) -> None: """note: it is recommended to call add_info_from_wallet first, as this can save some network requests""" if not self.is_missing_info_from_network(): return - async with OldTaskGroup() as group: - for txin in self.inputs(): - if txin.utxo is None: - await group.spawn(txin.add_info_from_network(network=network, ignore_network_issues=ignore_network_issues)) + if progress_cb is None: + progress_cb = lambda *args, **kwargs: None + num_tasks_done = 0 + num_tasks_total = 0 + has_errored = False + has_finished = False + async def add_info_to_txin(txin: TxInput): + nonlocal num_tasks_done, has_errored + progress_cb(TxinDataFetchProgress(num_tasks_done, num_tasks_total, has_errored, has_finished)) + success = await txin.add_info_from_network( + network=network, + ignore_network_issues=ignore_network_issues, + timeout=timeout, + ) + if success: + num_tasks_done += 1 + else: + has_errored = True + progress_cb(TxinDataFetchProgress(num_tasks_done, num_tasks_total, has_errored, has_finished)) + # schedule a network task for each txin + try: + async with OldTaskGroup() as group: + for txin in self.inputs(): + if txin.utxo is None: + num_tasks_total += 1 + await group.spawn(add_info_to_txin(txin=txin)) + except Exception as e: + has_errored = True + _logger.error(f"tx.add_info_from_network() got exc: {e!r}") + finally: + has_finished = True + progress_cb(TxinDataFetchProgress(num_tasks_done, num_tasks_total, has_errored, has_finished)) def is_missing_info_from_network(self) -> bool: return any(txin.utxo is None for txin in self.inputs()) diff --git a/electrum/wallet.py b/electrum/wallet.py index 8b003a2a1..af42eba51 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -277,6 +277,7 @@ class TxWalletDetails(NamedTuple): mempool_depth_bytes: Optional[int] can_remove: bool # whether user should be allowed to delete tx is_lightning_funding_tx: bool + is_related_to_wallet: bool class Abstract_Wallet(ABC, Logger, EventListener): @@ -862,6 +863,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: mempool_depth_bytes=exp_n, can_remove=can_remove, is_lightning_funding_tx=is_lightning_funding_tx, + is_related_to_wallet=is_relevant, ) def get_tx_parents(self, txid) -> Dict: From 473c86c395fd8b7f0d7f2090f450939c051cd572 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Mar 2023 10:11:08 +0100 Subject: [PATCH 0325/1143] toolbar: use custom MyMenu class with addToggle --- electrum/gui/qt/address_list.py | 5 ++--- electrum/gui/qt/contact_list.py | 9 ++++----- electrum/gui/qt/history_list.py | 14 +++++--------- electrum/gui/qt/main_window.py | 3 +++ electrum/gui/qt/receive_tab.py | 11 ++++------- electrum/gui/qt/send_tab.py | 19 +++++++++---------- electrum/gui/qt/util.py | 23 +++++++++++++++++------ 7 files changed, 44 insertions(+), 40 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index ab9c5ca65..379b2dc9f 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -109,9 +109,8 @@ def __init__(self, parent): self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder) def create_toolbar(self, config): - toolbar = self.create_toolbar_with_menu('', [ - (_("&Filter"), lambda: self.toggle_toolbar(self.config)), - ]) + toolbar, menu = self.create_toolbar_with_menu('') + menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar(self.config)) hbox = self.create_toolbar_buttons() toolbar.insertLayout(1, hbox) return toolbar diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 33cc23029..446f5e5a2 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -129,9 +129,8 @@ def get_edit_key_from_coordinate(self, row, col): return self.get_role_data_from_coordinate(row, col, role=self.ROLE_CONTACT_KEY) def create_toolbar(self, config): - toolbar = self.create_toolbar_with_menu('', [ - (_("&New contact"), self.parent.new_contact_dialog), - (_("Import"), lambda: self.parent.import_contacts()), - (_("Export"), lambda: self.parent.export_contacts()), - ]) + toolbar, menu = self.create_toolbar_with_menu('') + menu.addAction(_("&New contact"), self.parent.new_contact_dialog) + menu.addAction(_("Import"), lambda: self.parent.import_contacts()) + menu.addAction(_("Export"), lambda: self.parent.export_contacts()) return toolbar diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index eff2d7a40..5325e394a 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -528,19 +528,15 @@ def on_combo(self, x): self.hide_rows() def create_toolbar(self, config): - toolbar = self.create_toolbar_with_menu('', [ - (_("&Filter Period"), lambda: self.toggle_toolbar(self.config)), - (_("&Summary"), self.show_summary), - (_("&Plot"), self.plot_history_dialog), - (_("&Export"), self.export_history_dialog), - ]) + toolbar, menu = self.create_toolbar_with_menu('') + menu.addToggle(_("&Filter Period"), lambda: self.toggle_toolbar(self.config)) + menu.addAction(_("&Summary"), self.show_summary) + menu.addAction(_("&Plot"), self.plot_history_dialog) + menu.addAction(_("&Export"), self.export_history_dialog) hbox = self.create_toolbar_buttons() toolbar.insertLayout(1, hbox) return toolbar - def toggle_filter(self): - pass - def get_toolbar_buttons(self): return self.period_combo, self.start_button, self.end_button diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 0de92a984..68649f32d 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1065,6 +1065,9 @@ def create_receive_tab(self): def do_copy(self, text: str, *, title: str = None) -> None: self.app.clipboard().setText(text) message = _("Text copied to Clipboard") if title is None else _("{} copied to Clipboard").format(title) + self.show_tooltip_after_delay(message) + + def show_tooltip_after_delay(self, message): # tooltip cannot be displayed immediately when called from a menu; wait 200ms self.gui_object.timer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, self)) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index ed28ac19f..cfa9d38ca 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -174,13 +174,10 @@ def on_receive_swap(): self.receive_requests_label.setMaximumWidth(400) from .request_list import RequestList self.request_list = RequestList(self) - self.toolbar = self.request_list.create_toolbar_with_menu( - '', - [ - (_("Toggle QR code window"), self.window.toggle_qr_window), - (_("Import requests"), self.window.import_requests), - (_("Export requests"), self.window.export_requests), - ]) + self.toolbar, menu = self.request_list.create_toolbar_with_menu('') + menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) + menu.addAction(_("Import requests"), self.window.import_requests) + menu.addAction(_("Export requests"), self.window.export_requests) # layout vbox_g = QVBoxLayout() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 1280c6e80..c4f56bfe2 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -149,13 +149,10 @@ def reset_max(text): self.invoices_label = QLabel(_('Invoices')) from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) - self.toolbar = self.invoice_list.create_toolbar_with_menu( - '', - [ - (_("&Pay to many"), self.paytomany), - (_("Import invoices"), self.window.import_invoices), - (_("Export invoices"), self.window.export_invoices), - ]) + self.toolbar, menu = self.invoice_list.create_toolbar_with_menu('') + menu.addToggle(_("&Pay to many"), self.paytomany) + menu.addAction(_("Import invoices"), self.window.import_invoices) + menu.addAction(_("Export invoices"), self.window.export_invoices) vbox0 = QVBoxLayout() vbox0.addLayout(grid) @@ -754,15 +751,17 @@ def broadcast_done(result): broadcast_thread, broadcast_done, self.window.on_error) def paytomany(self): - self.window.show_send_tab() + if self.payto_e.is_multiline(): + self.payto_e.do_clear() + return self.payto_e.paytomany() - msg = '\n'.join([ + message = '\n'.join([ _('Enter a list of outputs in the \'Pay to\' field.'), _('One output per line.'), _('Format: address, amount'), _('You may load a CSV file using the file icon.') ]) - self.show_message(msg, title=_('Pay to many')) + self.window.show_tooltip_after_delay(message) def payto_contacts(self, labels): paytos = [self.window.get_contact_payto(label) for label in labels] diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index ddcadadfa..29a8b8319 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -569,6 +569,20 @@ def sizeHint(self, option: QStyleOptionViewItem, idx: QModelIndex) -> QSize: return custom_data.sizeHint(default_size) +class MyMenu(QMenu): + + def __init__(self, config): + QMenu.__init__(self) + self.setToolTipsVisible(True) + self.config = config + + def addToggle(self, text: str, callback, *, tooltip=''): + m = self.addAction(text, callback) + m.setCheckable(True) + m.setToolTip(tooltip) + return m + + class MyTreeView(QTreeView): ROLE_CLIPBOARD_DATA = Qt.UserRole + 100 ROLE_CUSTOM_PAINT = Qt.UserRole + 101 @@ -754,11 +768,8 @@ def create_toolbar_buttons(self): self.toolbar_buttons = buttons return hbox - def create_toolbar_with_menu(self, title, menu_items): - menu = QMenu() - menu.setToolTipsVisible(True) - for k, v in menu_items: - menu.addAction(k, v) + def create_toolbar_with_menu(self, title): + menu = MyMenu(self.config) toolbar_button = QToolButton() toolbar_button.setIcon(read_QIcon("preferences.png")) toolbar_button.setMenu(menu) @@ -768,7 +779,7 @@ def create_toolbar_with_menu(self, title, menu_items): toolbar.addWidget(QLabel(title)) toolbar.addStretch() toolbar.addWidget(toolbar_button) - return toolbar + return toolbar, menu def show_toolbar(self, state, config=None): if state == self.toolbar_shown: From 37a0e125c66e367bedf2782fce124aee147011b6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Mar 2023 10:42:26 +0100 Subject: [PATCH 0326/1143] move config settings that are related to invoice creation to receive tab. --- electrum/gui/qt/receive_tab.py | 14 +++++++++++++- electrum/gui/qt/settings_dialog.py | 20 -------------------- electrum/gui/qt/util.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index cfa9d38ca..73ee71564 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -175,10 +175,16 @@ def on_receive_swap(): from .request_list import RequestList self.request_list = RequestList(self) self.toolbar, menu = self.request_list.create_toolbar_with_menu('') + menu.addConfig( + _('Add on-chain fallback to lightning requests'), 'bolt11_fallback', True, + callback=self.on_toggle_bolt11_fallback) + menu.addConfig( + _('Add lightning requests to bitcoin URIs'), 'bip21_lightning', False, + tooltip=_('This may result in large QR codes'), + callback=self.update_current_request) menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) - # layout vbox_g = QVBoxLayout() vbox_g.addLayout(grid) @@ -199,6 +205,12 @@ def on_receive_swap(): vbox.setStretchFactor(self.request_list, 60) self.request_list.update() # after parented and put into a layout, can update without flickering + def on_toggle_bolt11_fallback(self): + if not self.wallet.lnworker: + return + self.wallet.lnworker.clear_invoices_cache() + self.update_current_request() + def on_tab_changed(self, i): self.config.set_key('receive_tabs_index', i) title, data = self.get_tab_data(i) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 1dad08386..264927159 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -104,21 +104,6 @@ def on_nz(): self.app.update_status_signal.emit() nz.valueChanged.connect(on_nz) - # invoices - bolt11_fallback_cb = QCheckBox(_('Add on-chain fallback to lightning invoices')) - bolt11_fallback_cb.setChecked(bool(self.config.get('bolt11_fallback', True))) - bolt11_fallback_cb.setToolTip(_('Add fallback addresses to BOLT11 lightning invoices.')) - def on_bolt11_fallback(x): - self.config.set_key('bolt11_fallback', bool(x)) - bolt11_fallback_cb.stateChanged.connect(on_bolt11_fallback) - - bip21_lightning_cb = QCheckBox(_('Add lightning invoice to bitcoin URIs')) - bip21_lightning_cb.setChecked(bool(self.config.get('bip21_lightning', False))) - bip21_lightning_cb.setToolTip(_('This may create larger qr codes.')) - def on_bip21_lightning(x): - self.config.set_key('bip21_lightning', bool(x)) - bip21_lightning_cb.stateChanged.connect(on_bip21_lightning) - # lightning help_recov = _(messages.MSG_RECOVERABLE_CHANNELS) recov_cb = QCheckBox(_("Create recoverable channels")) @@ -426,10 +411,6 @@ def on_fiat_address(checked): gui_widgets.append((nz_label, nz)) gui_widgets.append((msat_cb, None)) gui_widgets.append((thousandsep_cb, None)) - invoices_widgets = [] - invoices_widgets.append((bolt11_fallback_cb, None)) - invoices_widgets.append((bip21_lightning_cb, None)) - lightning_widgets = [] lightning_widgets.append((recov_cb, None)) lightning_widgets.append((trampoline_cb, None)) @@ -452,7 +433,6 @@ def on_fiat_address(checked): tabs_info = [ (gui_widgets, _('Appearance')), - (invoices_widgets, _('Invoices')), (lightning_widgets, _('Lightning')), (fiat_widgets, _('Fiat')), (misc_widgets, _('Misc')), diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 29a8b8319..b0c31c8e9 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -582,6 +582,20 @@ def addToggle(self, text: str, callback, *, tooltip=''): m.setToolTip(tooltip) return m + def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=None): + b = self.config.get(name, default) + m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback)) + m.setCheckable(True) + m.setChecked(b) + m.setToolTip(tooltip) + return m + + def _do_toggle_config(self, name, default, callback): + b = self.config.get(name, default) + self.config.set_key(name, not b) + if callback: + callback() + class MyTreeView(QTreeView): ROLE_CLIPBOARD_DATA = Qt.UserRole + 100 From 98f052699679246de28abeaf5d31c25249485782 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Mar 2023 11:54:44 +0100 Subject: [PATCH 0327/1143] swap_dialog: minor fix --- electrum/gui/qt/swap_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 6db147234..381b05686 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -99,7 +99,7 @@ def init_recv_amount(self, recv_amount_sat): self.max_button.setChecked(True) self.spend_max() else: - recv_amount_sat = max(recv_amount_sat, self.swap_manager.min_amount) + recv_amount_sat = max(recv_amount_sat, self.swap_manager.get_min_amount()) self.recv_amount_e.setAmount(recv_amount_sat) def fee_slider_callback(self, dyn, pos, fee_rate): From 503776c0dee2a544c0d746e9b853f29fe0b54279 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 12 Mar 2023 13:30:11 +0100 Subject: [PATCH 0328/1143] move fiat columns show/hide settings from settings_dialog to tab toolbars --- electrum/exchange_rate.py | 27 ++--------- electrum/gui/qml/qefx.py | 4 +- electrum/gui/qml/qetransactionlistmodel.py | 7 ++- electrum/gui/qt/address_list.py | 14 ++++-- electrum/gui/qt/history_list.py | 38 +++++++++++---- electrum/gui/qt/main_window.py | 1 + electrum/gui/qt/settings_dialog.py | 55 ++++------------------ electrum/wallet.py | 6 +-- 8 files changed, 62 insertions(+), 90 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 480b5030e..fd3280c9c 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -583,7 +583,7 @@ async def run(self): await self._trigger.wait() self._trigger.clear() # we were manually triggered, so get historical rates - if self.is_enabled() and self.show_history(): + if self.is_enabled() and self.has_history(): self.exchange.get_historical_rates(self.ccy, self.cache_dir) except TaskTimeout: pass @@ -597,26 +597,8 @@ def set_enabled(self, b): self.config.set_key('use_exchange_rate', bool(b)) self.trigger_update() - def get_history_config(self, *, allow_none=False): - val = self.config.get('history_rates', None) - if val is None and allow_none: - return None - return bool(val) - - def set_history_config(self, b): - self.config.set_key('history_rates', bool(b)) - - def get_history_capital_gains_config(self): - return bool(self.config.get('history_rates_capital_gains', False)) - - def set_history_capital_gains_config(self, b): - self.config.set_key('history_rates_capital_gains', bool(b)) - - def get_fiat_address_config(self): - return bool(self.config.get('fiat_address')) - - def set_fiat_address_config(self, b): - self.config.set_key('fiat_address', bool(b)) + def has_history(self): + return self.is_enabled() and self.ccy in self.exchange.history_ccys() def get_currency(self) -> str: '''Use when dynamic fetching is needed''' @@ -625,9 +607,6 @@ def get_currency(self) -> str: def config_exchange(self): return self.config.get('use_exchange', DEFAULT_EXCHANGE) - def show_history(self): - return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys() - def set_currency(self, ccy: str): self.ccy = ccy self.config.set_key('currency', ccy, True) diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index ba1e0d1c7..689481cfa 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -63,12 +63,12 @@ def fiatCurrency(self, currency): historicRatesChanged = pyqtSignal() @pyqtProperty(bool, notify=historicRatesChanged) def historicRates(self): - return self.fx.get_history_config() + return self.fx.config.get('history_rates', True) @historicRates.setter def historicRates(self, checked): if checked != self.historicRates: - self.fx.set_history_config(checked) + self.fx.config.set_key('history_rates', bool(checked)) self.historicRatesChanged.emit() self.rateSourcesChanged.emit() diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 9081af66d..2f98fbd1d 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -157,8 +157,11 @@ def init_model(self, force: bool = False): return self._logger.debug('retrieving history') - history = self.wallet.get_full_history(onchain_domain=self.onchain_domain, - include_lightning=self.include_lightning) + history = self.wallet.get_full_history( + onchain_domain=self.onchain_domain, + include_lightning=self.include_lightning, + include_fiat=False, + ) txs = [] for key, tx in history.items(): txs.append(self.tx_to_model(tx)) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 379b2dc9f..130dfdc57 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -88,6 +88,7 @@ def __init__(self, parent): super().__init__(parent, self.create_menu, stretch_column=self.Columns.LABEL, editable_columns=[self.Columns.LABEL]) + self.main_window = parent self.wallet = self.parent.wallet self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) @@ -111,10 +112,14 @@ def __init__(self, parent): def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar(self.config)) + menu.addConfig(_('Show Fiat balances'), 'fiat_address', False, callback=self.main_window.app.update_fiat_signal.emit) hbox = self.create_toolbar_buttons() toolbar.insertLayout(1, hbox) return toolbar + def should_show_fiat(self): + return self.parent.fx and self.parent.fx.is_enabled() and self.config.get('fiat_address', False) + def get_toolbar_buttons(self): return QLabel(_("Filter:")), self.change_button, self.used_button @@ -124,9 +129,8 @@ def on_hide_toolbar(self): self.update() def refresh_headers(self): - fx = self.parent.fx - if fx and fx.get_fiat_address_config(): - ccy = fx.get_currency() + if self.should_show_fiat(): + ccy = self.parent.fx.get_currency() else: ccy = _('Fiat') headers = { @@ -211,7 +215,7 @@ def update(self): set_address = QPersistentModelIndex(address_idx) self.set_current_idx(set_address) # show/hide columns - if fx and fx.get_fiat_address_config(): + if self.should_show_fiat(): self.showColumn(self.Columns.FIAT_BALANCE) else: self.hideColumn(self.Columns.FIAT_BALANCE) @@ -228,7 +232,7 @@ def refresh_row(self, key, row): balance_text = self.parent.format_amount(balance, whitespaces=True) # create item fx = self.parent.fx - if fx and fx.get_fiat_address_config(): + if self.should_show_fiat(): rate = fx.exchange_rate() fiat_balance_str = fx.value_str(balance, rate) else: diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5325e394a..6a79f1477 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -262,6 +262,17 @@ def should_include_lightning_payments(self) -> bool: """Overridden in address_dialog.py""" return True + def should_show_fiat(self): + if not self.window.config.get('history_rates', False): + return False + fx = self.window.fx + if not fx or not fx.is_enabled(): + return False + return fx.has_history() + + def should_show_capital_gains(self): + return self.should_show_fiat() and self.window.config.get('history_rates_capital_gains', False) + @profiler def refresh(self, reason: str): self.logger.info(f"refreshing... reason: {reason}") @@ -280,7 +291,9 @@ def refresh(self, reason: str): transactions = wallet.get_full_history( self.window.fx, onchain_domain=self.get_domain(), - include_lightning=self.should_include_lightning_payments()) + include_lightning=self.should_include_lightning_payments(), + include_fiat=self.should_show_fiat(), + ) if transactions == self.transactions: return old_length = self._root.childCount() @@ -361,8 +374,8 @@ def set_visible(col: int, b: bool): set_visible(HistoryColumns.TXID, False) set_visible(HistoryColumns.SHORT_ID, False) # fiat - history = self.window.fx.show_history() - cap_gains = self.window.fx.get_history_capital_gains_config() + history = self.should_show_fiat() + cap_gains = self.should_show_capital_gains() set_visible(HistoryColumns.FIAT_VALUE, history) set_visible(HistoryColumns.FIAT_ACQ_PRICE, history and cap_gains) set_visible(HistoryColumns.FIAT_CAP_GAINS, history and cap_gains) @@ -412,7 +425,7 @@ def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDat fiat_title = 'n/a fiat value' fiat_acq_title = 'n/a fiat acquisition price' fiat_cg_title = 'n/a fiat capital gains' - if fx and fx.show_history(): + if self.should_show_fiat(): fiat_title = '%s '%fx.ccy + _('Value') fiat_acq_title = '%s '%fx.ccy + _('Acquisition price') fiat_cg_title = '%s '%fx.ccy + _('Capital Gains') @@ -473,6 +486,7 @@ def __init__(self, parent, model: HistoryModel): super().__init__(parent, self.create_menu, stretch_column=HistoryColumns.DESCRIPTION, editable_columns=[HistoryColumns.DESCRIPTION, HistoryColumns.FIAT_VALUE]) + self.main_window = parent self.config = parent.config self.hm = model self.proxy = HistorySortModel(self) @@ -529,14 +543,23 @@ def on_combo(self, x): def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') - menu.addToggle(_("&Filter Period"), lambda: self.toggle_toolbar(self.config)) + menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar(self.config)) + self.menu_fiat = menu.addConfig(_('Show Fiat Values'), 'history_rates', False, callback=self.main_window.app.update_fiat_signal.emit) + self.menu_capgains = menu.addConfig(_('Show Capital Gains'), 'history_rates_capital_gains', False, callback=self.main_window.app.update_fiat_signal.emit) menu.addAction(_("&Summary"), self.show_summary) menu.addAction(_("&Plot"), self.plot_history_dialog) menu.addAction(_("&Export"), self.export_history_dialog) hbox = self.create_toolbar_buttons() toolbar.insertLayout(1, hbox) + self.update_toolbar_menu() return toolbar + def update_toolbar_menu(self): + fx = self.main_window.fx + b = fx and fx.is_enabled() and fx.has_history() + self.menu_fiat.setEnabled(b) + self.menu_capgains.setEnabled(b) + def get_toolbar_buttons(self): return self.period_combo, self.start_button, self.end_button @@ -574,11 +597,10 @@ def on_date(date): return datetime.datetime(date.year, date.month, date.day) def show_summary(self): - fx = self.parent.fx - show_fiat = fx and fx.is_enabled() and fx.get_history_config() - if not show_fiat: + if not self.hm.should_show_fiat(): self.parent.show_message(_("Enable fiat exchange rate with history.")) return + fx = self.parent.fx h = self.wallet.get_detailed_history( from_timestamp = time.mktime(self.start_date.timetuple()) if self.start_date else None, to_timestamp = time.mktime(self.end_date.timetuple()) if self.end_date else None, diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 68649f32d..cf2c80334 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2463,6 +2463,7 @@ def update_fiat(self): self.send_tab.fiat_send_e.setVisible(b) self.receive_tab.fiat_receive_e.setVisible(b) self.history_model.refresh('update_fiat') + self.history_list.update_toolbar_menu() self.address_list.refresh_headers() self.address_list.update() self.update_status() diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 264927159..f00ec6c47 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -313,41 +313,25 @@ def on_be_edit(): block_ex_hbox_w.setLayout(block_ex_hbox) # Fiat Currency - hist_checkbox = QCheckBox() - hist_capgains_checkbox = QCheckBox() - fiat_address_checkbox = QCheckBox() + self.require_history_checkbox = QCheckBox() ccy_combo = QComboBox() ex_combo = QComboBox() def update_currencies(): if not self.fx: return - currencies = sorted(self.fx.get_currencies(self.fx.get_history_config())) + currencies = sorted(self.fx.get_currencies(self.require_history_checkbox.isChecked())) ccy_combo.clear() ccy_combo.addItems([_('None')] + currencies) if self.fx.is_enabled(): ccy_combo.setCurrentIndex(ccy_combo.findText(self.fx.get_currency())) - def update_history_cb(): - if not self.fx: return - hist_checkbox.setChecked(self.fx.get_history_config()) - hist_checkbox.setEnabled(self.fx.is_enabled()) - - def update_fiat_address_cb(): - if not self.fx: return - fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config()) - - def update_history_capgains_cb(): - if not self.fx: return - hist_capgains_checkbox.setChecked(self.fx.get_history_capital_gains_config()) - hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked()) - def update_exchanges(): if not self.fx: return b = self.fx.is_enabled() ex_combo.setEnabled(b) if b: - h = self.fx.get_history_config() + h = self.require_history_checkbox.isChecked() c = self.fx.get_currency() exchanges = self.fx.get_exchanges_by_ccy(c, h) else: @@ -365,7 +349,6 @@ def on_currency(hh): self.fx.set_enabled(b) if b and ccy != self.fx.ccy: self.fx.set_currency(ccy) - update_history_cb() update_exchanges() self.app.update_fiat_signal.emit() @@ -373,35 +356,17 @@ def on_exchange(idx): exchange = str(ex_combo.currentText()) if self.fx and self.fx.is_enabled() and exchange and exchange != self.fx.exchange.name(): self.fx.set_exchange(exchange) - - def on_history(checked): - if not self.fx: return - self.fx.set_history_config(checked) - update_exchanges() - if self.fx.is_enabled() and checked: - self.fx.trigger_update() - update_history_capgains_cb() self.app.update_fiat_signal.emit() - def on_history_capgains(checked): - if not self.fx: return - self.fx.set_history_capital_gains_config(checked) - self.app.update_fiat_signal.emit() - - def on_fiat_address(checked): - if not self.fx: return - self.fx.set_fiat_address_config(checked) - self.app.update_fiat_signal.emit() + def on_require_history(checked): + if not self.fx: + return + update_exchanges() update_currencies() - update_history_cb() - update_history_capgains_cb() - update_fiat_address_cb() update_exchanges() ccy_combo.currentIndexChanged.connect(on_currency) - hist_checkbox.stateChanged.connect(on_history) - hist_capgains_checkbox.stateChanged.connect(on_history_capgains) - fiat_address_checkbox.stateChanged.connect(on_fiat_address) + self.require_history_checkbox.stateChanged.connect(on_require_history) ex_combo.currentIndexChanged.connect(on_exchange) gui_widgets = [] @@ -419,9 +384,7 @@ def on_fiat_address(checked): fiat_widgets = [] fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo)) fiat_widgets.append((QLabel(_('Source')), ex_combo)) - fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox)) - fiat_widgets.append((QLabel(_('Show capital gains in history')), hist_capgains_checkbox)) - fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')), fiat_address_checkbox)) + fiat_widgets.append((QLabel(_('Show sources with historical data')), self.require_history_checkbox)) misc_widgets = [] misc_widgets.append((updatecheck_cb, None)) misc_widgets.append((filelogging_cb, None)) diff --git a/electrum/wallet.py b/electrum/wallet.py index af42eba51..441e1e6a7 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1189,7 +1189,7 @@ def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[ return is_paid, conf_needed @profiler - def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True): + def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False): transactions_tmp = OrderedDictWithIndex() # add on-chain txns onchain_history = self.get_onchain_history(domain=onchain_domain) @@ -1245,7 +1245,7 @@ def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=Tr item['value'] = Satoshis(value) balance += value item['balance'] = Satoshis(balance) - if fx and fx.is_enabled() and fx.get_history_config(): + if include_fiat: txid = item.get('txid') if not item.get('lightning') and txid: fiat_fields = self.get_tx_item_fiat(tx_hash=txid, amount_sat=value, fx=fx, tx_fee=item['fee_sat']) @@ -1272,7 +1272,7 @@ def get_detailed_history( and (from_height is not None or to_height is not None): raise Exception('timestamp and block height based filtering cannot be used together') - show_fiat = fx and fx.is_enabled() and fx.get_history_config() + show_fiat = fx and fx.is_enabled() and fx.has_history() out = [] income = 0 expenditures = 0 From eef1f0b2fd15f7da255b7c7b916dcd7af54c551c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 08:16:44 +0100 Subject: [PATCH 0329/1143] transaction_dialog: move tx_dialog_fetch_txin_data checkbox into toolbar --- electrum/gui/qt/transaction_dialog.py | 31 ++++++++++++--------------- electrum/gui/qt/util.py | 24 +++++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 1fb6ad5ce..4a7843e20 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -56,6 +56,7 @@ from electrum.util import ShortID, get_asyncio_loop from electrum.network import Network +from . import util from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, ShowQRLineEdit, text_dialog, char_width_in_lineedit, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, @@ -407,6 +408,14 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav vbox = QVBoxLayout() self.setLayout(vbox) + toolbar, menu = util.create_toolbar_with_menu(self.config, '') + menu.addConfig( + _('Download missing data'), 'tx_dialog_fetch_txin_data', False, + tooltip=_( + 'Download parent transactions from the network.\n' + 'Allows filling in missing fee and input details.'), + callback=self.maybe_fetch_txin_data) + vbox.addLayout(toolbar) vbox.addWidget(QLabel(_("Transaction ID:"))) self.tx_hash_e = ShowQRLineEdit('', self.config, title='Transaction ID') @@ -419,20 +428,6 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav self.io_widget = TxInOutWidget(self.main_window, self.wallet) vbox.addWidget(self.io_widget) - # add "fetch_txin_data" checkbox to io_widget - fetch_txin_data_cb = QCheckBox(_('Download input data')) - fetch_txin_data_cb.setChecked(bool(self.config.get('tx_dialog_fetch_txin_data', False))) - fetch_txin_data_cb.setToolTip(_('Download parent transactions from the network.\n' - 'Allows filling in missing fee and address details.')) - def on_fetch_txin_data_cb(x): - self.config.set_key('tx_dialog_fetch_txin_data', bool(x)) - if x: - self.initiate_fetch_txin_data() - fetch_txin_data_cb.stateChanged.connect(on_fetch_txin_data_cb) - self.io_widget.inheader_hbox.addStretch(1) - self.io_widget.inheader_hbox.addWidget(fetch_txin_data_cb) - self.io_widget.inheader_hbox.addStretch(10) - self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) @@ -517,8 +512,8 @@ def set_tx(self, tx: 'Transaction'): lambda: Network.run_from_another_thread( tx.add_info_from_network(self.wallet.network, timeout=10)), ) - elif self.config.get('tx_dialog_fetch_txin_data', False): - self.initiate_fetch_txin_data() + else: + self.maybe_fetch_txin_data() def do_broadcast(self): self.main_window.push_top_level_window(self) @@ -932,12 +927,14 @@ def on_finalize(self): def update_fee_fields(self): pass # overridden in subclass - def initiate_fetch_txin_data(self): + def maybe_fetch_txin_data(self): """Download missing input data from the network, asynchronously. Note: we fetch the prev txs, which allows calculating the fee and showing "input addresses". We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp), but this is not done currently. """ + if not self.config.get('tx_dialog_fetch_txin_data', False): + return tx = self.tx if not tx: return diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index b0c31c8e9..2b7e267e2 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -596,6 +596,18 @@ def _do_toggle_config(self, name, default, callback): if callback: callback() +def create_toolbar_with_menu(config, title): + menu = MyMenu(config) + toolbar_button = QToolButton() + toolbar_button.setIcon(read_QIcon("preferences.png")) + toolbar_button.setMenu(menu) + toolbar_button.setPopupMode(QToolButton.InstantPopup) + toolbar_button.setFocusPolicy(Qt.NoFocus) + toolbar = QHBoxLayout() + toolbar.addWidget(QLabel(title)) + toolbar.addStretch() + toolbar.addWidget(toolbar_button) + return toolbar, menu class MyTreeView(QTreeView): ROLE_CLIPBOARD_DATA = Qt.UserRole + 100 @@ -783,17 +795,7 @@ def create_toolbar_buttons(self): return hbox def create_toolbar_with_menu(self, title): - menu = MyMenu(self.config) - toolbar_button = QToolButton() - toolbar_button.setIcon(read_QIcon("preferences.png")) - toolbar_button.setMenu(menu) - toolbar_button.setPopupMode(QToolButton.InstantPopup) - toolbar_button.setFocusPolicy(Qt.NoFocus) - toolbar = QHBoxLayout() - toolbar.addWidget(QLabel(title)) - toolbar.addStretch() - toolbar.addWidget(toolbar_button) - return toolbar, menu + return create_toolbar_with_menu(self.config, title) def show_toolbar(self, state, config=None): if state == self.toolbar_shown: From b15387c89b8b5e4ef9ec9dcf87b791feb5fafb2b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 08:55:45 +0100 Subject: [PATCH 0330/1143] Qt send tab: move payto_edit input buttons to toolbar --- electrum/gui/qt/paytoedit.py | 2 +- electrum/gui/qt/qrtextedit.py | 31 ++++++++++++++++++------------- electrum/gui/qt/send_tab.py | 5 +++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 75d8b89ae..7b40c937d 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -65,7 +65,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): def __init__(self, send_tab: 'SendTab'): CompletionTextEdit.__init__(self) - ScanQRTextEdit.__init__(self, config=send_tab.config, setText=self._on_input_btn) + ScanQRTextEdit.__init__(self, config=send_tab.config, setText=self._on_input_btn, is_payto=True) Logger.__init__(self) self.send_tab = send_tab self.win = send_tab.window diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index 45ec66eff..5866d761c 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -30,44 +30,49 @@ def __init__( *, config: SimpleConfig, setText: Callable[[str], None] = None, + is_payto = False, ): ButtonsTextEdit.__init__(self, text) self.setReadOnly(False) - - input_qr_from_camera = partial( + self.on_qr_from_camera_input_btn = partial( self.input_qr_from_camera, config=config, allow_multi=allow_multi, show_error=self.show_error, setText=setText, ) - self.on_qr_from_camera_input_btn = input_qr_from_camera - - input_qr_from_screenshot = partial( + self.on_qr_from_screenshot_input_btn = partial( self.input_qr_from_screenshot, allow_multi=allow_multi, show_error=self.show_error, setText=setText, ) - self.on_qr_from_screenshot_input_btn = input_qr_from_screenshot - - input_file = partial(self.input_file, config=config, show_error=self.show_error, setText=setText) + self.on_input_file = partial( + self.input_file, + config=config, + show_error=self.show_error, + setText=setText, + ) + # for send tab, buttons are available in the toolbar + if not is_payto: + self.add_input_buttons(config, allow_multi, setText) + run_hook('scan_text_edit', self) + def add_input_buttons(self, config, allow_multi, setText): self.add_menu_button( options=[ - ("picture_in_picture.png", _("Read QR code from screen"), input_qr_from_screenshot), - ("file.png", _("Read file"), input_file), + ("picture_in_picture.png", _("Read QR code from screen"), self.on_qr_from_screenshot_input_btn), + ("file.png", _("Read file"), self.on_input_file), ], ) self.add_qr_input_from_camera_button(config=config, show_error=self.show_error, allow_multi=allow_multi, setText=setText) - run_hook('scan_text_edit', self) - def contextMenuEvent(self, e): m = self.createStandardContextMenu() m.addSeparator() - m.addAction(read_QIcon(get_iconname_camera()), _("Read QR code from camera"), self.on_qr_from_camera_input_btn) + m.addAction(read_QIcon(get_iconname_camera()), _("Read QR code with camera"), self.on_qr_from_camera_input_btn) m.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), self.on_qr_from_screenshot_input_btn) + m.addAction(read_QIcon("file.png"), _("Read file"), self.on_input_file) m.exec_(e.globalPos()) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index c4f56bfe2..640f1a03a 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -27,6 +27,7 @@ from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit +from .util import get_iconname_camera, get_iconname_qrcode, read_QIcon from .confirm_tx_dialog import ConfirmTxDialog if TYPE_CHECKING: @@ -150,7 +151,11 @@ def reset_max(text): from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) self.toolbar, menu = self.invoice_list.create_toolbar_with_menu('') + menu.addAction(read_QIcon(get_iconname_camera()), _("Read QR code with camera"), self.payto_e.on_qr_from_camera_input_btn) + menu.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), self.payto_e.on_qr_from_screenshot_input_btn) + menu.addAction(read_QIcon("file.png"), _("Read invoice from file"), self.payto_e.on_input_file) menu.addToggle(_("&Pay to many"), self.paytomany) + menu.addSeparator() menu.addAction(_("Import invoices"), self.window.import_invoices) menu.addAction(_("Export invoices"), self.window.export_invoices) From 4909cebdae5705bfcd10ae6e8c0af89bdf545b28 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 09:36:02 +0100 Subject: [PATCH 0331/1143] move recoverable channel option from preferences to new_channel_dialog toolbar --- electrum/gui/qt/new_channel_dialog.py | 10 +++++++++- electrum/gui/qt/settings_dialog.py | 11 ----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index 3b3059bd3..717b53dbc 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -9,7 +9,8 @@ from electrum import ecc from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates - +from electrum.gui import messages +from . import util from .util import (WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit) @@ -33,6 +34,13 @@ def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, m self.trampoline_names = list(self.trampolines.keys()) self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT vbox = QVBoxLayout(self) + toolbar, menu = util.create_toolbar_with_menu(self.config, '') + recov_tooltip = messages.to_rtf(_(messages.MSG_RECOVERABLE_CHANNELS)) + menu.addConfig( + _("Create recoverable channels"), 'use_recoverable_channels', True, + tooltip=recov_tooltip, + ).setEnabled(self.lnworker.can_have_recoverable_channels()) + vbox.addLayout(toolbar) msg = _('Choose a remote node and an amount to fund the channel.') if min_amount_sat: # only displayed if min_amount_sat is passed as parameter diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index f00ec6c47..12ee77f1a 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -105,16 +105,6 @@ def on_nz(): nz.valueChanged.connect(on_nz) # lightning - help_recov = _(messages.MSG_RECOVERABLE_CHANNELS) - recov_cb = QCheckBox(_("Create recoverable channels")) - enable_toggle_use_recoverable_channels = bool(self.wallet.lnworker and self.wallet.lnworker.can_have_recoverable_channels()) - recov_cb.setEnabled(enable_toggle_use_recoverable_channels) - recov_cb.setToolTip(messages.to_rtf(help_recov)) - recov_cb.setChecked(bool(self.config.get('use_recoverable_channels', True)) and enable_toggle_use_recoverable_channels) - def on_recov_checked(x): - self.config.set_key('use_recoverable_channels', bool(x)) - recov_cb.stateChanged.connect(on_recov_checked) - help_trampoline = _(messages.MSG_HELP_TRAMPOLINE) trampoline_cb = QCheckBox(_("Use trampoline routing (disable gossip)")) trampoline_cb.setToolTip(messages.to_rtf(help_trampoline)) @@ -377,7 +367,6 @@ def on_require_history(checked): gui_widgets.append((msat_cb, None)) gui_widgets.append((thousandsep_cb, None)) lightning_widgets = [] - lightning_widgets.append((recov_cb, None)) lightning_widgets.append((trampoline_cb, None)) lightning_widgets.append((instant_swaps_cb, None)) lightning_widgets.append((remote_wt_cb, self.watchtower_url_e)) From 519926ade3336f1049c3dd06c24443381dbe2128 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 09:53:08 +0100 Subject: [PATCH 0332/1143] move 'allow_instant_swaps' option from preferences dialog to swap_dialog toolbar --- electrum/gui/messages.py | 6 ++++++ electrum/gui/qt/settings_dialog.py | 11 ----------- electrum/gui/qt/swap_dialog.py | 10 +++++++++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/electrum/gui/messages.py b/electrum/gui/messages.py index 839c06e59..d4edfc0f0 100644 --- a/electrum/gui/messages.py +++ b/electrum/gui/messages.py @@ -12,6 +12,12 @@ def to_rtf(msg): If this is enabled, other nodes cannot open a channel to you. Channel recovery data is encrypted, so that only your wallet can decrypt it. However, blockchain analysis will be able to tell that the transaction was probably created by Electrum. """ +MSG_CONFIG_INSTANT_SWAPS = """ +If this option is checked, your client will complete reverse swaps before the funding transaction is confirmed. + +Note you are at risk of losing the funds in the swap, if the funding transaction never confirms. +""" + MSG_COOPERATIVE_CLOSE = """ Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees.""" diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 12ee77f1a..783dbce3d 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -122,16 +122,6 @@ def on_trampoline_checked(use_trampoline): util.trigger_callback('channels_updated', self.wallet) trampoline_cb.stateChanged.connect(on_trampoline_checked) - help_instant_swaps = ' '.join([ - _("If this option is checked, your client will complete reverse swaps before the funding transaction is confirmed."), - _("Note you are at risk of losing the funds in the swap, if the funding transaction never confirms.") - ]) - instant_swaps_cb = QCheckBox(_("Allow instant swaps")) - instant_swaps_cb.setToolTip(messages.to_rtf(help_instant_swaps)) - instant_swaps_cb.setChecked(bool(self.config.get('allow_instant_swaps', False))) - def on_instant_swaps_checked(allow_instant_swaps): - self.config.set_key('allow_instant_swaps', bool(allow_instant_swaps)) - instant_swaps_cb.stateChanged.connect(on_instant_swaps_checked) help_remote_wt = ' '.join([ _("A watchtower is a daemon that watches your channels and prevents the other party from stealing funds by broadcasting an old state."), @@ -368,7 +358,6 @@ def on_require_history(checked): gui_widgets.append((thousandsep_cb, None)) lightning_widgets = [] lightning_widgets.append((trampoline_cb, None)) - lightning_widgets.append((instant_swaps_cb, None)) lightning_widgets.append((remote_wt_cb, self.watchtower_url_e)) fiat_widgets = [] fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo)) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 381b05686..f2561d021 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -8,6 +8,8 @@ from electrum.lnutil import ln_dummy_address from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.gui import messages +from . import util from .util import (WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit) from .amountedit import BTCAmountEdit @@ -39,6 +41,12 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No self.channels = channels self.is_reverse = is_reverse if is_reverse is not None else True vbox = QVBoxLayout(self) + toolbar, menu = util.create_toolbar_with_menu(self.config, '') + menu.addConfig( + _("Allow instant swaps"), 'allow_instant_swaps', False, + tooltip=messages.to_rtf(messages.MSG_CONFIG_INSTANT_SWAPS), + ).setEnabled(self.lnworker.can_have_recoverable_channels()) + vbox.addLayout(toolbar) self.description_label = WWLabel(self.get_description()) self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point) self.recv_amount_e = BTCAmountEdit(self.window.get_decimal_point) @@ -302,7 +310,7 @@ def get_description(self): onchain_funds = "onchain funds" lightning_funds = "lightning funds" - return "Swap {fromType} for {toType}. This will increase your {capacityType} capacity. This service is powered by the Boltz backend.".format( + return "Swap {fromType} for {toType}.\nThis will increase your {capacityType} capacity.".format( fromType=lightning_funds if self.is_reverse else onchain_funds, toType=onchain_funds if self.is_reverse else lightning_funds, capacityType="receiving" if self.is_reverse else "sending", From e150a9ccad28683507388b0e24771dd5864880ee Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 10:02:47 +0100 Subject: [PATCH 0333/1143] restructure settings_dialog, create 'Units' tab --- electrum/gui/qt/settings_dialog.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 783dbce3d..abd3b1a84 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -352,10 +352,12 @@ def on_require_history(checked): gui_widgets = [] gui_widgets.append((lang_label, lang_combo)) gui_widgets.append((colortheme_label, colortheme_combo)) - gui_widgets.append((unit_label, unit_combo)) - gui_widgets.append((nz_label, nz)) - gui_widgets.append((msat_cb, None)) - gui_widgets.append((thousandsep_cb, None)) + gui_widgets.append((block_ex_label, block_ex_hbox_w)) + units_widgets = [] + units_widgets.append((unit_label, unit_combo)) + units_widgets.append((nz_label, nz)) + units_widgets.append((msat_cb, None)) + units_widgets.append((thousandsep_cb, None)) lightning_widgets = [] lightning_widgets.append((trampoline_cb, None)) lightning_widgets.append((remote_wt_cb, self.watchtower_url_e)) @@ -368,14 +370,14 @@ def on_require_history(checked): misc_widgets.append((filelogging_cb, None)) misc_widgets.append((alias_label, self.alias_e)) misc_widgets.append((qr_label, qr_combo)) - misc_widgets.append((block_ex_label, block_ex_hbox_w)) if len(choosers) > 1: misc_widgets.append((chooser_label, chooser_combo)) tabs_info = [ (gui_widgets, _('Appearance')), - (lightning_widgets, _('Lightning')), + (units_widgets, _('Units')), (fiat_widgets, _('Fiat')), + (lightning_widgets, _('Lightning')), (misc_widgets, _('Misc')), ] for widgets, name in tabs_info: From 439f1e6331097c1f36dd5ff8bdbc89f59a619a33 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 11:08:04 +0100 Subject: [PATCH 0334/1143] settings_dialog: add confirmation dialog before disabling trampoline --- electrum/gui/qt/settings_dialog.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index abd3b1a84..7dcbf20e0 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -106,13 +106,22 @@ def on_nz(): # lightning help_trampoline = _(messages.MSG_HELP_TRAMPOLINE) - trampoline_cb = QCheckBox(_("Use trampoline routing (disable gossip)")) + trampoline_cb = QCheckBox(_("Use trampoline routing")) trampoline_cb.setToolTip(messages.to_rtf(help_trampoline)) trampoline_cb.setChecked(not bool(self.config.get('use_gossip', False))) def on_trampoline_checked(use_trampoline): - use_gossip = not bool(use_trampoline) - self.config.set_key('use_gossip', use_gossip) - if use_gossip: + use_trampoline = bool(use_trampoline) + if not use_trampoline: + if not window.question('\n'.join([ + _("Are you sure you want to disable trampoline?"), + _("Without this option, Electrum will need to sync with the Lightning network on every start."), + _("This may impact the reliability of your payments."), + ])): + # FIXME: Qt bug? stateChanged not triggered on second click + trampoline_cb.setChecked(True) + return + self.config.set_key('use_gossip', not use_trampoline) + if not use_trampoline: self.network.start_gossip() else: self.network.run_from_another_thread( @@ -122,7 +131,6 @@ def on_trampoline_checked(use_trampoline): util.trigger_callback('channels_updated', self.wallet) trampoline_cb.stateChanged.connect(on_trampoline_checked) - help_remote_wt = ' '.join([ _("A watchtower is a daemon that watches your channels and prevents the other party from stealing funds by broadcasting an old state."), _("If you have private a watchtower, enter its URL here."), From 303ad02d17a580912bb9d06b14f0ea75dc7e2ff4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 11:41:36 +0100 Subject: [PATCH 0335/1143] privacy analysis: add warning about tx downstream of address reuse --- electrum/gui/qt/utxo_dialog.py | 9 +++++++-- electrum/gui/qt/utxo_list.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 8d539e419..760cc100e 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -84,6 +84,7 @@ def __init__(self, window: 'ElectrumWindow', utxo: 'PartialTxInput'): ASCII_PIPE = '│' ASCII_SPACE = ' ' + self.num_reuse = 0 def print_ascii_tree(_txid, prefix, is_last, is_uncle): if _txid not in parents: return @@ -95,6 +96,7 @@ def print_ascii_tree(_txid, prefix, is_last, is_uncle): c = '' if _txid == txid else (ASCII_EDGE if is_last else ASCII_BRANCH) cursor.insertText(prefix + c, ext) if is_uncle: + self.num_reuse += 1 lnk = QTextCharFormat(self.txo_color_uncle.text_char_format) else: lnk = QTextCharFormat(self.txo_color_parent.text_char_format) @@ -119,7 +121,10 @@ def print_ascii_tree(_txid, prefix, is_last, is_uncle): vbox = QVBoxLayout() vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) - vbox.addWidget(QLabel(_("This UTXO has {} parent transactions in your wallet").format(num_parents))) + msg = _("This UTXO has {} parent transactions in your wallet.").format(num_parents) + if self.num_reuse: + msg += '\n' + _('This does not include transactions that are downstream of address reuse.') + vbox.addWidget(WWLabel(msg)) vbox.addWidget(self.parents_list) legend_hbox = QHBoxLayout() legend_hbox.setContentsMargins(0, 0, 0, 0) @@ -128,7 +133,7 @@ def print_ascii_tree(_txid, prefix, is_last, is_uncle): legend_hbox.addWidget(self.txo_color_uncle.legend_label) vbox.addLayout(legend_hbox) self.txo_color_parent.legend_label.setVisible(True) - self.txo_color_uncle.legend_label.setVisible(True) + self.txo_color_uncle.legend_label.setVisible(bool(self.num_reuse)) vbox.addLayout(Buttons(CloseButton(self))) self.setLayout(vbox) # set cursor to top diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index dc23805a0..409dc222e 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -262,7 +262,7 @@ def create_menu(self, position): tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) - menu.addAction(_("View parents"), lambda: self.parent.show_utxo(utxo)) + menu.addAction(_("Privacy analysis"), lambda: self.parent.show_utxo(utxo)) # fully spend menu_spend = menu.addMenu(_("Fully spend") + '…') m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) From cbab50e4ceb1fbf662bb35c89f7929b0c12e9979 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 12:11:50 +0100 Subject: [PATCH 0336/1143] channels_list: move swap and rebalance buttons into toolbar --- electrum/gui/qt/channels_list.py | 42 ++++++++++---------------------- electrum/gui/qt/main_window.py | 3 +++ 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 100e37b01..81d2515ef 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -74,7 +74,6 @@ def __init__(self, parent): self.network = self.parent.network self.wallet = self.parent.wallet self.setSortingEnabled(True) - self.selectionModel().selectionChanged.connect(self.on_selection_changed) @property # property because lnworker might be initialized at runtime @@ -208,24 +207,22 @@ def get_rebalance_pair(self): def on_rebalance(self): chan1, chan2 = self.get_rebalance_pair() + if chan1 is None: + self.parent.show_error("Select two active channels to rebalance.") + return self.parent.rebalance_dialog(chan1, chan2) - def on_selection_changed(self): - chan1, chan2 = self.get_rebalance_pair() - self.rebalance_button.setEnabled(chan1 is not None) - def create_menu(self, position): menu = QMenu() menu.setSeparatorsCollapsible(True) # consecutive separators are merged together selected = self.selected_in_column(self.Columns.NODE_ALIAS) if not selected: - menu.addAction(_("Import channel backup"), lambda: self.parent.do_process_from_text_channel_backup()) menu.exec_(self.viewport().mapToGlobal(position)) return if len(selected) == 2: chan1, chan2 = self.get_rebalance_pair() if chan1 and chan2: - menu.addAction(_("Rebalance"), lambda: self.parent.rebalance_dialog(chan1, chan2)) + menu.addAction(_("Rebalance channels"), lambda: self.parent.rebalance_dialog(chan1, chan2)) menu.exec_(self.viewport().mapToGlobal(position)) return elif len(selected) > 2: @@ -345,31 +342,18 @@ def update_can_send(self, lnworker: LNWallet): + _('can receive') + ' ' + self.parent.format_amount(lnworker.num_sats_can_receive())\ + ' ' + self.parent.base_unit() self.can_send_label.setText(msg) - self.update_swap_button(lnworker) - - def update_swap_button(self, lnworker: LNWallet): - if lnworker.num_sats_can_send() or lnworker.num_sats_can_receive(): - self.swap_button.setEnabled(True) - else: - self.swap_button.setEnabled(False) def create_toolbar(self, config): - h = QHBoxLayout() - self.can_send_label = QLabel('') - h.addWidget(self.can_send_label) - h.addStretch() - self.rebalance_button = EnterButton(_('Rebalance'), lambda x: self.on_rebalance()) - self.rebalance_button.setToolTip("Select two active channels to rebalance.") - self.rebalance_button.setDisabled(True) - self.swap_button = EnterButton(_('Swap'), lambda x: self.parent.run_swap_dialog()) - self.swap_button.setToolTip("Have at least one channel to do swaps.") - self.swap_button.setDisabled(True) - self.new_channel_button = EnterButton(_('Open Channel'), self.new_channel_with_warning) + toolbar, menu = self.create_toolbar_with_menu('') + self.can_send_label = toolbar.itemAt(0).widget() + menu.addAction(_('Rebalance channels'), lambda: self.on_rebalance()) + menu.addAction(_('Submarine swap'), lambda: self.parent.run_swap_dialog()) + menu.addSeparator() + menu.addAction(_("Import channel backup"), lambda: self.parent.do_process_from_text_channel_backup()) + self.new_channel_button = EnterButton(_('New Channel'), self.new_channel_with_warning) self.new_channel_button.setEnabled(self.parent.wallet.has_lightning()) - h.addWidget(self.new_channel_button) - h.addWidget(self.rebalance_button) - h.addWidget(self.swap_button) - return h + toolbar.insertWidget(2, self.new_channel_button) + return toolbar def new_channel_with_warning(self): lnworker = self.parent.wallet.lnworker diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index cf2c80334..2e8dbf69f 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1111,6 +1111,9 @@ def run_swap_dialog(self, is_reverse=None, recv_amount_sat=None, channels=None): if not self.network: self.show_error(_("You are offline.")) return + if not self.wallet.lnworker.num_sats_can_send() and not self.wallet.lnworker.num_sats_can_receive(): + self.show_error(_("You do not have liquidity in your active channels.")) + return def get_pairs_thread(): self.network.run_from_another_thread(self.wallet.lnworker.swap_manager.get_pairs()) try: From 0ebec200e284220ab57a7b97158291bafdd2652b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 13 Mar 2023 12:17:00 +0100 Subject: [PATCH 0337/1143] qml: render fee histogram as a HSV gradient over next 25 blocks --- .../gui/qml/components/NetworkOverview.qml | 89 +++++++++++++++---- electrum/gui/qml/qenetwork.py | 26 +++++- electrum/gui/qml/qeserverlistmodel.py | 2 +- 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 91c0f79c7..f8ba8a6cc 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -79,8 +79,76 @@ Pane { text: qsTr('Network fees:'); color: Material.accentColor } - Label { - id: feeHistogram + Item { + id: histogramRoot + Layout.fillWidth: true + implicitHeight: histogramLayout.height + + ColumnLayout { + id: histogramLayout + width: parent.width + spacing: 0 + RowLayout { + Layout.fillWidth: true + height: 28 + spacing: 0 + Repeater { + model: Network.feeHistogram.histogram + Rectangle { + Layout.preferredWidth: 300 * (modelData[1] / Network.feeHistogram.total) + Layout.fillWidth: true + height: parent.height + color: Qt.hsva(2/3-(2/3*(Math.log(modelData[0])/Math.log(Math.max(25, Network.feeHistogram.max_fee)))), 0.8, 1, 1) + } + } + } + RowLayout { + Layout.fillWidth: true + height: 3 + spacing: 0 + + Repeater { + model: Network.feeHistogram.total / 1000000 + RowLayout { + height: parent.height + spacing: 0 + Rectangle { + Layout.preferredWidth: 1 + Layout.fillWidth: false + height: parent.height + width: 1 + color: 'white' + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + } + } + } + Rectangle { + Layout.preferredWidth: 1 + Layout.fillWidth: false + height: parent.height + width: 1 + color: 'white' + } + } + RowLayout { + Layout.fillWidth: true + Label { + text: '< ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) + font.pixelSize: constants.fontSizeXSmall + color: Material.accentColor + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: qsTr('%1 sat/vB').arg(Math.floor(Network.feeHistogram.min_fee)) + ' >' + font.pixelSize: constants.fontSizeXSmall + color: Material.accentColor + } + } + } } Heading { @@ -191,21 +259,6 @@ Pane { } } - function setFeeHistogram() { - var txt = '' - Network.feeHistogram.forEach(function(item) { - txt = txt + item[0] + ': ' + item[1] + '\n'; - }) - feeHistogram.text = txt.trim() - } - - Connections { - target: Network - function onFeeHistogramUpdated() { - setFeeHistogram() - } - } - Component { id: serverConfig ServerConfigDialog { @@ -219,6 +272,4 @@ Pane { onClosed: destroy() } } - - Component.onCompleted: setFeeHistogram() } diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index ebe780d47..e7675c920 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -87,8 +87,30 @@ def on_event_status(self, *args): @event_listener def on_event_fee_histogram(self, histogram): - self._logger.debug('fee histogram updated') - self._fee_histogram = histogram if histogram else [] + self._logger.debug(f'fee histogram updated: {repr(histogram)}') + if histogram is None: + histogram = [] + self.update_histogram(histogram) + + def update_histogram(self, histogram): + # cap the histogram to a limited number of megabytes + bytes_limit=25*1000*1000 + bytes_current = 0 + capped_histogram = [] + for item in sorted(histogram, key=lambda x: x[0], reverse=True): + if bytes_current >= bytes_limit: + break + slot = min(item[1], bytes_limit-bytes_current) + bytes_current += slot + capped_histogram.append([item[0], slot]) + + # add clamping attributes for the GUI + self._fee_histogram = { + 'histogram': capped_histogram, + 'total': bytes_current, + 'min_fee': capped_histogram[-1][0], + 'max_fee': capped_histogram[0][0] + } self.feeHistogramUpdated.emit() @event_listener diff --git a/electrum/gui/qml/qeserverlistmodel.py b/electrum/gui/qml/qeserverlistmodel.py index 40ab39b5a..d839b1b79 100644 --- a/electrum/gui/qml/qeserverlistmodel.py +++ b/electrum/gui/qml/qeserverlistmodel.py @@ -132,7 +132,7 @@ def init_model(self): server['name'] = s.net_addr_str() server['address'] = server['name'] - self._logger.debug(f'adding server: {repr(server)}') + # self._logger.debug(f'adding server: {repr(server)}') servers.append(server) self.beginInsertRows(QModelIndex(), 0, len(servers) - 1) From 2bf2d815d2506d596ce97efddc05ed0e62cf6ebd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 12:25:41 +0100 Subject: [PATCH 0338/1143] Qt: add tx and address counters --- electrum/gui/qt/address_list.py | 3 +++ electrum/gui/qt/history_list.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 130dfdc57..b16e6e3f4 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -111,6 +111,7 @@ def __init__(self, parent): def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') + self.num_addr_label = toolbar.itemAt(0).widget() menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar(self.config)) menu.addConfig(_('Show Fiat balances'), 'fiat_address', False, callback=self.main_window.app.update_fiat_signal.emit) hbox = self.create_toolbar_buttons() @@ -221,6 +222,8 @@ def update(self): self.hideColumn(self.Columns.FIAT_BALANCE) self.filter() self.proxy.setDynamicSortFilter(True) + # update counter + self.num_addr_label.setText(_("{} addresses").format(len(addr_list))) def refresh_row(self, key, row): assert row is not None diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 6a79f1477..eef8d6cea 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -366,6 +366,10 @@ def refresh(self, reason: str): if not tx_item.get('lightning', False): tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info) + # update counter + num_tx = len(self.transactions) + if self.view: + self.view.num_tx_label.setText(_("{} transactions").format(num_tx)) def set_visibility_of_columns(self): def set_visible(col: int, b: bool): @@ -543,6 +547,7 @@ def on_combo(self, x): def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') + self.num_tx_label = toolbar.itemAt(0).widget() menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar(self.config)) self.menu_fiat = menu.addConfig(_('Show Fiat Values'), 'history_rates', False, callback=self.main_window.app.update_fiat_signal.emit) self.menu_capgains = menu.addConfig(_('Show Capital Gains'), 'history_rates_capital_gains', False, callback=self.main_window.app.update_fiat_signal.emit) From 90b46885a95a6c47c6bebaae6dd3828bec5c40aa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 12:55:30 +0100 Subject: [PATCH 0339/1143] address_dialog: set num_tx_label --- electrum/gui/qt/address_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py index 36cd426c8..c34799e6f 100644 --- a/electrum/gui/qt/address_dialog.py +++ b/electrum/gui/qt/address_dialog.py @@ -102,10 +102,11 @@ def __init__(self, window: 'ElectrumWindow', address: str, *, parent=None): der_path_e.setReadOnly(True) vbox.addWidget(der_path_e) - vbox.addWidget(QLabel(_("History"))) addr_hist_model = AddressHistoryModel(self.window, self.address) self.hw = HistoryList(self.window, addr_hist_model) + self.hw.num_tx_label = QLabel('') addr_hist_model.set_view(self.hw) + vbox.addWidget(self.hw.num_tx_label) vbox.addWidget(self.hw) vbox.addLayout(Buttons(CloseButton(self))) From 375ae851ecfd024b730b872405a5e6c138baccee Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 13:14:03 +0000 Subject: [PATCH 0340/1143] qt tx dialog: better size policy. for nicer window resizing --- electrum/gui/qt/transaction_dialog.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 4a7843e20..05aef8a46 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -38,7 +38,7 @@ from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap, QCursor from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QWidget, QGridLayout, QTextEdit, QFrame, QAction, QToolButton, QMenu, QCheckBox, QTextBrowser, QToolTip, - QApplication) + QApplication, QSizePolicy) import qrcode from qrcode import exceptions @@ -145,6 +145,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): vbox.addLayout(outheader_hbox) vbox.addWidget(self.outputs_textedit) self.setLayout(vbox) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def update(self, tx: Optional[Transaction]): self.tx = tx @@ -852,6 +853,10 @@ def update(self): def add_tx_stats(self, vbox): hbox_stats = QHBoxLayout() + hbox_stats.setContentsMargins(0, 0, 0, 0) + hbox_stats_w = QWidget() + hbox_stats_w.setLayout(hbox_stats) + hbox_stats_w.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) # left column vbox_left = QVBoxLayout() @@ -904,7 +909,7 @@ def add_tx_stats(self, vbox): vbox_right.addStretch(1) hbox_stats.addLayout(vbox_right, 50) - vbox.addLayout(hbox_stats) + vbox.addWidget(hbox_stats_w) # below columns self.block_hash_label = TxDetailLabel(word_wrap=True) From b690f2e5cd2ea2614c2552c009d2144dedf22d63 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 14:23:36 +0100 Subject: [PATCH 0341/1143] qr_window: keep menu in sync with actual visibility --- electrum/gui/qt/main_window.py | 1 + electrum/gui/qt/qrwindow.py | 5 ++++- electrum/gui/qt/receive_tab.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2e8dbf69f..f22adc065 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1084,6 +1084,7 @@ def toggle_qr_window(self): else: self.qr_window_geometry = self.qr_window.geometry() self.qr_window.setVisible(False) + self.receive_tab.update_receive_qr_window() def show_send_tab(self): self.tabs.setCurrentIndex(self.tabs.indexOf(self.send_tab)) diff --git a/electrum/gui/qt/qrwindow.py b/electrum/gui/qt/qrwindow.py index eca450eb3..d70d694f5 100644 --- a/electrum/gui/qt/qrwindow.py +++ b/electrum/gui/qt/qrwindow.py @@ -35,7 +35,7 @@ class QR_Window(QWidget): def __init__(self, win): QWidget.__init__(self) - self.win = win + self.main_window = win self.setWindowTitle('Electrum - '+_('Payment Request')) self.setMinimumSize(800, 800) self.setFocusPolicy(Qt.NoFocus) @@ -43,3 +43,6 @@ def __init__(self, win): self.qrw = QRCodeWidget() main_box.addWidget(self.qrw, 1) self.setLayout(main_box) + + def closeEvent(self, event): + self.main_window.receive_tab.qr_menu_action.setChecked(False) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 73ee71564..f45d8d8fb 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -182,7 +182,7 @@ def on_receive_swap(): _('Add lightning requests to bitcoin URIs'), 'bip21_lightning', False, tooltip=_('This may result in large QR codes'), callback=self.update_current_request) - menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) + self.qr_menu_action = menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) # layout From a7e5349a58c787bcb84e38f9e5e1a38eda29a830 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 13 Mar 2023 14:24:11 +0100 Subject: [PATCH 0342/1143] qml: clamp min fees in histogram to 1, server can report invalid 0 fees --- electrum/gui/qml/qenetwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index e7675c920..6f5bc047a 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -102,7 +102,7 @@ def update_histogram(self, histogram): break slot = min(item[1], bytes_limit-bytes_current) bytes_current += slot - capped_histogram.append([item[0], slot]) + capped_histogram.append([max(1, item[0]), slot]) # clamped to [1,inf] # add clamping attributes for the GUI self._fee_histogram = { From 32ee70438cd4feaac21d345e6771dd767e171318 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 14:30:45 +0100 Subject: [PATCH 0343/1143] Address filter: remove label --- electrum/gui/qt/address_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index b16e6e3f4..19052f902 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -47,7 +47,7 @@ class AddressUsageStateFilter(IntEnum): def ui_text(self) -> str: return { - self.ALL: _('All'), + self.ALL: _('All status'), self.UNUSED: _('Unused'), self.FUNDED: _('Funded'), self.USED_AND_EMPTY: _('Used'), @@ -62,7 +62,7 @@ class AddressTypeFilter(IntEnum): def ui_text(self) -> str: return { - self.ALL: _('All'), + self.ALL: _('All types'), self.RECEIVING: _('Receiving'), self.CHANGE: _('Change'), }[self] @@ -122,7 +122,7 @@ def should_show_fiat(self): return self.parent.fx and self.parent.fx.is_enabled() and self.config.get('fiat_address', False) def get_toolbar_buttons(self): - return QLabel(_("Filter:")), self.change_button, self.used_button + return self.change_button, self.used_button def on_hide_toolbar(self): self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter From 0f1fe1b1f1dcae3ceb96a6fd64c49b5ea43f8ee2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 14:38:21 +0100 Subject: [PATCH 0344/1143] address_list: fix counter (was not displaying filtered count) --- electrum/gui/qt/address_list.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 19052f902..4a5b3db38 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -172,6 +172,7 @@ def update(self): self.refresh_headers() fx = self.parent.fx set_address = None + num_shown = 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) @@ -185,6 +186,7 @@ def update(self): continue if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty: continue + num_shown += 1 labels = ['', address, '', '', '', ''] address_item = [QStandardItem(e) for e in labels] # align text and set fonts @@ -223,7 +225,7 @@ def update(self): self.filter() self.proxy.setDynamicSortFilter(True) # update counter - self.num_addr_label.setText(_("{} addresses").format(len(addr_list))) + self.num_addr_label.setText(_("{} addresses").format(num_shown)) def refresh_row(self, key, row): assert row is not None From c39653c796575766ee9fba31ec6b65411dfce4a6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 14:21:40 +0000 Subject: [PATCH 0345/1143] qt wallet info dlg: make mess smaller --- electrum/gui/qt/main_window.py | 50 ++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f22adc065..e7060d215 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1729,28 +1729,34 @@ def show_wallet_info(self): keystore_types = [k.get_type_text() for k in self.wallet.get_keystores()] grid = QGridLayout() basename = os.path.basename(self.wallet.storage.path) - grid.addWidget(WWLabel(_("Wallet name")+ ':'), 0, 0) - grid.addWidget(WWLabel(basename), 0, 1) - grid.addWidget(WWLabel(_("Wallet type")+ ':'), 1, 0) - grid.addWidget(WWLabel(wallet_type), 1, 1) - grid.addWidget(WWLabel(_("Script type")+ ':'), 2, 0) - grid.addWidget(WWLabel(self.wallet.txin_type), 2, 1) - grid.addWidget(WWLabel(_("Seed available") + ':'), 3, 0) - grid.addWidget(WWLabel(str(seed_available)), 3, 1) + cur_row = 0 + grid.addWidget(WWLabel(_("Wallet name")+ ':'), cur_row, 0) + grid.addWidget(WWLabel(basename), cur_row, 1) + cur_row += 1 + grid.addWidget(WWLabel(_("Wallet type")+ ':'), cur_row, 0) + grid.addWidget(WWLabel(wallet_type), cur_row, 1) + cur_row += 1 + grid.addWidget(WWLabel(_("Script type")+ ':'), cur_row, 0) + grid.addWidget(WWLabel(self.wallet.txin_type), cur_row, 1) + cur_row += 1 + grid.addWidget(WWLabel(_("Seed available") + ':'), cur_row, 0) + grid.addWidget(WWLabel(str(seed_available)), cur_row, 1) + cur_row += 1 if len(keystore_types) <= 1: - grid.addWidget(WWLabel(_("Keystore type") + ':'), 4, 0) + grid.addWidget(WWLabel(_("Keystore type") + ':'), cur_row, 0) ks_type = str(keystore_types[0]) if keystore_types else _('No keystore') - grid.addWidget(WWLabel(ks_type), 4, 1) + grid.addWidget(WWLabel(ks_type), cur_row, 1) + cur_row += 1 # lightning - grid.addWidget(WWLabel(_('Lightning') + ':'), 5, 0) + grid.addWidget(WWLabel(_('Lightning') + ':'), cur_row, 0) from .util import IconLabel if self.wallet.has_lightning(): if self.wallet.lnworker.has_deterministic_node_id(): - grid.addWidget(WWLabel(_('Enabled')), 5, 1) + grid.addWidget(WWLabel(_('Enabled')), cur_row, 1) else: label = IconLabel(text='Enabled, non-recoverable channels') label.setIcon(read_QIcon('nocloud')) - grid.addWidget(label, 5, 1) + grid.addWidget(label, cur_row, 1) if self.wallet.db.get('seed_type') == 'segwit': msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. " "This means that you must save a backup of your wallet everytime you create a new channel.\n\n" @@ -1759,20 +1765,24 @@ def show_wallet_info(self): msg = _("Your channels cannot be recovered from seed. " "This means that you must save a backup of your wallet everytime you create a new channel.\n\n" "If you want to have recoverable channels, you must create a new wallet with an Electrum seed") - grid.addWidget(HelpButton(msg), 5, 3) - grid.addWidget(WWLabel(_('Lightning Node ID:')), 7, 0) + grid.addWidget(HelpButton(msg), cur_row, 3) + cur_row += 1 + grid.addWidget(WWLabel(_('Lightning Node ID:')), cur_row, 0) + cur_row += 1 nodeid_text = self.wallet.lnworker.node_keypair.pubkey.hex() nodeid_e = ShowQRLineEdit(nodeid_text, self.config, title=_("Node ID")) - grid.addWidget(nodeid_e, 8, 0, 1, 4) + grid.addWidget(nodeid_e, cur_row, 0, 1, 4) + cur_row += 1 else: if self.wallet.can_have_lightning(): - grid.addWidget(WWLabel('Not enabled'), 5, 1) + grid.addWidget(WWLabel('Not enabled'), cur_row, 1) button = QPushButton(_("Enable")) button.pressed.connect(lambda: self.init_lightning_dialog(dialog)) - grid.addWidget(button, 5, 3) + grid.addWidget(button, cur_row, 3) else: - grid.addWidget(WWLabel(_("Not available for this wallet.")), 5, 1) - grid.addWidget(HelpButton(_("Lightning is currently restricted to HD wallets with p2wpkh addresses.")), 5, 2) + grid.addWidget(WWLabel(_("Not available for this wallet.")), cur_row, 1) + grid.addWidget(HelpButton(_("Lightning is currently restricted to HD wallets with p2wpkh addresses.")), cur_row, 2) + cur_row += 1 vbox.addLayout(grid) labels_clayout = None From 2571eeeecdbe6f5d8f695a108ab56b3e66aba31b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 13 Mar 2023 15:35:35 +0100 Subject: [PATCH 0346/1143] coins tab: add toolbar. --- electrum/gui/qt/utxo_list.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 409dc222e..05987d7f3 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -70,12 +70,16 @@ def __init__(self, parent): self._spend_set = set() self._utxo_dict = {} self.wallet = self.parent.wallet - self.std_model = QStandardItemModel(self) self.setModel(self.std_model) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) - self.update() + + def create_toolbar(self, config): + toolbar, menu = self.create_toolbar_with_menu('') + self.num_coins_label = toolbar.itemAt(0).widget() + menu.addAction(_('Coin control'), lambda: self.add_selection_to_coincontrol()) + return toolbar def update(self): # not calling maybe_defer_update() as it interferes with coincontrol status bar @@ -103,6 +107,7 @@ def update(self): self.refresh_row(name, idx) self.filter() self.update_coincontrol_bar() + self.num_coins_label.setText(_('{} unspent transaction outputs').format(len(utxos))) def update_coincontrol_bar(self): # update coincontrol status bar @@ -142,9 +147,9 @@ def refresh_row(self, key, row): utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True)) utxo_item[self.Columns.OUTPOINT].setToolTip(f"{key}\n{_('Coin is frozen')}") - def get_selected_outpoints(self) -> Optional[List[str]]: + def get_selected_outpoints(self) -> List[str]: if not self.model(): - return None + return [] items = self.selected_in_column(self.Columns.OUTPOINT) return [x.data(self.ROLE_PREVOUT_STR) for x in items] @@ -172,6 +177,17 @@ def clear_coincontrol(self): self._spend_set.clear() self._refresh_coincontrol() + def add_selection_to_coincontrol(self): + if bool(self._spend_set): + self.clear_coincontrol() + return + selected = self.get_selected_outpoints() + coins = [self._utxo_dict[name] for name in selected] + if not coins: + self.parent.show_error(_('You need to select coins from the list first.\nUse ctrl+left mouse button to select multiple items')) + return + self.add_to_coincontrol(coins) + def _refresh_coincontrol(self): self.refresh_all() self.update_coincontrol_bar() @@ -244,8 +260,6 @@ def pay_to_clipboard_address(self, coins): def create_menu(self, position): selected = self.get_selected_outpoints() - if selected is None: - return menu = QMenu() menu.setSeparatorsCollapsible(True) # consecutive separators are merged together coins = [self._utxo_dict[name] for name in selected] From c29e82053fe9f15d28dec5004c02e4f7af40eb4e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 13 Mar 2023 15:49:07 +0100 Subject: [PATCH 0347/1143] qml: use config.FEERATE_DEFAULT_RELAY as lower bound for fee histogram --- electrum/gui/qml/qenetwork.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 6f5bc047a..d32a93ac4 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -3,6 +3,7 @@ from electrum.logging import get_logger from electrum import constants from electrum.interface import ServerAddr +from electrum.simple_config import FEERATE_DEFAULT_RELAY from .util import QtEventListener, event_listener from .qeserverlistmodel import QEServerListModel @@ -102,7 +103,7 @@ def update_histogram(self, histogram): break slot = min(item[1], bytes_limit-bytes_current) bytes_current += slot - capped_histogram.append([max(1, item[0]), slot]) # clamped to [1,inf] + capped_histogram.append([max(FEERATE_DEFAULT_RELAY/1000, item[0]), slot]) # clamped to [FEERATE_DEFAULT_RELAY/1000,inf] # add clamping attributes for the GUI self._fee_histogram = { From 9e0d7b61bb183c46cfe5c616f524ee72506c55bf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 15:03:36 +0000 Subject: [PATCH 0348/1143] util.format_time: trivial clean-up --- electrum/util.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index a0213a583..3d1723f95 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -771,14 +771,15 @@ def quantize_feerate(fee) -> Union[None, Decimal, int]: return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN) -def timestamp_to_datetime(timestamp: Optional[int]) -> Optional[datetime]: +def timestamp_to_datetime(timestamp: Union[int, float, None]) -> Optional[datetime]: if timestamp is None: return None return datetime.fromtimestamp(timestamp) -def format_time(timestamp): + +def format_time(timestamp: Union[int, float, None]) -> str: date = timestamp_to_datetime(timestamp) - return date.isoformat(' ')[:-3] if date else _("Unknown") + return date.isoformat(' ', timespec="minutes") if date else _("Unknown") # Takes a timestamp and returns a string with the approximation of the age From 950065a3de61a35ed59d5f10612211d752554db5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 15:15:50 +0000 Subject: [PATCH 0349/1143] Store file creation date and version in db Store the electrum version used to create a wallet file and a timestamp, in the file itself. This can be useful for debugging. --- electrum/gui/qt/main_window.py | 4 ++++ electrum/wallet_db.py | 37 +++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e7060d215..ac7f1f844 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1733,6 +1733,10 @@ def show_wallet_info(self): grid.addWidget(WWLabel(_("Wallet name")+ ':'), cur_row, 0) grid.addWidget(WWLabel(basename), cur_row, 1) cur_row += 1 + if db_metadata := self.wallet.db.get_db_metadata(): + grid.addWidget(WWLabel(_("File created") + ':'), cur_row, 0) + grid.addWidget(WWLabel(db_metadata.to_str()), cur_row, 1) + cur_row += 1 grid.addWidget(WWLabel(_("Wallet type")+ ':'), cur_row, 0) grid.addWidget(WWLabel(wallet_type), cur_row, 1) cur_row += 1 diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 157b5a477..3ef8a3a53 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -24,12 +24,16 @@ # SOFTWARE. import os import ast +import datetime import json import copy import threading from collections import defaultdict from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence, TYPE_CHECKING, Union import binascii +import time + +import attr from . import util, bitcoin from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh @@ -40,9 +44,10 @@ from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, ChannelType from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage from .lnutil import ChannelConstraints, Outpoint, ShachainElement -from .json_db import StoredDict, JsonDB, locked, modifier +from .json_db import StoredDict, JsonDB, locked, modifier, StoredObject from .plugin import run_hook, plugin_loaders from .submarine_swaps import SwapData +from .version import ELECTRUM_VERSION if TYPE_CHECKING: from .storage import WalletStorage @@ -62,6 +67,20 @@ class TxFeesValue(NamedTuple): num_inputs: Optional[int] = None +@attr.s +class DBMetadata(StoredObject): + creation_timestamp = attr.ib(default=None, type=int) + first_electrum_version_used = attr.ib(default=None, type=str) + + def to_str(self) -> str: + ts = self.creation_timestamp + ver = self.first_electrum_version_used + if ts is None or ver is None: + return "unknown" + date_str = datetime.date.fromtimestamp(ts).isoformat() + return f"using {ver}, on {date_str}" + + class WalletDB(JsonDB): def __init__(self, raw, *, manual_upgrades: bool): @@ -73,6 +92,7 @@ def __init__(self, raw, *, manual_upgrades: bool): self.load_plugins() else: # creating new db self.put('seed_version', FINAL_SEED_VERSION) + self._add_db_creation_metadata() self._after_upgrade_tasks() def load_data(self, s): @@ -1074,6 +1094,19 @@ def _raise_unsupported_version(self, seed_version): msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet." raise WalletFileException(msg) + def _add_db_creation_metadata(self): + # store this for debugging purposes + v = DBMetadata( + creation_timestamp=int(time.time()), + first_electrum_version_used=ELECTRUM_VERSION, + ) + assert self.get("db_metadata", None) is None + self.put("db_metadata", v) + + def get_db_metadata(self) -> Optional[DBMetadata]: + # field only present for wallet files created with ver 4.4.0 or later + return self.get("db_metadata") + @locked def get_txi_addresses(self, tx_hash: str) -> List[str]: """Returns list of is_mine addresses that appear as inputs in tx.""" @@ -1517,6 +1550,8 @@ def _convert_value(self, path, key, v): v = Outpoint(**v) elif key == 'channel_type': v = ChannelType(v) + elif key == 'db_metadata': + v = DBMetadata(**v) return v def _should_convert_to_stored_dict(self, key) -> bool: From 638c896b119998c9e77423f2c0fa02b283e6335a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 15:54:52 +0000 Subject: [PATCH 0350/1143] qt: MyTreeView: make item labels independent of column order --- electrum/gui/qt/address_list.py | 3 ++- electrum/gui/qt/invoice_list.py | 10 +++++----- electrum/gui/qt/utxo_list.py | 7 ++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 4a5b3db38..e869ddfd5 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -187,7 +187,8 @@ def update(self): if self.show_used == AddressUsageStateFilter.FUNDED_OR_UNUSED and is_used_and_empty: continue num_shown += 1 - labels = ['', address, '', '', '', ''] + labels = [""] * len(self.Columns) + labels[self.Columns.ADDRESS] = address address_item = [QStandardItem(e) for e in labels] # align text and set fonts for i, item in enumerate(address_item): diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 6a5375ae9..87abf84e3 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -110,13 +110,13 @@ def update(self): if item.bip70: icon_name = 'seal.png' status = self.wallet.get_invoice_status(item) - status_str = item.get_status_str(status) - message = item.message amount = item.get_amount_sat() timestamp = item.time or 0 - date_str = format_time(timestamp) if timestamp else _('Unknown') - amount_str = self.parent.format_amount(amount, whitespaces=True) - labels = [date_str, message, amount_str, status_str] + labels = [""] * len(self.Columns) + labels[self.Columns.DATE] = format_time(timestamp) if timestamp else _('Unknown') + labels[self.Columns.DESCRIPTION] = item.message + labels[self.Columns.AMOUNT] = self.parent.format_amount(amount, whitespaces=True) + labels[self.Columns.STATUS] = item.get_status_str(status) items = [QStandardItem(e) for e in labels] self.set_editability(items) items[self.Columns.DATE].setIcon(read_QIcon(icon_name)) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 05987d7f3..f0a00cfa8 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -92,9 +92,10 @@ def update(self): for idx, utxo in enumerate(utxos): name = utxo.prevout.to_str() self._utxo_dict[name] = utxo - address = utxo.address - amount_str = self.parent.format_amount(utxo.value_sats(), whitespaces=True) - labels = [str(utxo.short_id), address, '', amount_str, ''] + labels = [""] * len(self.Columns) + labels[self.Columns.OUTPOINT] = str(utxo.short_id) + labels[self.Columns.ADDRESS] = utxo.address + labels[self.Columns.AMOUNT] = self.parent.format_amount(utxo.value_sats(), whitespaces=True) utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) From d3c241db4c1171584cf4c7486cf1504631429ab0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 16:12:50 +0000 Subject: [PATCH 0351/1143] qt: MyTreeView: use enum.auto() in Columns enum --- electrum/gui/qt/address_list.py | 15 ++++++++------- electrum/gui/qt/channels_list.py | 20 ++++++++++---------- electrum/gui/qt/contact_list.py | 8 ++++---- electrum/gui/qt/invoice_list.py | 12 ++++++------ electrum/gui/qt/request_list.py | 16 ++++++++-------- electrum/gui/qt/util.py | 7 +++++++ electrum/gui/qt/utxo_list.py | 14 +++++++------- 7 files changed, 50 insertions(+), 42 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index e869ddfd5..cbcf3c47b 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -23,6 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import enum from enum import IntEnum from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex @@ -70,13 +71,13 @@ def ui_text(self) -> str: class AddressList(MyTreeView): - class Columns(IntEnum): - TYPE = 0 - ADDRESS = 1 - LABEL = 2 - COIN_BALANCE = 3 - FIAT_BALANCE = 4 - NUM_TXS = 5 + class Columns(MyTreeView.BaseColumnsEnum): + TYPE = enum.auto() + ADDRESS = enum.auto() + LABEL = enum.auto() + COIN_BALANCE = enum.auto() + FIAT_BALANCE = enum.auto() + NUM_TXS = enum.auto() filter_columns = [Columns.TYPE, Columns.ADDRESS, Columns.LABEL, Columns.COIN_BALANCE] diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 81d2515ef..2fca083ff 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import traceback -from enum import IntEnum +import enum from typing import Sequence, Optional, Dict from abc import abstractmethod, ABC @@ -33,15 +33,15 @@ class ChannelsList(MyTreeView): update_single_row = QtCore.pyqtSignal(Abstract_Wallet, AbstractChannel) gossip_db_loaded = QtCore.pyqtSignal() - class Columns(IntEnum): - FEATURES = 0 - SHORT_CHANID = 1 - NODE_ALIAS = 2 - CAPACITY = 3 - LOCAL_BALANCE = 4 - REMOTE_BALANCE = 5 - CHANNEL_STATUS = 6 - LONG_CHANID = 7 + class Columns(MyTreeView.BaseColumnsEnum): + FEATURES = enum.auto() + SHORT_CHANID = enum.auto() + NODE_ALIAS = enum.auto() + CAPACITY = enum.auto() + LOCAL_BALANCE = enum.auto() + REMOTE_BALANCE = enum.auto() + CHANNEL_STATUS = enum.auto() + LONG_CHANID = enum.auto() headers = { Columns.SHORT_CHANID: _('Short Channel ID'), diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 446f5e5a2..291461f66 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -23,7 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from enum import IntEnum +import enum from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex @@ -39,9 +39,9 @@ class ContactList(MyTreeView): - class Columns(IntEnum): - NAME = 0 - ADDRESS = 1 + class Columns(MyTreeView.BaseColumnsEnum): + NAME = enum.auto() + ADDRESS = enum.auto() headers = { Columns.NAME: _('Name'), diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 87abf84e3..f1d6078eb 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -23,7 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from enum import IntEnum +import enum from typing import Sequence, TYPE_CHECKING from PyQt5.QtCore import Qt, QItemSelectionModel @@ -53,11 +53,11 @@ class InvoiceList(MyTreeView): key_role = ROLE_REQUEST_ID - class Columns(IntEnum): - DATE = 0 - DESCRIPTION = 1 - AMOUNT = 2 - STATUS = 3 + class Columns(MyTreeView.BaseColumnsEnum): + DATE = enum.auto() + DESCRIPTION = enum.auto() + AMOUNT = enum.auto() + STATUS = enum.auto() headers = { Columns.DATE: _('Date'), diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 49cead064..8fe5a7898 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -23,7 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from enum import IntEnum +import enum from typing import Optional, TYPE_CHECKING from PyQt5.QtGui import QStandardItemModel, QStandardItem @@ -50,13 +50,13 @@ class RequestList(MyTreeView): key_role = ROLE_KEY - class Columns(IntEnum): - DATE = 0 - DESCRIPTION = 1 - AMOUNT = 2 - STATUS = 3 - ADDRESS = 4 - LN_RHASH = 5 + class Columns(MyTreeView.BaseColumnsEnum): + DATE = enum.auto() + DESCRIPTION = enum.auto() + AMOUNT = enum.auto() + STATUS = enum.auto() + ADDRESS = enum.auto() + LN_RHASH = enum.auto() headers = { Columns.DATE: _('Date'), diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 2b7e267e2..dfc726929 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1,4 +1,5 @@ import asyncio +import enum import os.path import time import sys @@ -617,6 +618,12 @@ class MyTreeView(QTreeView): filter_columns: Iterable[int] + class BaseColumnsEnum(enum.IntEnum): + @staticmethod + def _generate_next_value_(name: str, start: int, count: int, last_values): + # this is overridden to get a 0-based counter + return count + def __init__(self, parent: 'ElectrumWindow', create_menu, *, stretch_column=None, editable_columns=None): super().__init__(parent) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index f0a00cfa8..635ee09c3 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -24,7 +24,7 @@ # SOFTWARE. from typing import Optional, List, Dict, Sequence, Set -from enum import IntEnum +import enum import copy from PyQt5.QtCore import Qt @@ -44,12 +44,12 @@ class UTXOList(MyTreeView): _spend_set: Set[str] # coins selected by the user to spend from _utxo_dict: Dict[str, PartialTxInput] # coin name -> coin - class Columns(IntEnum): - OUTPOINT = 0 - ADDRESS = 1 - LABEL = 2 - AMOUNT = 3 - PARENTS = 4 + class Columns(MyTreeView.BaseColumnsEnum): + OUTPOINT = enum.auto() + ADDRESS = enum.auto() + LABEL = enum.auto() + AMOUNT = enum.auto() + PARENTS = enum.auto() headers = { Columns.OUTPOINT: _('Output point'), From 8e2a5853b8f7c62df9c6ad1f2351b4ae75dbf5ba Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 13 Mar 2023 17:20:01 +0100 Subject: [PATCH 0352/1143] qml: don't crash on bolt11 invoice when wallet is non-lightning --- electrum/gui/qml/qeinvoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 9e7969b9e..03bd923e9 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -115,7 +115,7 @@ def get_max_spendable_onchain(self): return spendable def get_max_spendable_lightning(self): - return self._wallet.wallet.lnworker.num_sats_can_send() + return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0 class QEInvoiceParser(QEInvoice): _logger = get_logger(__name__) From 08c37ab088f4e99ff919f481788ad9c0371f8bc2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 16:39:13 +0000 Subject: [PATCH 0353/1143] qt: HistoryList to also use BaseColumnsEnum --- electrum/gui/qt/history_list.py | 39 ++++++++++++++++++--------------- electrum/gui/qt/util.py | 4 +++- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index eef8d6cea..ab89a8685 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -30,7 +30,7 @@ from datetime import date from typing import TYPE_CHECKING, Tuple, Dict import threading -from enum import IntEnum +import enum from decimal import Decimal from PyQt5.QtGui import QMouseEvent, QFont, QBrush, QColor @@ -79,18 +79,6 @@ ROLE_SORT_ORDER = Qt.UserRole + 1000 -class HistoryColumns(IntEnum): - STATUS = 0 - DESCRIPTION = 1 - AMOUNT = 2 - BALANCE = 3 - FIAT_VALUE = 4 - FIAT_ACQ_PRICE = 5 - FIAT_CAP_GAINS = 6 - TXID = 7 - SHORT_ID = 8 # ~SCID - - class HistorySortModel(QSortFilterProxyModel): def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): item1 = self.sourceModel().data(source_left, ROLE_SORT_ORDER) @@ -464,12 +452,24 @@ def tx_mined_info_from_tx_item(tx_item): class HistoryList(MyTreeView, AcceptFileDragDrop): + + class Columns(MyTreeView.BaseColumnsEnum): + STATUS = enum.auto() + DESCRIPTION = enum.auto() + AMOUNT = enum.auto() + BALANCE = enum.auto() + FIAT_VALUE = enum.auto() + FIAT_ACQ_PRICE = enum.auto() + FIAT_CAP_GAINS = enum.auto() + TXID = enum.auto() + SHORT_ID = enum.auto() # ~SCID + filter_columns = [ - HistoryColumns.STATUS, - HistoryColumns.DESCRIPTION, - HistoryColumns.AMOUNT, - HistoryColumns.TXID, - HistoryColumns.SHORT_ID, + Columns.STATUS, + Columns.DESCRIPTION, + Columns.AMOUNT, + Columns.TXID, + Columns.SHORT_ID, ] def tx_item_from_proxy_row(self, proxy_row): @@ -892,3 +892,6 @@ def get_text_from_coordinate(self, row, col): def get_role_data_from_coordinate(self, row, col, *, role): idx = self.model().mapToSource(self.model().index(row, col)) return self.hm.data(idx, role).value() + + +HistoryColumns = HistoryList.Columns diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index dfc726929..26d15a0c3 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -11,7 +11,7 @@ from decimal import Decimal from functools import partial, lru_cache, wraps from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any, - Sequence, Iterable, Tuple) + Sequence, Iterable, Tuple, Type) from PyQt5 import QtWidgets, QtCore from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, QImage, @@ -624,6 +624,8 @@ def _generate_next_value_(name: str, start: int, count: int, last_values): # this is overridden to get a 0-based counter return count + Columns: Type[BaseColumnsEnum] + def __init__(self, parent: 'ElectrumWindow', create_menu, *, stretch_column=None, editable_columns=None): super().__init__(parent) From dd27c6beff8d393752a5fa51efa9b3dfa50b84ad Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 16:41:10 +0000 Subject: [PATCH 0354/1143] qt utxo list: copy menu: separate items for short/long outpoint --- electrum/gui/qt/utxo_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 635ee09c3..4039928da 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -98,7 +98,6 @@ def update(self): labels[self.Columns.AMOUNT] = self.parent.format_amount(utxo.value_sats(), whitespaces=True) utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) - utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_CLIPBOARD_DATA) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR) utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) @@ -270,9 +269,10 @@ def create_menu(self, position): idx = self.indexAt(position) if not idx.isValid(): return - self.add_copy_menu(menu, idx) utxo = coins[0] txid = utxo.prevout.txid.hex() + cc = self.add_copy_menu(menu, idx) + cc.addAction(_("Long Output point"), lambda: self.place_text_on_clipboard(utxo.prevout.to_str(), title="Long Output point")) # "Details" tx = self.wallet.adb.get_transaction(txid) if tx: From faf0c808937be6ebc319036c866035f58b85da22 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 13 Mar 2023 17:50:00 +0100 Subject: [PATCH 0355/1143] qml: enable canPay in InvoiceDialog if wallet has insufficient funds to pay via lightning and invoice has fallback address and amount can be paid on-chain. In WalletMainView, follow on-chain payment path if available lighting balance is insufficient for the invoice amount --- electrum/gui/qml/components/InvoiceDialog.qml | 4 +++- electrum/gui/qml/components/WalletMainView.qml | 8 ++++---- electrum/gui/qml/qeinvoice.py | 9 ++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 831a63435..87558ab0d 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -78,7 +78,9 @@ ElDialog { text: invoice.invoiceType == Invoice.OnchainInvoice ? qsTr('On chain') : invoice.invoiceType == Invoice.LightningInvoice - ? qsTr('Lightning') + ? invoice.address + ? qsTr('Lightning with on-chain fallback address') + : qsTr('Lightning') : '' Layout.fillWidth: true } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 96bad9a02..957817a27 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -255,11 +255,11 @@ Item { height: parent.height onDoPay: { - if (invoice.invoiceType == Invoice.OnchainInvoice) { + if (invoice.invoiceType == Invoice.OnchainInvoice || (invoice.invoiceType == Invoice.LightningInvoice && invoice.amount.satsInt > Daemon.currentWallet.lightningCanSend ) ) { var dialog = confirmPaymentDialog.createObject(mainView, { - 'address': invoice.address, - 'satoshis': invoice.amount, - 'message': invoice.message + address: invoice.address, + satoshis: invoice.amount, + message: invoice.message }) var canComplete = !Daemon.currentWallet.isWatchOnly && Daemon.currentWallet.canSignWithoutCosigner dialog.txaccepted.connect(function() { diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 03bd923e9..7b8bec520 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -27,8 +27,7 @@ class Type: Invalid = -1 OnchainInvoice = 0 LightningInvoice = 1 - LightningAndOnchainInvoice = 2 - LNURLPayRequest = 3 + LNURLPayRequest = 2 class Status: Unpaid = PR_UNPAID @@ -307,6 +306,10 @@ def determine_can_pay(self): self.userinfo = _('Cannot pay less than the amount specified in the invoice') else: self.canPay = True + elif self.address and self.get_max_spendable_onchain() > self.amount.satsInt: + # TODO: validate address? + # TODO: subtract fee? + self.canPay = True else: self.userinfo = _('Insufficient balance') else: @@ -323,7 +326,7 @@ def determine_can_pay(self): # TODO: dust limit? self.canPay = True elif self.get_max_spendable_onchain() >= self.amount.satsInt: - # TODO: dust limit? + # TODO: subtract fee? self.canPay = True else: self.userinfo = _('Insufficient balance') From 5fd77215089ec254e6e1ff28c98342cb5f951f54 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 13 Mar 2023 18:22:51 +0100 Subject: [PATCH 0356/1143] qml: de-dupe broadcastFailed handler, styling InvoiceDialog amount --- electrum/gui/qml/components/InvoiceDialog.qml | 2 ++ .../gui/qml/components/WalletMainView.qml | 25 +++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 87558ab0d..d20f35ff9 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -254,6 +254,7 @@ ElDialog { } Label { + Layout.alignment: Qt.AlignRight visible: !invoice.amount.isMax font.pixelSize: constants.fontSizeXLarge font.family: FixedFont @@ -271,6 +272,7 @@ ElDialog { Label { id: fiatValue + Layout.alignment: Qt.AlignRight visible: Daemon.fx.enabled && !invoice.amount.isMax text: Daemon.fx.fiatValue(invoice.amount, false) font.pixelSize: constants.fontSizeMedium diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 957817a27..e664e7cb1 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -240,6 +240,15 @@ Item { Connections { target: Daemon.currentWallet + function onOtpRequested() { + console.log('OTP requested') + var dialog = otpDialog.createObject(mainView) + dialog.accepted.connect(function() { + console.log('accepted ' + dialog.otpauth) + Daemon.currentWallet.finish_otp(dialog.otpauth) + }) + dialog.open() + } function onBroadcastFailed(txid, code, message) { var dialog = app.messageDialog.createObject(app, { text: message @@ -289,22 +298,6 @@ Item { } } - Connections { - target: Daemon.currentWallet - function onOtpRequested() { - console.log('OTP requested') - var dialog = otpDialog.createObject(mainView) - dialog.accepted.connect(function() { - console.log('accepted ' + dialog.otpauth) - Daemon.currentWallet.finish_otp(dialog.otpauth) - }) - dialog.open() - } - function onBroadcastFailed() { - notificationPopup.show(qsTr('Broadcast transaction failed')) - } - } - Component { id: sendDialog SendDialog { From 9d64fe7046d4e14e831ea17a71d3ba9d8f9ae491 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 17:29:29 +0000 Subject: [PATCH 0357/1143] qt: MyTreeView: disambiguate "parent" and "main_window" --- electrum/gui/qt/address_list.py | 63 ++++++++++++------------ electrum/gui/qt/channels_list.py | 71 +++++++++++++++------------- electrum/gui/qt/contact_list.py | 32 ++++++++----- electrum/gui/qt/history_list.py | 70 +++++++++++++-------------- electrum/gui/qt/invoice_list.py | 14 +++--- electrum/gui/qt/request_list.py | 16 ++++--- electrum/gui/qt/util.py | 22 ++++++--- electrum/gui/qt/utxo_list.py | 53 +++++++++++---------- electrum/gui/qt/watchtower_dialog.py | 11 +++-- 9 files changed, 194 insertions(+), 158 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index cbcf3c47b..69868a4a0 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -25,6 +25,7 @@ import enum from enum import IntEnum +from typing import TYPE_CHECKING from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont @@ -38,6 +39,9 @@ from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen, MySortModel +if TYPE_CHECKING: + from .main_window import ElectrumWindow + class AddressUsageStateFilter(IntEnum): ALL = 0 @@ -85,12 +89,13 @@ class Columns(MyTreeView.BaseColumnsEnum): ROLE_ADDRESS_STR = Qt.UserRole + 1001 key_role = ROLE_ADDRESS_STR - def __init__(self, parent): - super().__init__(parent, self.create_menu, - stretch_column=self.Columns.LABEL, - editable_columns=[self.Columns.LABEL]) - self.main_window = parent - self.wallet = self.parent.wallet + def __init__(self, main_window: 'ElectrumWindow'): + super().__init__( + main_window=main_window, + stretch_column=self.Columns.LABEL, + editable_columns=[self.Columns.LABEL], + ) + self.wallet = self.main_window.wallet self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) self.show_change = AddressTypeFilter.ALL # type: AddressTypeFilter @@ -120,7 +125,7 @@ def create_toolbar(self, config): return toolbar def should_show_fiat(self): - return self.parent.fx and self.parent.fx.is_enabled() and self.config.get('fiat_address', False) + return self.main_window.fx and self.main_window.fx.is_enabled() and self.config.get('fiat_address', False) def get_toolbar_buttons(self): return self.change_button, self.used_button @@ -132,7 +137,7 @@ def on_hide_toolbar(self): def refresh_headers(self): if self.should_show_fiat(): - ccy = self.parent.fx.get_currency() + ccy = self.main_window.fx.get_currency() else: ccy = _('Fiat') headers = { @@ -171,7 +176,7 @@ def update(self): self.proxy.setDynamicSortFilter(False) # temp. disable re-sorting after every change self.std_model.clear() self.refresh_headers() - fx = self.parent.fx + fx = self.main_window.fx set_address = None num_shown = 0 self.addresses_beyond_gap_limit = self.wallet.get_all_known_addresses_beyond_gap_limit() @@ -236,9 +241,9 @@ def refresh_row(self, key, row): num = self.wallet.adb.get_address_history_len(address) c, u, x = self.wallet.get_addr_balance(address) balance = c + u + x - balance_text = self.parent.format_amount(balance, whitespaces=True) + balance_text = self.main_window.format_amount(balance, whitespaces=True) # create item - fx = self.parent.fx + fx = self.main_window.fx if self.should_show_fiat(): rate = fx.exchange_rate() fiat_balance_str = fx.value_str(balance, rate) @@ -276,37 +281,37 @@ def create_menu(self, position): addr_column_title = self.std_model.horizontalHeaderItem(self.Columns.LABEL).text() addr_idx = idx.sibling(idx.row(), self.Columns.LABEL) self.add_copy_menu(menu, idx) - menu.addAction(_('Details'), lambda: self.parent.show_address(addr)) + menu.addAction(_('Details'), lambda: self.main_window.show_address(addr)) persistent = QPersistentModelIndex(addr_idx) menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p))) - #menu.addAction(_("Request payment"), lambda: self.parent.receive_at(addr)) + #menu.addAction(_("Request payment"), lambda: self.main_window.receive_at(addr)) if self.wallet.can_export(): - menu.addAction(_("Private key"), lambda: self.parent.show_private_key(addr)) + menu.addAction(_("Private key"), lambda: self.main_window.show_private_key(addr)) if not is_multisig and not self.wallet.is_watching_only(): - menu.addAction(_("Sign/verify message"), lambda: self.parent.sign_verify_message(addr)) - menu.addAction(_("Encrypt/decrypt message"), lambda: self.parent.encrypt_message(addr)) + menu.addAction(_("Sign/verify message"), lambda: self.main_window.sign_verify_message(addr)) + menu.addAction(_("Encrypt/decrypt message"), lambda: self.main_window.encrypt_message(addr)) if can_delete: - menu.addAction(_("Remove from wallet"), lambda: self.parent.remove_address(addr)) + menu.addAction(_("Remove from wallet"), lambda: self.main_window.remove_address(addr)) addr_URL = block_explorer_URL(self.config, 'addr', addr) if addr_URL: menu.addAction(_("View on block explorer"), lambda: webopen(addr_URL)) if not self.wallet.is_frozen_address(addr): - menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) + menu.addAction(_("Freeze"), lambda: self.main_window.set_frozen_state_of_addresses([addr], True)) else: - menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses([addr], False)) + menu.addAction(_("Unfreeze"), lambda: self.main_window.set_frozen_state_of_addresses([addr], False)) else: # multiple items selected - menu.addAction(_("Freeze"), lambda: self.parent.set_frozen_state_of_addresses(addrs, True)) - menu.addAction(_("Unfreeze"), lambda: self.parent.set_frozen_state_of_addresses(addrs, False)) + menu.addAction(_("Freeze"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, True)) + menu.addAction(_("Unfreeze"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, False)) coins = self.wallet.get_spendable_coins(addrs) if coins: - if self.parent.utxo_list.are_in_coincontrol(coins): - menu.addAction(_("Remove from coin control"), lambda: self.parent.utxo_list.remove_from_coincontrol(coins)) + if self.main_window.utxo_list.are_in_coincontrol(coins): + menu.addAction(_("Remove from coin control"), lambda: self.main_window.utxo_list.remove_from_coincontrol(coins)) else: - menu.addAction(_("Add to coin control"), lambda: self.parent.utxo_list.add_to_coincontrol(coins)) + menu.addAction(_("Add to coin control"), lambda: self.main_window.utxo_list.add_to_coincontrol(coins)) run_hook('receive_menu', menu, addrs, self.wallet) menu.exec_(self.viewport().mapToGlobal(position)) @@ -316,7 +321,7 @@ def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: try: self.wallet.check_address_for_corruption(text) except InternalAddressCorruption as e: - self.parent.show_error(str(e)) + self.main_window.show_error(str(e)) raise super().place_text_on_clipboard(text, title=title) @@ -326,7 +331,7 @@ def get_edit_key_from_coordinate(self, row, col): return self.get_role_data_from_coordinate(row, 0, role=self.ROLE_ADDRESS_STR) def on_edited(self, idx, edit_key, *, text): - self.parent.wallet.set_label(edit_key, text) - self.parent.history_model.refresh('address label edited') - self.parent.utxo_list.update() - self.parent.update_completions() + self.wallet.set_label(edit_key, text) + self.main_window.history_model.refresh('address label edited') + self.main_window.utxo_list.update() + self.main_window.update_completions() diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 2fca083ff..fe4f90708 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import traceback import enum -from typing import Sequence, Optional, Dict +from typing import Sequence, Optional, Dict, TYPE_CHECKING from abc import abstractmethod, ABC from PyQt5 import QtCore, QtGui @@ -24,6 +24,9 @@ from .amountedit import BTCAmountEdit, FreezableLineEdit from .util import read_QIcon, font_height +if TYPE_CHECKING: + from .main_window import ElectrumWindow + ROLE_CHANNEL_ID = Qt.UserRole @@ -63,16 +66,18 @@ class Columns(MyTreeView.BaseColumnsEnum): _default_item_bg_brush = None # type: Optional[QBrush] - def __init__(self, parent): - super().__init__(parent, self.create_menu, stretch_column=self.Columns.NODE_ALIAS) + def __init__(self, main_window: 'ElectrumWindow'): + super().__init__( + main_window=main_window, + stretch_column=self.Columns.NODE_ALIAS, + ) self.setModel(QtGui.QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) - self.main_window = parent self.gossip_db_loaded.connect(self.on_gossip_db) self.update_rows.connect(self.do_update_rows) self.update_single_row.connect(self.do_update_single_row) - self.network = self.parent.network - self.wallet = self.parent.wallet + self.network = self.main_window.network + self.wallet = self.main_window.wallet self.setSortingEnabled(True) @property @@ -85,12 +90,12 @@ def format_fields(self, chan: AbstractChannel) -> Dict['ChannelsList.Columns', s for subject in (REMOTE, LOCAL): if isinstance(chan, Channel): can_send = chan.available_to_spend(subject) / 1000 - label = self.parent.format_amount(can_send, whitespaces=True) + label = self.main_window.format_amount(can_send, whitespaces=True) other = subject.inverted() bal_other = chan.balance(other)//1000 bal_minus_htlcs_other = chan.balance_minus_outgoing_htlcs(other)//1000 if bal_other != bal_minus_htlcs_other: - label += ' (+' + self.parent.format_amount(bal_other - bal_minus_htlcs_other, whitespaces=False) + ')' + label += ' (+' + self.main_window.format_amount(bal_other - bal_minus_htlcs_other, whitespaces=False) + ')' else: assert isinstance(chan, ChannelBackup) label = '' @@ -98,7 +103,7 @@ def format_fields(self, chan: AbstractChannel) -> Dict['ChannelsList.Columns', s status = chan.get_state_for_GUI() closed = chan.is_closed() node_alias = self.lnworker.get_node_alias(chan.node_id) or chan.node_id.hex() - capacity_str = self.parent.format_amount(chan.get_capacity(), whitespaces=True) + capacity_str = self.main_window.format_amount(chan.get_capacity(), whitespaces=True) return { self.Columns.SHORT_CHANID: chan.short_id_for_GUI(), self.Columns.LONG_CHANID: chan.channel_id.hex(), @@ -125,7 +130,7 @@ def close_channel(self, channel_id): self.is_force_close = False msg = _('Cooperative close?') msg += '\n' + _(messages.MSG_COOPERATIVE_CLOSE) - if not self.parent.question(msg): + if not self.main_window.question(msg): return coro = self.lnworker.close_channel(channel_id) on_success = self.on_channel_closed @@ -147,10 +152,10 @@ def on_checked(b): + '' + _('Please create a backup of your wallet file!') + ' '\ + '

' + _('Funds in this channel will not be recoverable from seed until they are swept back into your wallet, and might be lost if you lose your wallet file.') + ' '\ + _('To prevent that, you should save a backup of your wallet on another device.') + '

' - if not self.parent.question(msg, title=_('Force-close channel'), rich_text=True, checkbox=backup_cb): + if not self.main_window.question(msg, title=_('Force-close channel'), rich_text=True, checkbox=backup_cb): return if self.save_backup: - if not self.parent.backup_wallet(): + if not self.main_window.backup_wallet(): return def task(): coro = self.lnworker.force_close_channel(channel_id) @@ -178,7 +183,7 @@ def export_channel_backup(self, channel_id): def request_force_close(self, channel_id): msg = _('Request force-close from remote peer?') msg += '\n' + _(messages.MSG_REQUEST_FORCE_CLOSE) - if not self.parent.question(msg): + if not self.main_window.question(msg): return def task(): coro = self.lnworker.request_force_close(channel_id) @@ -208,9 +213,9 @@ def get_rebalance_pair(self): def on_rebalance(self): chan1, chan2 = self.get_rebalance_pair() if chan1 is None: - self.parent.show_error("Select two active channels to rebalance.") + self.main_window.show_error("Select two active channels to rebalance.") return - self.parent.rebalance_dialog(chan1, chan2) + self.main_window.rebalance_dialog(chan1, chan2) def create_menu(self, position): menu = QMenu() @@ -222,7 +227,7 @@ def create_menu(self, position): if len(selected) == 2: chan1, chan2 = self.get_rebalance_pair() if chan1 and chan2: - menu.addAction(_("Rebalance channels"), lambda: self.parent.rebalance_dialog(chan1, chan2)) + menu.addAction(_("Rebalance channels"), lambda: self.main_window.rebalance_dialog(chan1, chan2)) menu.exec_(self.viewport().mapToGlobal(position)) return elif len(selected) > 2: @@ -235,7 +240,7 @@ def create_menu(self, position): return channel_id = idx.sibling(idx.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID) chan = self.lnworker.get_channel_by_id(channel_id) or self.lnworker.channel_backups[channel_id] - menu.addAction(_("Details..."), lambda: self.parent.show_channel_details(chan)) + menu.addAction(_("Details..."), lambda: self.main_window.show_channel_details(chan)) menu.addSeparator() cc = self.add_copy_menu(menu, idx) cc.addAction(_("Node ID"), lambda: self.place_text_on_clipboard( @@ -272,7 +277,7 @@ def create_menu(self, position): @QtCore.pyqtSlot(Abstract_Wallet, AbstractChannel) def do_update_single_row(self, wallet: Abstract_Wallet, chan: AbstractChannel): - if wallet != self.parent.wallet: + if wallet != self.wallet: return for row in range(self.model().rowCount()): item = self.model().item(row, self.Columns.NODE_ALIAS) @@ -287,11 +292,11 @@ def do_update_single_row(self, wallet: Abstract_Wallet, chan: AbstractChannel): @QtCore.pyqtSlot() def on_gossip_db(self): - self.do_update_rows(self.parent.wallet) + self.do_update_rows(self.wallet) @QtCore.pyqtSlot(Abstract_Wallet) def do_update_rows(self, wallet): - if wallet != self.parent.wallet: + if wallet != self.wallet: return self.model().clear() self.update_headers(self.headers) @@ -337,29 +342,29 @@ def _update_chan_frozen_bg(self, *, chan: AbstractChannel, items: Sequence[QStan item.setToolTip("") def update_can_send(self, lnworker: LNWallet): - msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.num_sats_can_send())\ - + ' ' + self.parent.base_unit() + '; '\ - + _('can receive') + ' ' + self.parent.format_amount(lnworker.num_sats_can_receive())\ - + ' ' + self.parent.base_unit() + msg = _('Can send') + ' ' + self.main_window.format_amount(lnworker.num_sats_can_send())\ + + ' ' + self.main_window.base_unit() + '; '\ + + _('can receive') + ' ' + self.main_window.format_amount(lnworker.num_sats_can_receive())\ + + ' ' + self.main_window.base_unit() self.can_send_label.setText(msg) def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') self.can_send_label = toolbar.itemAt(0).widget() menu.addAction(_('Rebalance channels'), lambda: self.on_rebalance()) - menu.addAction(_('Submarine swap'), lambda: self.parent.run_swap_dialog()) + menu.addAction(_('Submarine swap'), lambda: self.main_window.run_swap_dialog()) menu.addSeparator() - menu.addAction(_("Import channel backup"), lambda: self.parent.do_process_from_text_channel_backup()) + menu.addAction(_("Import channel backup"), lambda: self.main_window.do_process_from_text_channel_backup()) self.new_channel_button = EnterButton(_('New Channel'), self.new_channel_with_warning) - self.new_channel_button.setEnabled(self.parent.wallet.has_lightning()) + self.new_channel_button.setEnabled(self.wallet.has_lightning()) toolbar.insertWidget(2, self.new_channel_button) return toolbar def new_channel_with_warning(self): - lnworker = self.parent.wallet.lnworker + lnworker = self.wallet.lnworker if not lnworker.channels and not lnworker.channel_backups: warning = _(messages.MSG_LIGHTNING_WARNING) - answer = self.parent.question( + answer = self.main_window.question( _('Do you want to create your first channel?') + '\n\n' + warning) if answer: self.new_channel_dialog() @@ -367,9 +372,9 @@ def new_channel_with_warning(self): self.new_channel_dialog() def statistics_dialog(self): - channel_db = self.parent.network.channel_db - capacity = self.parent.format_amount(channel_db.capacity()) + ' '+ self.parent.base_unit() - d = WindowModalDialog(self.parent, _('Lightning Network Statistics')) + channel_db = self.network.channel_db + capacity = self.main_window.format_amount(channel_db.capacity()) + ' '+ self.main_window.base_unit() + d = WindowModalDialog(self.main_window, _('Lightning Network Statistics')) d.setMinimumWidth(400) vbox = QVBoxLayout(d) h = QGridLayout() @@ -385,7 +390,7 @@ def statistics_dialog(self): def new_channel_dialog(self, *, amount_sat=None, min_amount_sat=None): from .new_channel_dialog import NewChannelDialog - d = NewChannelDialog(self.parent, amount_sat, min_amount_sat) + d = NewChannelDialog(self.main_window, amount_sat, min_amount_sat) return d.run() def set_visibility_of_columns(self): diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 291461f66..ff805feab 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -24,6 +24,7 @@ # SOFTWARE. import enum +from typing import TYPE_CHECKING from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtCore import Qt, QPersistentModelIndex, QModelIndex @@ -36,6 +37,9 @@ from .util import MyTreeView, webopen +if TYPE_CHECKING: + from .main_window import ElectrumWindow + class ContactList(MyTreeView): @@ -52,10 +56,12 @@ class Columns(MyTreeView.BaseColumnsEnum): ROLE_CONTACT_KEY = Qt.UserRole + 1000 key_role = ROLE_CONTACT_KEY - def __init__(self, parent): - super().__init__(parent, self.create_menu, - stretch_column=self.Columns.NAME, - editable_columns=[self.Columns.NAME]) + def __init__(self, main_window: 'ElectrumWindow'): + super().__init__( + main_window=main_window, + stretch_column=self.Columns.NAME, + editable_columns=[self.Columns.NAME], + ) self.setModel(QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSortingEnabled(True) @@ -63,8 +69,8 @@ def __init__(self, parent): self.update() def on_edited(self, idx, edit_key, *, text): - _type, prior_name = self.parent.contacts.pop(edit_key) - self.parent.set_contact(text, edit_key) + _type, prior_name = self.main_window.contacts.pop(edit_key) + self.main_window.set_contact(text, edit_key) self.update() def create_menu(self, position): @@ -86,8 +92,8 @@ def create_menu(self, position): # would not be editable if openalias persistent = QPersistentModelIndex(idx) menu.addAction(_("Edit {}").format(column_title), lambda p=persistent: self.edit(QModelIndex(p))) - menu.addAction(_("Pay to"), lambda: self.parent.payto_contacts(selected_keys)) - menu.addAction(_("Delete"), lambda: self.parent.delete_contacts(selected_keys)) + menu.addAction(_("Pay to"), lambda: self.main_window.payto_contacts(selected_keys)) + menu.addAction(_("Delete"), lambda: self.main_window.delete_contacts(selected_keys)) URLs = [block_explorer_URL(self.config, 'addr', key) for key in filter(is_address, selected_keys)] if URLs: menu.addAction(_("View on block explorer"), lambda: [webopen(u) for u in URLs]) @@ -102,8 +108,8 @@ def update(self): self.model().clear() self.update_headers(self.__class__.headers) set_current = None - for key in sorted(self.parent.contacts.keys()): - contact_type, name = self.parent.contacts[key] + for key in sorted(self.main_window.contacts.keys()): + contact_type, name = self.main_window.contacts[key] items = [QStandardItem(x) for x in (name, key)] items[self.Columns.NAME].setEditable(contact_type != 'openalias') items[self.Columns.ADDRESS].setEditable(False) @@ -130,7 +136,7 @@ def get_edit_key_from_coordinate(self, row, col): def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') - menu.addAction(_("&New contact"), self.parent.new_contact_dialog) - menu.addAction(_("Import"), lambda: self.parent.import_contacts()) - menu.addAction(_("Export"), lambda: self.parent.export_contacts()) + menu.addAction(_("&New contact"), self.main_window.new_contact_dialog) + menu.addAction(_("Import"), lambda: self.main_window.import_contacts()) + menu.addAction(_("Export"), lambda: self.main_window.export_contacts()) return toolbar diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index ab89a8685..dd4a76056 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -486,12 +486,12 @@ def should_hide(self, proxy_row): return True return False - def __init__(self, parent, model: HistoryModel): - super().__init__(parent, self.create_menu, - stretch_column=HistoryColumns.DESCRIPTION, - editable_columns=[HistoryColumns.DESCRIPTION, HistoryColumns.FIAT_VALUE]) - self.main_window = parent - self.config = parent.config + def __init__(self, main_window: 'ElectrumWindow', model: HistoryModel): + super().__init__( + main_window=main_window, + stretch_column=HistoryColumns.DESCRIPTION, + editable_columns=[HistoryColumns.DESCRIPTION, HistoryColumns.FIAT_VALUE], + ) self.hm = model self.proxy = HistorySortModel(self) self.proxy.setSourceModel(model) @@ -510,7 +510,7 @@ def __init__(self, parent, model: HistoryModel): self.end_button.setEnabled(False) self.period_combo.addItems([_('All'), _('Custom')]) self.period_combo.activated.connect(self.on_combo) - self.wallet = self.parent.wallet # type: Abstract_Wallet + self.wallet = self.main_window.wallet # type: Abstract_Wallet self.sortByColumn(HistoryColumns.STATUS, Qt.AscendingOrder) self.setRootIsDecorated(True) self.header().setStretchLastSection(False) @@ -603,24 +603,24 @@ def on_date(date): def show_summary(self): if not self.hm.should_show_fiat(): - self.parent.show_message(_("Enable fiat exchange rate with history.")) + self.main_window.show_message(_("Enable fiat exchange rate with history.")) return - fx = self.parent.fx + fx = self.main_window.fx h = self.wallet.get_detailed_history( from_timestamp = time.mktime(self.start_date.timetuple()) if self.start_date else None, to_timestamp = time.mktime(self.end_date.timetuple()) if self.end_date else None, fx=fx) summary = h['summary'] if not summary: - self.parent.show_message(_("Nothing to summarize.")) + self.main_window.show_message(_("Nothing to summarize.")) return start = summary['begin'] end = summary['end'] flow = summary['flow'] start_date = start.get('date') end_date = end.get('date') - format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit() - format_fiat = lambda x: str(x) + ' ' + self.parent.fx.ccy + format_amount = lambda x: self.main_window.format_amount(x.value) + ' ' + self.main_window.base_unit() + format_fiat = lambda x: str(x) + ' ' + self.main_window.fx.ccy d = WindowModalDialog(self, _("Summary")) d.setMinimumSize(600, 150) @@ -679,7 +679,7 @@ def plot_history_dialog(self): from electrum.plot import plot_history, NothingToPlotException except Exception as e: _logger.error(f"could not import electrum.plot. This feature needs matplotlib to be installed. exc={e!r}") - self.parent.show_message( + self.main_window.show_message( _("Can't plot history.") + '\n' + _("Perhaps some dependencies are missing...") + " (matplotlib?)" + '\n' + f"Error: {e!r}" @@ -689,7 +689,7 @@ def plot_history_dialog(self): plt = plot_history(list(self.hm.transactions.values())) plt.show() except NothingToPlotException as e: - self.parent.show_message(str(e)) + self.main_window.show_message(str(e)) def on_edited(self, idx, edit_key, *, text): index = self.model().mapToSource(idx) @@ -699,9 +699,9 @@ def on_edited(self, idx, edit_key, *, text): if column == HistoryColumns.DESCRIPTION: if self.wallet.set_label(key, text): #changed self.hm.update_label(index) - self.parent.update_completions() + self.main_window.update_completions() elif column == HistoryColumns.FIAT_VALUE: - self.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, tx_item['value'].value) + self.wallet.set_fiat_value(key, self.main_window.fx.ccy, text, self.main_window.fx, tx_item['value'].value) value = tx_item['value'].value if value is not None: self.hm.update_fiat(index) @@ -720,13 +720,13 @@ def mouseDoubleClickEvent(self, event: QMouseEvent): else: if tx_item.get('lightning'): if tx_item['type'] == 'payment': - self.parent.show_lightning_transaction(tx_item) + self.main_window.show_lightning_transaction(tx_item) return tx_hash = tx_item['txid'] tx = self.wallet.adb.get_transaction(tx_hash) if not tx: return - self.parent.show_transaction(tx) + self.main_window.show_transaction(tx) def add_copy_menu(self, menu, idx): cc = menu.addMenu(_("Copy")) @@ -751,14 +751,14 @@ def create_menu(self, position: QPoint): tx_item = idx.internalPointer().get_data() if tx_item.get('lightning') and tx_item['type'] == 'payment': menu = QMenu() - menu.addAction(_("View Payment"), lambda: self.parent.show_lightning_transaction(tx_item)) + menu.addAction(_("View Payment"), lambda: self.main_window.show_lightning_transaction(tx_item)) cc = self.add_copy_menu(menu, idx) cc.addAction(_("Payment Hash"), lambda: self.place_text_on_clipboard(tx_item['payment_hash'], title="Payment Hash")) cc.addAction(_("Preimage"), lambda: self.place_text_on_clipboard(tx_item['preimage'], title="Preimage")) key = tx_item['payment_hash'] log = self.wallet.lnworker.logs.get(key) if log: - menu.addAction(_("View log"), lambda: self.parent.send_tab.invoice_list.show_log(key, log)) + menu.addAction(_("View log"), lambda: self.main_window.send_tab.invoice_list.show_log(key, log)) menu.exec_(self.viewport().mapToGlobal(position)) return tx_hash = tx_item['txid'] @@ -780,25 +780,25 @@ def create_menu(self, position: QPoint): # TODO use siblingAtColumn when min Qt version is >=5.11 persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) menu_edit.addAction(_("{}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) - menu.addAction(_("View Transaction"), lambda: self.parent.show_transaction(tx)) + menu.addAction(_("View Transaction"), lambda: self.main_window.show_transaction(tx)) channel_id = tx_item.get('channel_id') if channel_id and self.wallet.lnworker and (chan := self.wallet.lnworker.get_channel_by_id(bytes.fromhex(channel_id))): - menu.addAction(_("View Channel"), lambda: self.parent.show_channel_details(chan)) + menu.addAction(_("View Channel"), lambda: self.main_window.show_channel_details(chan)) if is_unconfirmed and tx: if tx_details.can_bump: - menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx)) + menu.addAction(_("Increase fee"), lambda: self.main_window.bump_fee_dialog(tx)) else: if tx_details.can_cpfp: - menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp_dialog(tx)) + menu.addAction(_("Child pays for parent"), lambda: self.main_window.cpfp_dialog(tx)) if tx_details.can_dscancel: - menu.addAction(_("Cancel (double-spend)"), lambda: self.parent.dscancel_dialog(tx)) + menu.addAction(_("Cancel (double-spend)"), lambda: self.main_window.dscancel_dialog(tx)) invoices = self.wallet.get_relevant_invoices_for_tx(tx_hash) if len(invoices) == 1: - menu.addAction(_("View invoice"), lambda inv=invoices[0]: self.parent.show_onchain_invoice(inv)) + menu.addAction(_("View invoice"), lambda inv=invoices[0]: self.main_window.show_onchain_invoice(inv)) elif len(invoices) > 1: menu_invs = menu.addMenu(_("Related invoices")) for inv in invoices: - menu_invs.addAction(_("View invoice"), lambda inv=inv: self.parent.show_onchain_invoice(inv)) + menu_invs.addAction(_("View invoice"), lambda inv=inv: self.main_window.show_onchain_invoice(inv)) if tx_URL: menu.addAction(_("View on block explorer"), lambda: webopen(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) @@ -809,24 +809,24 @@ def remove_local_tx(self, tx_hash: str): if num_child_txs > 0: question = (_("Are you sure you want to remove this transaction and {} child transactions?") .format(num_child_txs)) - if not self.parent.question(msg=question, + if not self.main_window.question(msg=question, title=_("Please confirm")): return self.wallet.adb.remove_transaction(tx_hash) self.wallet.save_db() # need to update at least: history_list, utxo_list, address_list - self.parent.need_update.set() + self.main_window.need_update.set() def onFileAdded(self, fn): try: with open(fn) as f: - tx = self.parent.tx_from_text(f.read()) + tx = self.main_window.tx_from_text(f.read()) except IOError as e: - self.parent.show_error(e) + self.main_window.show_error(e) return if not tx: return - self.parent.save_transaction_into_wallet(tx) + self.main_window.save_transaction_into_wallet(tx) def export_history_dialog(self): d = WindowModalDialog(self, _('Export History')) @@ -850,12 +850,12 @@ def export_history_dialog(self): self.do_export_history(filename, csv_button.isChecked()) except (IOError, os.error) as reason: export_error_label = _("Electrum was unable to produce a transaction export.") - self.parent.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history")) + self.main_window.show_critical(export_error_label + "\n" + str(reason), title=_("Unable to export history")) return - self.parent.show_message(_("Your wallet history has been successfully exported.")) + self.main_window.show_message(_("Your wallet history has been successfully exported.")) def do_export_history(self, file_name, is_csv): - hist = self.wallet.get_detailed_history(fx=self.parent.fx) + hist = self.wallet.get_detailed_history(fx=self.main_window.fx) txns = hist['transactions'] lines = [] if is_csv: diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index f1d6078eb..0ef2b8095 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -69,8 +69,10 @@ class Columns(MyTreeView.BaseColumnsEnum): def __init__(self, send_tab: 'SendTab'): window = send_tab.window - super().__init__(window, self.create_menu, - stretch_column=self.Columns.DESCRIPTION) + super().__init__( + main_window=window, + stretch_column=self.Columns.DESCRIPTION, + ) self.wallet = window.wallet self.send_tab = send_tab self.std_model = QStandardItemModel(self) @@ -115,7 +117,7 @@ def update(self): labels = [""] * len(self.Columns) labels[self.Columns.DATE] = format_time(timestamp) if timestamp else _('Unknown') labels[self.Columns.DESCRIPTION] = item.message - labels[self.Columns.AMOUNT] = self.parent.format_amount(amount, whitespaces=True) + labels[self.Columns.AMOUNT] = self.main_window.format_amount(amount, whitespaces=True) labels[self.Columns.STATUS] = item.get_status_str(status) items = [QStandardItem(e) for e in labels] self.set_editability(items) @@ -160,11 +162,11 @@ def create_menu(self, position): copy_menu = self.add_copy_menu(menu, idx) address = invoice.get_address() if address: - copy_menu.addAction(_("Address"), lambda: self.parent.do_copy(invoice.get_address(), title='Bitcoin Address')) + copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Address')) if invoice.is_lightning(): - menu.addAction(_("Details"), lambda: self.parent.show_lightning_invoice(invoice)) + menu.addAction(_("Details"), lambda: self.main_window.show_lightning_invoice(invoice)) else: - menu.addAction(_("Details"), lambda: self.parent.show_onchain_invoice(invoice)) + menu.addAction(_("Details"), lambda: self.main_window.show_onchain_invoice(invoice)) status = wallet.get_invoice_status(invoice) if status == PR_UNPAID: menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 8fe5a7898..f9fb2ff14 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -73,8 +73,10 @@ class Columns(MyTreeView.BaseColumnsEnum): def __init__(self, receive_tab: 'ReceiveTab'): window = receive_tab.window - super().__init__(window, self.create_menu, - stretch_column=self.Columns.DESCRIPTION) + super().__init__( + main_window=window, + stretch_column=self.Columns.DESCRIPTION, + ) self.wallet = window.wallet self.receive_tab = receive_tab self.std_model = QStandardItemModel(self) @@ -141,7 +143,7 @@ def update(self): amount = req.get_amount_sat() message = req.get_message() date = format_time(timestamp) - amount_str = self.parent.format_amount(amount) if amount else "" + amount_str = self.main_window.format_amount(amount) if amount else "" labels = [""] * len(self.Columns) labels[self.Columns.DATE] = date labels[self.Columns.DESCRIPTION] = message @@ -193,15 +195,15 @@ def create_menu(self, position): menu = QMenu(self) copy_menu = self.add_copy_menu(menu, idx) if req.get_address(): - copy_menu.addAction(_("Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address')) + copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(req.get_address(), title='Bitcoin Address')) if URI := self.wallet.get_request_URI(req): - copy_menu.addAction(_("Bitcoin URI"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) + copy_menu.addAction(_("Bitcoin URI"), lambda: self.main_window.do_copy(URI, title='Bitcoin URI')) if req.is_lightning(): - copy_menu.addAction(_("Lightning Request"), lambda: self.parent.do_copy(self.wallet.get_bolt11_invoice(req), title='Lightning Request')) + copy_menu.addAction(_("Lightning Request"), lambda: self.main_window.do_copy(self.wallet.get_bolt11_invoice(req), title='Lightning Request')) #if 'view_url' in req: # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) menu.addAction(_("Delete"), lambda: self.delete_requests([key])) - run_hook('receive_list_menu', self.parent, menu, key) + run_hook('receive_list_menu', self.main_window, menu, key) menu.exec_(self.viewport().mapToGlobal(position)) def delete_requests(self, keys): diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 26d15a0c3..b82b060bd 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -626,14 +626,21 @@ def _generate_next_value_(name: str, start: int, count: int, last_values): Columns: Type[BaseColumnsEnum] - def __init__(self, parent: 'ElectrumWindow', create_menu, *, - stretch_column=None, editable_columns=None): + def __init__( + self, + *, + parent: Optional[QWidget] = None, + main_window: Optional['ElectrumWindow'] = None, + stretch_column: Optional[int] = None, + editable_columns: Optional[Sequence[int]] = None, + ): + parent = parent or main_window super().__init__(parent) - self.parent = parent - self.config = self.parent.config + self.main_window = main_window + self.config = self.main_window.config if self.main_window else None self.stretch_column = stretch_column self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(create_menu) + self.customContextMenuRequested.connect(self.create_menu) self.setUniformRowHeights(True) # Control which columns are editable @@ -659,6 +666,9 @@ def __init__(self, parent: 'ElectrumWindow', create_menu, *, self._default_bg_brush = QStandardItem().background() + def create_menu(self, position: QPoint) -> None: + pass + def set_editability(self, items): for idx, i in enumerate(items): i.setEditable(idx in self.editable_columns) @@ -836,7 +846,7 @@ def add_copy_menu(self, menu: QMenu, idx) -> QMenu: return cc def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: - self.parent.do_copy(text, title=title) + self.main_window.do_copy(text, title=title) def showEvent(self, e: 'QShowEvent'): super().showEvent(e) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 4039928da..e4361394b 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -23,7 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import Optional, List, Dict, Sequence, Set +from typing import Optional, List, Dict, Sequence, Set, TYPE_CHECKING import enum import copy @@ -39,6 +39,9 @@ from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton from .new_channel_dialog import NewChannelDialog +if TYPE_CHECKING: + from .main_window import ElectrumWindow + class UTXOList(MyTreeView): _spend_set: Set[str] # coins selected by the user to spend from @@ -64,12 +67,14 @@ class Columns(MyTreeView.BaseColumnsEnum): ROLE_PREVOUT_STR = Qt.UserRole + 1000 key_role = ROLE_PREVOUT_STR - def __init__(self, parent): - super().__init__(parent, self.create_menu, - stretch_column=self.stretch_column) + def __init__(self, main_window: 'ElectrumWindow'): + super().__init__( + main_window=main_window, + stretch_column=self.stretch_column, + ) self._spend_set = set() self._utxo_dict = {} - self.wallet = self.parent.wallet + self.wallet = self.main_window.wallet self.std_model = QStandardItemModel(self) self.setModel(self.std_model) self.setSelectionMode(QAbstractItemView.ExtendedSelection) @@ -95,7 +100,7 @@ def update(self): labels = [""] * len(self.Columns) labels[self.Columns.OUTPOINT] = str(utxo.short_id) labels[self.Columns.ADDRESS] = utxo.address - labels[self.Columns.AMOUNT] = self.parent.format_amount(utxo.value_sats(), whitespaces=True) + labels[self.Columns.AMOUNT] = self.main_window.format_amount(utxo.value_sats(), whitespaces=True) utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR) @@ -115,11 +120,11 @@ def update_coincontrol_bar(self): coins = [self._utxo_dict[x] for x in self._spend_set] coins = self._filter_frozen_coins(coins) amount = sum(x.value_sats() for x in coins) - amount_str = self.parent.format_amount_and_units(amount) + amount_str = self.main_window.format_amount_and_units(amount) num_outputs_str = _("{} outputs available ({} total)").format(len(coins), len(self._utxo_dict)) - self.parent.set_coincontrol_msg(_("Coin control active") + f': {num_outputs_str}, {amount_str}') + self.main_window.set_coincontrol_msg(_("Coin control active") + f': {num_outputs_str}, {amount_str}') else: - self.parent.set_coincontrol_msg(None) + self.main_window.set_coincontrol_msg(None) def refresh_row(self, key, row): assert row is not None @@ -184,7 +189,7 @@ def add_selection_to_coincontrol(self): selected = self.get_selected_outpoints() coins = [self._utxo_dict[name] for name in selected] if not coins: - self.parent.show_error(_('You need to select coins from the list first.\nUse ctrl+left mouse button to select multiple items')) + self.main_window.show_error(_('You need to select coins from the list first.\nUse ctrl+left mouse button to select multiple items')) return self.add_to_coincontrol(coins) @@ -223,7 +228,7 @@ def can_swap_coins(self, coins): def swap_coins(self, coins): #self.clear_coincontrol() self.add_to_coincontrol(coins) - self.parent.run_swap_dialog(is_reverse=False, recv_amount_sat='!') + self.main_window.run_swap_dialog(is_reverse=False, recv_amount_sat='!') self.clear_coincontrol() def can_open_channel(self, coins): @@ -236,7 +241,7 @@ def open_channel_with_coins(self, coins): # todo : use a single dialog in new flow #self.clear_coincontrol() self.add_to_coincontrol(coins) - d = NewChannelDialog(self.parent) + d = NewChannelDialog(self.main_window) d.max_button.setChecked(True) d.max_button.setEnabled(False) d.min_button.setEnabled(False) @@ -247,15 +252,15 @@ def open_channel_with_coins(self, coins): self.clear_coincontrol() def clipboard_contains_address(self): - text = self.parent.app.clipboard().text() + text = self.main_window.app.clipboard().text() return is_address(text) def pay_to_clipboard_address(self, coins): - addr = self.parent.app.clipboard().text() + addr = self.main_window.app.clipboard().text() outputs = [PartialTxOutput.from_address_and_value(addr, '!')] #self.clear_coincontrol() self.add_to_coincontrol(coins) - self.parent.send_tab.pay_onchain_dialog(outputs) + self.main_window.send_tab.pay_onchain_dialog(outputs) self.clear_coincontrol() def create_menu(self, position): @@ -277,7 +282,7 @@ def create_menu(self, position): tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) - menu.addAction(_("Privacy analysis"), lambda: self.parent.show_utxo(utxo)) + menu.addAction(_("Privacy analysis"), lambda: self.main_window.show_utxo(utxo)) # fully spend menu_spend = menu.addMenu(_("Fully spend") + '…') m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) @@ -297,13 +302,13 @@ def create_menu(self, position): addr = utxo.address menu_freeze = menu.addMenu(_("Freeze")) if not self.wallet.is_frozen_coin(utxo): - menu_freeze.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True)) + menu_freeze.addAction(_("Freeze Coin"), lambda: self.main_window.set_frozen_state_of_coins([utxo], True)) else: - menu_freeze.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False)) + menu_freeze.addAction(_("Unfreeze Coin"), lambda: self.main_window.set_frozen_state_of_coins([utxo], False)) if not self.wallet.is_frozen_address(addr): - menu_freeze.addAction(_("Freeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], True)) + menu_freeze.addAction(_("Freeze Address"), lambda: self.main_window.set_frozen_state_of_addresses([addr], True)) else: - menu_freeze.addAction(_("Unfreeze Address"), lambda: self.parent.set_frozen_state_of_addresses([addr], False)) + menu_freeze.addAction(_("Unfreeze Address"), lambda: self.main_window.set_frozen_state_of_addresses([addr], False)) elif len(coins) > 1: # multiple items selected menu.addSeparator() addrs = [utxo.address for utxo in coins] @@ -311,13 +316,13 @@ def create_menu(self, position): is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins] menu_freeze = menu.addMenu(_("Freeze")) if not all(is_coin_frozen): - menu_freeze.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True)) + menu_freeze.addAction(_("Freeze Coins"), lambda: self.main_window.set_frozen_state_of_coins(coins, True)) if any(is_coin_frozen): - menu_freeze.addAction(_("Unfreeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, False)) + menu_freeze.addAction(_("Unfreeze Coins"), lambda: self.main_window.set_frozen_state_of_coins(coins, False)) if not all(is_addr_frozen): - menu_freeze.addAction(_("Freeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, True)) + menu_freeze.addAction(_("Freeze Addresses"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, True)) if any(is_addr_frozen): - menu_freeze.addAction(_("Unfreeze Addresses"), lambda: self.parent.set_frozen_state_of_addresses(addrs, False)) + menu_freeze.addAction(_("Unfreeze Addresses"), lambda: self.main_window.set_frozen_state_of_addresses(addrs, False)) menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/electrum/gui/qt/watchtower_dialog.py b/electrum/gui/qt/watchtower_dialog.py index 89033ae9b..a1083b5d9 100644 --- a/electrum/gui/qt/watchtower_dialog.py +++ b/electrum/gui/qt/watchtower_dialog.py @@ -32,15 +32,16 @@ class WatcherList(MyTreeView): - def __init__(self, parent): - super().__init__(parent, self.create_menu, stretch_column=0) + def __init__(self, parent: 'WatchtowerDialog'): + super().__init__( + parent=parent, + stretch_column=0, + ) + self.parent = parent self.setModel(QStandardItemModel(self)) self.setSortingEnabled(True) self.update() - def create_menu(self, x): - pass - def update(self): if self.parent.lnwatcher is None: return From 17407651250370f1510a00a8c7b9471c4b3b2fd1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 18:51:34 +0000 Subject: [PATCH 0358/1143] qt settings_dialog: fix trampoline_cb --- electrum/gui/qt/settings_dialog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 7dcbf20e0..6aeaf4a59 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -117,8 +117,7 @@ def on_trampoline_checked(use_trampoline): _("Without this option, Electrum will need to sync with the Lightning network on every start."), _("This may impact the reliability of your payments."), ])): - # FIXME: Qt bug? stateChanged not triggered on second click - trampoline_cb.setChecked(True) + trampoline_cb.setCheckState(Qt.Checked) return self.config.set_key('use_gossip', not use_trampoline) if not use_trampoline: From acc1f224425fc29467872471d482b80844ac744f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 13 Mar 2023 19:00:46 +0000 Subject: [PATCH 0359/1143] qt: MyTreeView: small clean-up for WatcherList and ContactList --- electrum/gui/qt/contact_list.py | 5 ++++- electrum/gui/qt/watchtower_dialog.py | 25 ++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index ff805feab..8dcb5a4b2 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -110,7 +110,10 @@ def update(self): set_current = None for key in sorted(self.main_window.contacts.keys()): contact_type, name = self.main_window.contacts[key] - items = [QStandardItem(x) for x in (name, key)] + labels = [""] * len(self.Columns) + labels[self.Columns.NAME] = name + labels[self.Columns.ADDRESS] = key + items = [QStandardItem(x) for x in labels] items[self.Columns.NAME].setEditable(contact_type != 'openalias') items[self.Columns.ADDRESS].setEditable(False) items[self.Columns.NAME].setData(key, self.ROLE_CONTACT_KEY) diff --git a/electrum/gui/qt/watchtower_dialog.py b/electrum/gui/qt/watchtower_dialog.py index a1083b5d9..37e943d0b 100644 --- a/electrum/gui/qt/watchtower_dialog.py +++ b/electrum/gui/qt/watchtower_dialog.py @@ -23,6 +23,8 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import enum + from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QPushButton, QLabel) @@ -32,10 +34,22 @@ class WatcherList(MyTreeView): + + class Columns(MyTreeView.BaseColumnsEnum): + OUTPOINT = enum.auto() + TX_COUNT = enum.auto() + STATUS = enum.auto() + + headers = { + Columns.OUTPOINT: _('Outpoint'), + Columns.TX_COUNT: _('Tx'), + Columns.STATUS: _('Status'), + } + def __init__(self, parent: 'WatchtowerDialog'): super().__init__( parent=parent, - stretch_column=0, + stretch_column=self.Columns.OUTPOINT, ) self.parent = parent self.setModel(QStandardItemModel(self)) @@ -46,13 +60,18 @@ def update(self): if self.parent.lnwatcher is None: return self.model().clear() - self.update_headers({0:_('Outpoint'), 1:_('Tx'), 2:_('Status')}) + self.update_headers(self.__class__.headers) lnwatcher = self.parent.lnwatcher l = lnwatcher.list_sweep_tx() for outpoint in l: n = lnwatcher.get_num_tx(outpoint) status = lnwatcher.get_channel_status(outpoint) - items = [QStandardItem(e) for e in [outpoint, "%d"%n, status]] + labels = [""] * len(self.Columns) + labels[self.Columns.OUTPOINT] = outpoint + labels[self.Columns.TX_COUNT] = str(n) + labels[self.Columns.STATUS] = status + items = [QStandardItem(e) for e in labels] + self.set_editability(items) self.model().insertRow(self.model().rowCount(), items) size = lnwatcher.sweepstore.filesize() self.parent.size_label.setText('Database size: %.2f Mb'%(size/1024/1024.)) From c0ce0296f8b6f73e1ed790b740785852865ff63b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 10:06:31 +0100 Subject: [PATCH 0360/1143] qml: show channel backups together with channels in Channels, remove Channel backups button from WalletDetails, filter backups to bottom, add backups section header --- electrum/gui/qml/components/Channels.qml | 33 +++++++++++++++++-- electrum/gui/qml/components/WalletDetails.qml | 10 ------ electrum/gui/qml/qechannellistmodel.py | 2 +- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 4bb7305ed..c75661bed 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -78,7 +78,22 @@ Pane { Layout.preferredWidth: parent.width Layout.fillHeight: true clip: true - model: Daemon.currentWallet.channelModel.filterModelNoBackups() + model: Daemon.currentWallet.channelModel + + section.property: 'is_backup' + section.criteria: ViewSection.FullString + section.delegate: RowLayout { + width: ListView.view.width + required property string section + Label { + visible: section == 'true' + text: qsTr('Channel backups') + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingLarge + font.pixelSize: constants.fontSizeSmall + color: Material.accentColor + } + } delegate: ChannelDelegate { onClicked: { @@ -102,7 +117,6 @@ Pane { } } - ButtonContainer { Layout.fillWidth: true FlatButton { @@ -158,4 +172,19 @@ Pane { } } + Component { + id: importChannelBackupDialog + ImportChannelBackupDialog { + onClosed: destroy() + } + } + + Connections { + target: Daemon.currentWallet + function onImportChannelBackupFailed(message) { + var dialog = app.messageDialog.createObject(root, { text: message }) + dialog.open() + } + } + } diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 100ee77a9..80acb6227 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -527,16 +527,6 @@ Pane { visible: Daemon.currentWallet && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning icon.source: '../../icons/lightning.png' } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Channel backups') - visible: Daemon.currentWallet && Daemon.currentWallet.isLightning - icon.source: '../../icons/lightning.png' - onClicked: { - app.stack.push(Qt.resolvedUrl('ChannelBackups.qml')) - } - } } } diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index c25f1dd4d..6cd4459e9 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -119,7 +119,7 @@ def init_model(self): # sort, for now simply by state def chan_sort_score(c): - return c['state_code'] + return c['state_code'] + (10 if c['is_backup'] else 0) channels.sort(key=chan_sort_score) self.clear() From 497934688179c3626105b7459a397b91c248c688 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 10:20:42 +0100 Subject: [PATCH 0361/1143] qml: detect channel backups in send dialog --- electrum/gui/qml/components/SendDialog.qml | 3 +++ electrum/gui/qml/components/WalletMainView.qml | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index 11df6b8f6..edf438446 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -13,6 +13,7 @@ ElDialog { property InvoiceParser invoiceParser signal txFound(data: string) + signal channelBackupFound(data: string) parent: Overlay.overlay modal: true @@ -32,6 +33,8 @@ ElDialog { function dispatch(data) { if (bitcoin.isRawTx(data)) { txFound(data) + } else if (Daemon.currentWallet.isValidChannelBackup(data)) { + channelBackupFound(data) } else { invoiceParser.recipient = data } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index e664e7cb1..a4d64daac 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -308,6 +308,20 @@ Item { app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data }) close() } + onChannelBackupFound: { + var dialog = app.messageDialog.createObject(app, { + text: qsTr('Import Channel backup?'), + yesno: true + }) + dialog.yesClicked.connect(function() { + Daemon.currentWallet.importChannelBackup(data) + close() + }) + dialog.rejected.connect(function() { + close() + }) + dialog.open() + } onClosed: destroy() } } From 842229c4bb190389994c30a57a44c775a0af3d07 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 12:36:31 +0100 Subject: [PATCH 0362/1143] qt: fit StatusBarButton to inner height of status bar --- electrum/gui/qt/main_window.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e7060d215..c19ab44a9 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -111,14 +111,15 @@ class StatusBarButton(QToolButton): # note: this class has a custom stylesheet applied in stylesheet_patcher.py - def __init__(self, icon, tooltip, func): + def __init__(self, icon, tooltip, func, size=0): QToolButton.__init__(self) self.setText('') self.setIcon(icon) self.setToolTip(tooltip) self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.setAutoRaise(True) - size = max(25, round(1.8 * font_height())) + if not size: + size = max(25, round(1.8 * font_height())) self.setMaximumWidth(size) self.clicked.connect(self.onPress) self.func = func @@ -1527,6 +1528,7 @@ def create_status_bar(self): font_height = QFontMetrics(self.balance_label.font()).height() sb_height = max(35, int(2 * font_height)) sb.setFixedHeight(sb_height) + sb_inner_height = sb.childrenRect().height() # remove border of all items in status bar self.setStyleSheet("QStatusBar::item { border: 0px;} ") @@ -1546,18 +1548,18 @@ def create_status_bar(self): self.tasks_label = QLabel('') sb.addPermanentWidget(self.tasks_label) - self.password_button = StatusBarButton(QIcon(), _("Password"), self.change_password_dialog) + self.password_button = StatusBarButton(QIcon(), _("Password"), self.change_password_dialog, sb_inner_height) sb.addPermanentWidget(self.password_button) - sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog)) - self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog) + sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog, sb_inner_height)) + self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog, sb_inner_height) sb.addPermanentWidget(self.seed_button) - self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog) + self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog, sb_inner_height) sb.addPermanentWidget(self.lightning_button) self.update_lightning_icon() self.status_button = None if self.network: - self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog) + self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog, sb_inner_height) sb.addPermanentWidget(self.status_button) run_hook('create_status_bar', sb) self.setStatusBar(sb) From 7fc4153f462aef31b660e8281bc34fda38372bf9 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 12:42:52 +0100 Subject: [PATCH 0363/1143] qml: render balance bar using (local|remote) capacity instead of can_(send|receive) --- electrum/gui/qml/components/controls/ChannelDelegate.qml | 8 ++++---- electrum/gui/qml/qechannellistmodel.py | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index 962fa7164..4728a0865 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -113,10 +113,10 @@ ItemDelegate { onWidthChanged: { var cap = model.capacity.satsInt * 1000 var twocap = cap * 2 - b1.width = width * (cap - model.can_send.msatsInt) / twocap - b2.width = width * model.can_send.msatsInt / twocap - b3.width = width * model.can_receive.msatsInt / twocap - b4.width = width * (cap - model.can_receive.msatsInt) / twocap + b1.width = width * (cap - model.local_capacity.msatsInt) / twocap + b2.width = width * model.local_capacity.msatsInt / twocap + b3.width = width * model.remote_capacity.msatsInt / twocap + b4.width = width * (cap - model.remote_capacity.msatsInt) / twocap } Rectangle { id: b1 diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 6cd4459e9..9a94c8c92 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -17,7 +17,7 @@ class QEChannelListModel(QAbstractListModel, QtEventListener): _ROLE_NAMES=('cid','state','state_code','initiator','capacity','can_send', 'can_receive','l_csv_delay','r_csv_delay','send_frozen','receive_frozen', 'type','node_id','node_alias','short_cid','funding_tx','is_trampoline', - 'is_backup', 'is_imported') + 'is_backup', 'is_imported', 'local_capacity', 'remote_capacity') _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)) @@ -91,10 +91,14 @@ def channel_to_model(self, lnc): if lnc.is_backup(): item['can_send'] = QEAmount() item['can_receive'] = QEAmount() + item['local_capacity'] = QEAmount() + item['remote_capacity'] = QEAmount() item['is_imported'] = lnc.is_imported else: item['can_send'] = QEAmount(amount_msat=lnc.available_to_spend(LOCAL)) item['can_receive'] = QEAmount(amount_msat=lnc.available_to_spend(REMOTE)) + item['local_capacity'] = QEAmount(amount_msat=lnc.balance(LOCAL)) + item['remote_capacity'] = QEAmount(amount_msat=lnc.balance(REMOTE)) item['is_imported'] = False return item From 5feb16ad751fd55db5113958653a7f93aabb1954 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 12:46:00 +0100 Subject: [PATCH 0364/1143] qml: SeedTextArea only lower case input --- electrum/gui/qml/components/controls/SeedTextArea.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 3ab6e95d9..49c9bc25a 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -77,7 +77,7 @@ Pane { font.bold: true font.pixelSize: constants.fontSizeLarge font.family: FixedFont - inputMethodHints: Qt.ImhSensitiveData | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhLowercaseOnly | Qt.ImhNoPredictiveText background: Rectangle { color: constants.darkerBackground From 876b0ff295a4eac36f3f71ab0425c41fceed25e4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 12:41:51 +0100 Subject: [PATCH 0365/1143] qml: handle empty histogram more gracefully, set histogram limit to 10MB --- electrum/gui/qml/qenetwork.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index d32a93ac4..4caa44f07 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -95,7 +95,7 @@ def on_event_fee_histogram(self, histogram): def update_histogram(self, histogram): # cap the histogram to a limited number of megabytes - bytes_limit=25*1000*1000 + bytes_limit=10*1000*1000 bytes_current = 0 capped_histogram = [] for item in sorted(histogram, key=lambda x: x[0], reverse=True): @@ -109,8 +109,8 @@ def update_histogram(self, histogram): self._fee_histogram = { 'histogram': capped_histogram, 'total': bytes_current, - 'min_fee': capped_histogram[-1][0], - 'max_fee': capped_histogram[0][0] + 'min_fee': capped_histogram[-1][0] if capped_histogram else FEERATE_DEFAULT_RELAY/1000, + 'max_fee': capped_histogram[0][0] if capped_histogram else FEERATE_DEFAULT_RELAY/1000 } self.feeHistogramUpdated.emit() From 950d8f488591175a082c271c68c8d611d3339930 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 14:01:03 +0100 Subject: [PATCH 0366/1143] qml: Pin dialog wider (small form factor issue) --- electrum/gui/qml/components/Pin.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index 9e58efb40..b27c4a671 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -13,7 +13,7 @@ ElDialog { title: qsTr('PIN') iconSource: '../../../icons/lock.png' - width: parent.width * 2/3 + width: parent.width * 3/4 anchors.centerIn: parent From 1b0a58a0ff77c9048fe35f744c007f7ab3d2847b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 14:49:58 +0100 Subject: [PATCH 0367/1143] qml: don't pass lightning flag from GUI when creating payment requests --- electrum/gui/qml/components/ReceiveDialog.qml | 21 ++++++++++++------- electrum/gui/qml/qewallet.py | 7 ++----- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 290f89533..d8cb65229 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -206,18 +206,31 @@ ElDialog { color: Material.accentColor } Label { + visible: request.message Layout.fillWidth: true text: request.message wrapMode: Text.Wrap } + Label { + visible: !request.message + Layout.fillWidth: true + text: qsTr('unspecified') + color: constants.mutedForeground + } Label { text: qsTr('Amount') color: Material.accentColor } FormattedAmount { + visible: !request.amount.isEmpty valid: !request.amount.isEmpty amount: request.amount } + Label { + visible: request.amount.isEmpty + text: qsTr('unspecified') + color: constants.mutedForeground + } } Rectangle { @@ -323,13 +336,7 @@ ElDialog { function createRequest() { var qamt = Config.unitsToSats(receiveDetailsDialog.amount) - if (qamt.satsInt > Daemon.currentWallet.lightningCanReceive.satsInt) { - console.log('Creating OnChain request') - Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, false, _ignore_gaplimit, _reuse_address) - } else { - console.log('Creating Lightning request') - Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, true, _ignore_gaplimit, _reuse_address) - } + Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, _ignore_gaplimit, _reuse_address) } function createDefaultRequest() { diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index cdfcf7000..9893061c0 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -616,12 +616,9 @@ def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) - def createRequest(self, amount: QEAmount, message: str, expiration: int, is_lightning: bool = False, ignore_gap: bool = False, reuse_address: bool = False): + def createRequest(self, amount: QEAmount, message: str, expiration: int, ignore_gap: bool = False, reuse_address: bool = False): try: - if is_lightning: - if not self.wallet.lnworker.channels: - self.requestCreateError.emit('fatal',_("You need to open a Lightning channel first.")) - return + if self.wallet.lnworker and self.wallet.lnworker.channels: # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) # TODO fallback address robustness addr = self.wallet.get_unused_address() From 1f4cedf56a342ed4b1e1e5dea69e037dd1f8083f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 14 Mar 2023 10:57:26 +0100 Subject: [PATCH 0368/1143] Rework PaytoEdit: - show a QLineEdit by default, a QTextEdit only if paytomany is active. paytomany is a rare use case, it should not interfer with regular use (e.g. when a user inadvertently types enter). - this also fixes the visual appearance if the payto line - keep paytomany menu in sync with actual state --- electrum/gui/qt/paytoedit.py | 180 +++++++++++++++++++----------- electrum/gui/qt/send_tab.py | 50 +++++---- electrum/gui/qt/util.py | 208 +++++++++++++++++++---------------- 3 files changed, 254 insertions(+), 184 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 7b40c937d..fc1cd7916 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -25,14 +25,15 @@ import re import decimal +from functools import partial from decimal import Decimal from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from PyQt5.QtGui import QFontMetrics, QFont -from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout from electrum import bitcoin -from electrum.util import bfh, parse_max_spend, FailedToParsePaymentIdentifier +from electrum.util import parse_max_spend, FailedToParsePaymentIdentifier from electrum.transaction import PartialTxOutput from electrum.bitcoin import opcodes, construct_script from electrum.logging import Logger @@ -41,7 +42,7 @@ from .qrtextedit import ScanQRTextEdit from .completion_text_edit import CompletionTextEdit from . import util -from .util import MONOSPACE_FONT +from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -61,48 +62,112 @@ class PayToLineError(NamedTuple): is_multiline: bool = False -class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): - def __init__(self, send_tab: 'SendTab'): - CompletionTextEdit.__init__(self) - ScanQRTextEdit.__init__(self, config=send_tab.config, setText=self._on_input_btn, is_payto=True) - Logger.__init__(self) - self.send_tab = send_tab - self.win = send_tab.window - self.app = QApplication.instance() - self.amount_edit = self.send_tab.amount_e - self.setFont(QFont(MONOSPACE_FONT)) +class ResizingTextEdit(QTextEdit): + + def __init__(self): + QTextEdit.__init__(self) document = self.document() document.contentsChanged.connect(self.update_size) - fontMetrics = QFontMetrics(document.defaultFont()) self.fontSpacing = fontMetrics.lineSpacing() - margins = self.contentsMargins() documentMargin = document.documentMargin() self.verticalMargins = margins.top() + margins.bottom() self.verticalMargins += self.frameWidth() * 2 self.verticalMargins += documentMargin * 2 - self.heightMin = self.fontSpacing + self.verticalMargins self.heightMax = (self.fontSpacing * 10) + self.verticalMargins + self.update_size() + + def update_size(self): + docLineCount = self.document().lineCount() + docHeight = max(3, docLineCount) * self.fontSpacing + h = docHeight + self.verticalMargins + h = min(max(h, self.heightMin), self.heightMax) + self.setMinimumHeight(int(h)) + self.setMaximumHeight(int(h)) + self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) + + +class PayToEdit(Logger, GenericInputHandler): - self.c = None - self.addPasteButton(setText=self._on_input_btn) - self.textChanged.connect(self._on_text_changed) + def __init__(self, send_tab: 'SendTab'): + Logger.__init__(self) + GenericInputHandler.__init__(self) + self.line_edit = QLineEdit() + self.text_edit = ResizingTextEdit() + self.text_edit.hide() + self._is_paytomany = False + for w in [self.line_edit, self.text_edit]: + w.setFont(QFont(MONOSPACE_FONT)) + w.textChanged.connect(self._on_text_changed) + self.send_tab = send_tab + self.config = send_tab.config + self.win = send_tab.window + self.app = QApplication.instance() + self.amount_edit = self.send_tab.amount_e + + self.is_multiline = False self.outputs = [] # type: List[PartialTxOutput] self.errors = [] # type: List[PayToLineError] self.disable_checks = False self.is_alias = False - self.update_size() self.payto_scriptpubkey = None # type: Optional[bytes] self.lightning_invoice = None self.previous_payto = '' + # editor methods + self.setStyleSheet = self.editor.setStyleSheet + self.setText = self.editor.setText + self.setEnabled = self.editor.setEnabled + self.setReadOnly = self.editor.setReadOnly + # button handlers + self.on_qr_from_camera_input_btn = partial( + self.input_qr_from_camera, + config=self.config, + allow_multi=False, + show_error=self.win.show_error, + setText=self._on_input_btn, + ) + self.on_qr_from_screenshot_input_btn = partial( + self.input_qr_from_screenshot, + allow_multi=False, + show_error=self.win.show_error, + setText=self._on_input_btn, + ) + self.on_input_file = partial( + self.input_file, + config=self.config, + show_error=self.win.show_error, + setText=self._on_input_btn, + ) + # + self.line_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.line_edit, self) + self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self) + + @property + def editor(self): + return self.text_edit if self.is_paytomany() else self.line_edit + + def set_paytomany(self, b): + self._is_paytomany = b + self.line_edit.setVisible(not b) + self.text_edit.setVisible(b) + self.send_tab.paytomany_menu.setChecked(b) + + def toggle_paytomany(self): + self.set_paytomany(not self._is_paytomany) + + def toPlainText(self): + return self.text_edit.toPlainText() if self.is_paytomany() else self.line_edit.text() + + def is_paytomany(self): + return self._is_paytomany def setFrozen(self, b): self.setReadOnly(b) - self.setStyleSheet(frozen_style if b else normal_style) - self.overlay_widget.setHidden(b) + if not b: + self.setStyleSheet(normal_style) def setTextNoCheck(self, text: str): """Sets the text, while also ensuring the new value will not be resolved/checked.""" @@ -110,9 +175,11 @@ def setTextNoCheck(self, text: str): self.setText(text) def do_clear(self): + self.set_paytomany(False) self.disable_checks = False self.is_alias = False - self.setText('') + self.line_edit.setText('') + self.text_edit.setText('') self.setFrozen(False) self.setEnabled(True) @@ -134,12 +201,12 @@ def parse_address_and_amount(self, line) -> PartialTxOutput: def parse_output(self, x) -> bytes: try: address = self.parse_address(x) - return bfh(bitcoin.address_to_script(address)) + return bytes.fromhex(bitcoin.address_to_script(address)) except Exception: pass try: script = self.parse_script(x) - return bfh(script) + return bytes.fromhex(script) except Exception: pass raise Exception("Invalid address or script.") @@ -151,7 +218,7 @@ def parse_script(self, x): opcode_int = opcodes[word] script += construct_script([opcode_int]) else: - bfh(word) # to test it is hex data + bytes.fromhex(word) # to test it is hex data script += construct_script([word]) return script @@ -176,30 +243,37 @@ def parse_address(self, line): def _on_input_btn(self, text: str): self.setText(text) - self._check_text(full_check=True) def _on_text_changed(self): - if self.app.clipboard().text() == self.toPlainText(): - # user likely pasted from clipboard - self._check_text(full_check=True) - else: - self._check_text(full_check=False) + text = self.toPlainText() + # False if user pasted from clipboard + full_check = self.app.clipboard().text() != text + self._check_text(text, full_check=full_check) + if self.is_multiline and not self._is_paytomany: + self.set_paytomany(True) + self.text_edit.setText(text) def on_timer_check_text(self): - if self.hasFocus(): + if self.editor.hasFocus(): return - self._check_text(full_check=True) - - def _check_text(self, *, full_check: bool): - if self.previous_payto == str(self.toPlainText()).strip(): + text = self.toPlainText() + self._check_text(text, full_check=True) + + def _check_text(self, text, *, full_check: bool): + """ + side effects: self.is_multiline, self.errors, self.outputs + """ + if self.previous_payto == str(text).strip(): return if full_check: - self.previous_payto = str(self.toPlainText()).strip() + self.previous_payto = str(text).strip() self.errors = [] if self.disable_checks: return # filter out empty lines - lines = [i for i in self.lines() if i] + lines = text.split('\n') + lines = [i for i in lines if i] + self.is_multiline = len(lines)>1 self.payto_scriptpubkey = None self.lightning_invoice = None @@ -242,6 +316,7 @@ def _check_text(self, *, full_check: bool): # there are multiple lines self._parse_as_multiline(lines, raise_errors=False) + def _parse_as_multiline(self, lines, *, raise_errors: bool): outputs = [] # type: List[PartialTxOutput] total = 0 @@ -292,33 +367,6 @@ def get_outputs(self, is_max: bool) -> List[PartialTxOutput]: return self.outputs[:] - def lines(self): - return self.toPlainText().split('\n') - - def is_multiline(self): - return len(self.lines()) > 1 - - def paytomany(self): - self.setTextNoCheck("\n\n\n") - self.update_size() - - def update_size(self): - docLineCount = self.document().lineCount() - if self.cursorRect().right() + 1 >= self.overlay_widget.pos().x(): - # Add a line if we are under the overlay widget - docLineCount += 1 - docHeight = docLineCount * self.fontSpacing - - h = docHeight + self.verticalMargins - h = min(max(h, self.heightMin), self.heightMax) - self.setMinimumHeight(int(h)) - self.setMaximumHeight(int(h)) - - self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) - - # The scrollbar visibility can have changed so we update the overlay position here - self._updateOverlayPos() - def _resolve_openalias(self, text: str) -> Optional[dict]: key = text key = key.strip() # strip whitespaces diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 640f1a03a..4b3f9b46e 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -9,7 +9,7 @@ from PyQt5.QtCore import pyqtSignal, QPoint from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, - QHBoxLayout, QCompleter, QWidget, QToolTip) + QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton) from electrum import util, paymentrequest from electrum import lnutil @@ -85,20 +85,21 @@ def __init__(self, window: 'ElectrumWindow'): "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 1, 0) - grid.addWidget(self.payto_e, 1, 1, 1, -1) + grid.addWidget(self.payto_e.line_edit, 1, 1, 1, 4) + grid.addWidget(self.payto_e.text_edit, 1, 1, 1, 4) - completer = QCompleter() - completer.setCaseSensitivity(False) - self.payto_e.set_completer(completer) - completer.setModel(self.window.completions) + #completer = QCompleter() + #completer.setCaseSensitivity(False) + #self.payto_e.set_completer(completer) + #completer.setModel(self.window.completions) msg = _('Description of the transaction (not mandatory).') + '\n\n' \ + _( 'The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.') description_label = HelpLabel(_('Description'), msg) grid.addWidget(description_label, 2, 0) - self.message_e = SizedFreezableLineEdit(width=700) - grid.addWidget(self.message_e, 2, 1, 1, -1) + self.message_e = SizedFreezableLineEdit(width=600) + grid.addWidget(self.message_e, 2, 1, 1, 4) msg = (_('The amount to be received by the recipient.') + ' ' + _('Fees are paid by the sender.') + '\n\n' @@ -127,9 +128,16 @@ def __init__(self, window: 'ElectrumWindow'): self.save_button = EnterButton(_("Save"), self.do_save_invoice) self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice) self.clear_button = EnterButton(_("Clear"), self.do_clear) + self.paste_button = QPushButton() + self.paste_button.clicked.connect(lambda: self.payto_e._on_input_btn(self.window.app.clipboard().text())) + self.paste_button.setIcon(read_QIcon('copy.png')) + self.paste_button.setToolTip(_('Paste invoice from clipboard')) + self.paste_button.setMaximumWidth(35) + grid.addWidget(self.paste_button, 1, 5) buttons = QHBoxLayout() buttons.addStretch(1) + #buttons.addWidget(self.paste_button) buttons.addWidget(self.clear_button) buttons.addWidget(self.save_button) buttons.addWidget(self.send_button) @@ -151,10 +159,12 @@ def reset_max(text): from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) self.toolbar, menu = self.invoice_list.create_toolbar_with_menu('') + + menu.addAction(read_QIcon(get_iconname_camera()), _("Read QR code with camera"), self.payto_e.on_qr_from_camera_input_btn) menu.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), self.payto_e.on_qr_from_screenshot_input_btn) menu.addAction(read_QIcon("file.png"), _("Read invoice from file"), self.payto_e.on_input_file) - menu.addToggle(_("&Pay to many"), self.paytomany) + self.paytomany_menu = menu.addToggle(_("&Pay to many"), self.toggle_paytomany) menu.addSeparator() menu.addAction(_("Import invoices"), self.window.import_invoices) menu.addAction(_("Export invoices"), self.window.export_invoices) @@ -755,18 +765,16 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.window.on_error) - def paytomany(self): - if self.payto_e.is_multiline(): - self.payto_e.do_clear() - return - self.payto_e.paytomany() - message = '\n'.join([ - _('Enter a list of outputs in the \'Pay to\' field.'), - _('One output per line.'), - _('Format: address, amount'), - _('You may load a CSV file using the file icon.') - ]) - self.window.show_tooltip_after_delay(message) + def toggle_paytomany(self): + self.payto_e.toggle_paytomany() + if self.payto_e.is_paytomany(): + message = '\n'.join([ + _('Enter a list of outputs in the \'Pay to\' field.'), + _('One output per line.'), + _('Format: address, amount'), + _('You may load a CSV file using the file icon.') + ]) + self.window.show_tooltip_after_delay(message) def payto_contacts(self, labels): paytos = [self.window.get_contact_payto(label) for label in labels] diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index b82b060bd..0ed206342 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -920,7 +920,116 @@ def get_iconname_camera() -> str: return "camera_white.png" if ColorScheme.dark_scheme else "camera_dark.png" -class OverlayControlMixin: +def editor_contextMenuEvent(self, p, e): + m = self.createStandardContextMenu() + m.addSeparator() + m.addAction(read_QIcon(get_iconname_camera()), _("Read QR code with camera"), p.on_qr_from_camera_input_btn) + m.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), p.on_qr_from_screenshot_input_btn) + m.addAction(read_QIcon("file.png"), _("Read file"), p.on_input_file) + m.exec_(e.globalPos()) + + +class GenericInputHandler: + + def input_qr_from_camera( + self, + *, + config: 'SimpleConfig', + allow_multi: bool = False, + show_error: Callable[[str], None], + setText: Callable[[str], None] = None, + ) -> None: + if setText is None: + setText = self.setText + def cb(success: bool, error: str, data): + if not success: + if error: + show_error(error) + return + if not data: + data = '' + if allow_multi: + new_text = self.text() + data + '\n' + else: + new_text = data + setText(new_text) + + from .qrreader import scan_qrcode + scan_qrcode(parent=self, config=config, callback=cb) + + def input_qr_from_screenshot( + self, + *, + allow_multi: bool = False, + show_error: Callable[[str], None], + setText: Callable[[str], None] = None, + ) -> None: + if setText is None: + setText = self.setText + from .qrreader import scan_qr_from_image + scanned_qr = None + for screen in QApplication.instance().screens(): + try: + scan_result = scan_qr_from_image(screen.grabWindow(0).toImage()) + except MissingQrDetectionLib as e: + show_error(_("Unable to scan image.") + "\n" + repr(e)) + return + if len(scan_result) > 0: + if (scanned_qr is not None) or len(scan_result) > 1: + show_error(_("More than one QR code was found on the screen.")) + return + scanned_qr = scan_result + if scanned_qr is None: + show_error(_("No QR code was found on the screen.")) + return + data = scanned_qr[0].data + if allow_multi: + new_text = self.text() + data + '\n' + else: + new_text = data + setText(new_text) + + def input_file( + self, + *, + config: 'SimpleConfig', + show_error: Callable[[str], None], + setText: Callable[[str], None] = None, + ) -> None: + if setText is None: + setText = self.setText + fileName = getOpenFileName( + parent=None, + title='select file', + config=config, + ) + if not fileName: + return + try: + try: + with open(fileName, "r") as f: + data = f.read() + except UnicodeError as e: + with open(fileName, "rb") as f: + data = f.read() + data = data.hex() + except BaseException as e: + show_error(_('Error opening file') + ':\n' + repr(e)) + else: + setText(data) + + def input_paste_from_clipboard( + self, + *, + setText: Callable[[str], None] = None, + ) -> None: + if setText is None: + setText = self.setText + app = QApplication.instance() + setText(app.clipboard().text()) + + +class OverlayControlMixin(GenericInputHandler): STYLE_SHEET_COMMON = ''' QPushButton { border-width: 1px; padding: 0px; margin: 0px; } ''' @@ -931,6 +1040,7 @@ class OverlayControlMixin: ''' def __init__(self, middle: bool = False): + GenericInputHandler.__init__(self) assert isinstance(self, QWidget) assert isinstance(self, OverlayControlMixin) # only here for type-hints in IDE self.middle = middle @@ -1107,102 +1217,6 @@ def add_menu_button( menu.addAction(read_QIcon(opt_icon), opt_text, opt_cb) btn.setMenu(menu) - def input_qr_from_camera( - self, - *, - config: 'SimpleConfig', - allow_multi: bool = False, - show_error: Callable[[str], None], - setText: Callable[[str], None] = None, - ) -> None: - if setText is None: - setText = self.setText - def cb(success: bool, error: str, data): - if not success: - if error: - show_error(error) - return - if not data: - data = '' - if allow_multi: - new_text = self.text() + data + '\n' - else: - new_text = data - setText(new_text) - - from .qrreader import scan_qrcode - scan_qrcode(parent=self, config=config, callback=cb) - - def input_qr_from_screenshot( - self, - *, - allow_multi: bool = False, - show_error: Callable[[str], None], - setText: Callable[[str], None] = None, - ) -> None: - if setText is None: - setText = self.setText - from .qrreader import scan_qr_from_image - scanned_qr = None - for screen in QApplication.instance().screens(): - try: - scan_result = scan_qr_from_image(screen.grabWindow(0).toImage()) - except MissingQrDetectionLib as e: - show_error(_("Unable to scan image.") + "\n" + repr(e)) - return - if len(scan_result) > 0: - if (scanned_qr is not None) or len(scan_result) > 1: - show_error(_("More than one QR code was found on the screen.")) - return - scanned_qr = scan_result - if scanned_qr is None: - show_error(_("No QR code was found on the screen.")) - return - data = scanned_qr[0].data - if allow_multi: - new_text = self.text() + data + '\n' - else: - new_text = data - setText(new_text) - - def input_file( - self, - *, - config: 'SimpleConfig', - show_error: Callable[[str], None], - setText: Callable[[str], None] = None, - ) -> None: - if setText is None: - setText = self.setText - fileName = getOpenFileName( - parent=self, - title='select file', - config=config, - ) - if not fileName: - return - try: - try: - with open(fileName, "r") as f: - data = f.read() - except UnicodeError as e: - with open(fileName, "rb") as f: - data = f.read() - data = data.hex() - except BaseException as e: - show_error(_('Error opening file') + ':\n' + repr(e)) - else: - setText(data) - - def input_paste_from_clipboard( - self, - *, - setText: Callable[[str], None] = None, - ) -> None: - if setText is None: - setText = self.setText - app = QApplication.instance() - setText(app.clipboard().text()) class ButtonsLineEdit(OverlayControlMixin, QLineEdit): From f0f320b119433152b01c469f0e270c9d0bb0709a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 15:52:10 +0100 Subject: [PATCH 0369/1143] qml: ElDialog titlebar click moves focus, hack for android to remove onscreen keyboard --- .../gui/qml/components/controls/ElDialog.qml | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index ed7865bc1..24f9cee17 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -29,42 +29,57 @@ Dialog { } } - header: ColumnLayout { - spacing: 0 + header: Item { + implicitWidth: rootLayout.implicitWidth + implicitHeight: rootLayout.implicitHeight - RowLayout { + MouseArea { + anchors.fill: parent + onClicked: { + // hack to allow titlebar click to remove on screen keyboard by + // moving focus to label + titleLabel.forceActiveFocus() + } + } + + ColumnLayout { + id: rootLayout spacing: 0 - Image { - visible: iconSource - source: iconSource - Layout.preferredWidth: constants.iconSizeXLarge - Layout.preferredHeight: constants.iconSizeXLarge - Layout.leftMargin: constants.paddingMedium - Layout.topMargin: constants.paddingMedium - Layout.bottomMargin: constants.paddingMedium + RowLayout { + spacing: 0 + + Image { + visible: iconSource + source: iconSource + Layout.preferredWidth: constants.iconSizeXLarge + Layout.preferredHeight: constants.iconSizeXLarge + Layout.leftMargin: constants.paddingMedium + Layout.topMargin: constants.paddingMedium + Layout.bottomMargin: constants.paddingMedium + } + + Label { + id: titleLabel + text: title + elide: Label.ElideRight + Layout.fillWidth: true + leftPadding: constants.paddingXLarge + topPadding: constants.paddingXLarge + bottomPadding: constants.paddingXLarge + rightPadding: constants.paddingXLarge + font.bold: true + font.pixelSize: constants.fontSizeMedium + } } - Label { - text: title - elide: Label.ElideRight + Rectangle { Layout.fillWidth: true - leftPadding: constants.paddingXLarge - topPadding: constants.paddingXLarge - bottomPadding: constants.paddingXLarge - rightPadding: constants.paddingXLarge - font.bold: true - font.pixelSize: constants.fontSizeMedium + Layout.leftMargin: constants.paddingXXSmall + Layout.rightMargin: constants.paddingXXSmall + height: 1 + color: Qt.rgba(0,0,0,0.5) } } - - Rectangle { - Layout.fillWidth: true - Layout.leftMargin: constants.paddingXXSmall - Layout.rightMargin: constants.paddingXXSmall - height: 1 - color: Qt.rgba(0,0,0,0.5) - } } - } From a6c40696178151c6fe57fb9507d8f51ad0ca41fe Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 14 Mar 2023 14:54:15 +0000 Subject: [PATCH 0370/1143] qt send_tab: allow saving bip70 payment requests probably got disabled in or around https://github.com/spesmilo/electrum/pull/7839 by accident --- electrum/gui/qt/send_tab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 4b3f9b46e..9146e0aed 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -347,7 +347,8 @@ def payment_request_ok(self): self.message_e.setText(pr.get_memo()) self.set_onchain(True) self.max_button.setEnabled(False) - for btn in [self.send_button, self.clear_button]: + # note: allow saving bip70 reqs, as we save them anyway when paying them + for btn in [self.send_button, self.clear_button, self.save_button]: btn.setEnabled(True) # signal to set fee self.amount_e.textEdited.emit("") From 0799560ae4ed2c41e8463ff2def6d44bc5e3de9d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 14 Mar 2023 16:36:13 +0100 Subject: [PATCH 0371/1143] qml: make fiat and btc states in BalanceSummary hopefully equally tall --- .../gui/qml/components/controls/BalanceSummary.qml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 363563844..e79631e66 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -40,6 +40,7 @@ Item { rightPadding: constants.paddingXLarge GridLayout { + id: balanceLayout columns: 3 opacity: Daemon.currentWallet.synchronizing ? 0 : 1 @@ -63,13 +64,15 @@ Item { Item { visible: Daemon.fx.enabled && root.state == 'fiat' - Layout.preferredHeight: 1 + // attempt at making fiat state as tall as btc state: + Layout.preferredHeight: fontMetrics.lineSpacing * 2 + balanceLayout.rowSpacing + 2 Layout.preferredWidth: 1 } Label { Layout.alignment: Qt.AlignRight visible: Daemon.fx.enabled && root.state == 'fiat' font.pixelSize: constants.fontSizeLarge + font.family: FixedFont color: constants.mutedForeground text: formattedTotalBalanceFiat } @@ -120,6 +123,7 @@ Item { } } Label { + id: formattedConfirmedBalanceLabel visible: root.state == 'btc' Layout.alignment: Qt.AlignRight text: formattedConfirmedBalance @@ -176,5 +180,10 @@ Item { } } + FontMetrics { + id: fontMetrics + font: formattedConfirmedBalanceLabel.font + } + Component.onCompleted: setBalances() } From d56162c588dccc353d164f6cc8da41ad4c0d8eec Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 14 Mar 2023 17:12:38 +0100 Subject: [PATCH 0372/1143] follow-up 842229c --- electrum/gui/qt/main_window.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e3cc703a3..ad6a8f74b 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -111,15 +111,14 @@ class StatusBarButton(QToolButton): # note: this class has a custom stylesheet applied in stylesheet_patcher.py - def __init__(self, icon, tooltip, func, size=0): + def __init__(self, icon, tooltip, func, sb_height): QToolButton.__init__(self) self.setText('') self.setIcon(icon) self.setToolTip(tooltip) self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.setAutoRaise(True) - if not size: - size = max(25, round(1.8 * font_height())) + size = max(25, round(0.9 * sb_height)) self.setMaximumWidth(size) self.clicked.connect(self.onPress) self.func = func @@ -1528,7 +1527,6 @@ def create_status_bar(self): font_height = QFontMetrics(self.balance_label.font()).height() sb_height = max(35, int(2 * font_height)) sb.setFixedHeight(sb_height) - sb_inner_height = sb.childrenRect().height() # remove border of all items in status bar self.setStyleSheet("QStatusBar::item { border: 0px;} ") @@ -1548,18 +1546,18 @@ def create_status_bar(self): self.tasks_label = QLabel('') sb.addPermanentWidget(self.tasks_label) - self.password_button = StatusBarButton(QIcon(), _("Password"), self.change_password_dialog, sb_inner_height) + self.password_button = StatusBarButton(QIcon(), _("Password"), self.change_password_dialog, sb_height) sb.addPermanentWidget(self.password_button) - sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog, sb_inner_height)) - self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog, sb_inner_height) + sb.addPermanentWidget(StatusBarButton(read_QIcon("preferences.png"), _("Preferences"), self.settings_dialog, sb_height)) + self.seed_button = StatusBarButton(read_QIcon("seed.png"), _("Seed"), self.show_seed_dialog, sb_height) sb.addPermanentWidget(self.seed_button) - self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog, sb_inner_height) + self.lightning_button = StatusBarButton(read_QIcon("lightning.png"), _("Lightning Network"), self.gui_object.show_lightning_dialog, sb_height) sb.addPermanentWidget(self.lightning_button) self.update_lightning_icon() self.status_button = None if self.network: - self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog, sb_inner_height) + self.status_button = StatusBarButton(read_QIcon("status_disconnected.png"), _("Network"), self.gui_object.show_network_dialog, sb_height) sb.addPermanentWidget(self.status_button) run_hook('create_status_bar', sb) self.setStatusBar(sb) From f770905551c7de793cef2ddd7deafdfb5d9e8156 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 14 Mar 2023 17:28:33 +0100 Subject: [PATCH 0373/1143] follow-up d56162c588dccc353d164f6cc8da41ad4c0d8eec --- electrum/plugins/hw_wallet/qt.py | 5 +++-- electrum/plugins/revealer/qt.py | 6 +++--- electrum/plugins/trustedcoin/qt.py | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index ab41be188..9448e88e9 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -220,10 +220,11 @@ def load_wallet(self: Union['QtPluginBase', HW_PluginBase], wallet: 'Abstract_Wa return tooltip = self.device + '\n' + (keystore.label or 'unnamed') cb = partial(self._on_status_bar_button_click, window=window, keystore=keystore) - button = StatusBarButton(read_QIcon(self.icon_unpaired), tooltip, cb) + sb = window.statusBar() + button = StatusBarButton(read_QIcon(self.icon_unpaired), tooltip, cb, sb.height()) button.icon_paired = self.icon_paired button.icon_unpaired = self.icon_unpaired - window.statusBar().addPermanentWidget(button) + sb.addPermanentWidget(button) handler = self.create_handler(window) handler.button = button keystore.handler = handler diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 52c975d6b..0edee7e55 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -61,10 +61,10 @@ def __init__(self, parent, config, name): self.extension = False @hook - def create_status_bar(self, parent): + def create_status_bar(self, sb): b = StatusBarButton(read_QIcon('revealer.png'), "Revealer "+_("Visual Cryptography Plugin"), - partial(self.setup_dialog, parent)) - parent.addPermanentWidget(b) + partial(self.setup_dialog, sb), sb.height()) + sb.addPermanentWidget(b) def requires_settings(self): return True diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index df8c7051c..038fcbfe4 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -103,8 +103,9 @@ def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'): else: action = partial(self.settings_dialog, window) icon = read_QIcon("trustedcoin-status.png") - button = StatusBarButton(icon, _("TrustedCoin"), action) - window.statusBar().addPermanentWidget(button) + sb = window.statusBar() + button = StatusBarButton(icon, _("TrustedCoin"), action, sb.height()) + sb.addPermanentWidget(button) self.start_request_thread(window.wallet) def auth_dialog(self, window): From e14ed717a82c70a7b35c682263f29525260aff08 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 14 Mar 2023 16:32:14 +0000 Subject: [PATCH 0374/1143] qml: fix paying bip70 invoices --- electrum/gui/qml/qeinvoice.py | 22 +++++++++++++++------- electrum/util.py | 8 +++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 7b8bec520..223cec4ee 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -1,4 +1,5 @@ import threading +from typing import TYPE_CHECKING import asyncio from urllib.parse import urlparse @@ -17,11 +18,13 @@ maybe_extract_lightning_payment_identifier) from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl from electrum.bitcoin import COIN +from electrum.paymentrequest import PaymentRequest from .qetypes import QEAmount from .qewallet import QEWallet from .util import status_update_timer_interval + class QEInvoice(QObject): class Type: Invalid = -1 @@ -131,6 +134,8 @@ class QEInvoiceParser(QEInvoice): lnurlRetrieved = pyqtSignal() lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) + _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['request']) + def __init__(self, parent=None): super().__init__(parent) @@ -144,6 +149,8 @@ def __init__(self, parent=None): self._timer.setSingleShot(True) self._timer.timeout.connect(self.updateStatusString) + self._bip70PrResolvedSignal.connect(self._bip70_payment_request_resolved) + self.clear() @pyqtProperty(int, notify=invoiceChanged) @@ -364,6 +371,13 @@ def create_onchain_invoice(self, outputs, message, payment_request, uri): URI=uri ) + def _bip70_payment_request_resolved(self, request: 'PaymentRequest'): + self._logger.debug('resolved payment request') + outputs = request.get_outputs() + invoice = self.create_onchain_invoice(outputs, None, request, None) + self.setValidOnchainInvoice(invoice) + self.validationSuccess.emit() + def validateRecipient(self, recipient): if not recipient: self.setInvoiceType(QEInvoice.Type.Invalid) @@ -371,14 +385,8 @@ def validateRecipient(self, recipient): maybe_lightning_invoice = recipient - def _payment_request_resolved(request): - self._logger.debug('resolved payment request') - outputs = request.get_outputs() - invoice = self.create_onchain_invoice(outputs, None, request, None) - self.setValidOnchainInvoice(invoice) - try: - self._bip21 = parse_URI(recipient, _payment_request_resolved) + self._bip21 = parse_URI(recipient, lambda pr: self._bip70PrResolvedSignal.emit(pr)) if self._bip21: if 'r' in self._bip21 or ('name' in self._bip21 and 'sig' in self._bip21): # TODO set flag in util? # let callback handle state diff --git a/electrum/util.py b/electrum/util.py index 3d1723f95..f15e0df2c 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -63,6 +63,7 @@ from .network import Network from .interface import Interface from .simple_config import SimpleConfig + from .paymentrequest import PaymentRequest _logger = get_logger(__name__) @@ -966,7 +967,12 @@ class InvalidBitcoinURI(Exception): pass # TODO rename to parse_bip21_uri or similar -def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: +def parse_URI( + uri: str, + on_pr: Callable[['PaymentRequest'], None] = None, + *, + loop: asyncio.AbstractEventLoop = None, +) -> dict: """Raises InvalidBitcoinURI on malformed URI.""" from . import bitcoin from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC From 02a2f02d024678bc6aea1b127284de85684c8ef9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 14 Mar 2023 17:04:39 +0000 Subject: [PATCH 0375/1143] qml: actually do the x509 validation for bip70 as in other GUIs --- electrum/gui/qml/qeinvoice.py | 23 +++++++++++++++-------- electrum/paymentrequest.py | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 223cec4ee..039151b24 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -1,5 +1,5 @@ import threading -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import asyncio from urllib.parse import urlparse @@ -50,7 +50,7 @@ class Status: def __init__(self, parent=None): super().__init__(parent) - self._wallet = None + self._wallet = None # type: Optional[QEWallet] self._canSave = False self._canPay = False self._key = None @@ -134,7 +134,7 @@ class QEInvoiceParser(QEInvoice): lnurlRetrieved = pyqtSignal() lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) - _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['request']) + _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['pr']) def __init__(self, parent=None): super().__init__(parent) @@ -371,12 +371,19 @@ def create_onchain_invoice(self, outputs, message, payment_request, uri): URI=uri ) - def _bip70_payment_request_resolved(self, request: 'PaymentRequest'): + def _bip70_payment_request_resolved(self, pr: 'PaymentRequest'): self._logger.debug('resolved payment request') - outputs = request.get_outputs() - invoice = self.create_onchain_invoice(outputs, None, request, None) - self.setValidOnchainInvoice(invoice) - self.validationSuccess.emit() + if pr.verify(self._wallet.wallet.contacts): + invoice = Invoice.from_bip70_payreq(pr, height=0) + if self._wallet.wallet.get_invoice_status(invoice) == PR_PAID: + self.validationError.emit('unknown', _('Invoice already paid')) + elif pr.has_expired(): + self.validationError.emit('unknown', _('Payment request has expired')) + else: + self.setValidOnchainInvoice(invoice) + self.validationSuccess.emit() + else: + self.validationError.emit('unknown', f"invoice error:\n{pr.error}") def validateRecipient(self, recipient): if not recipient: diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 554b71327..afc876e20 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -152,6 +152,7 @@ def parse(self, r: bytes): self.payment_url = self.details.payment_url def verify(self, contacts): + # FIXME: we should enforce that this method was called before we attempt payment if self.error: return False if not self.raw: From d166fa886e57a9f1bea97524da724b90f5b522c4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 14 Mar 2023 17:21:33 +0000 Subject: [PATCH 0376/1143] qt: fix paying to openalias Probably got broken in https://github.com/spesmilo/electrum/pull/7839 , which got released in 4.3.0, ~7 months ago. As no one complained, this really again raises the question of removing openalias... related https://github.com/spesmilo/electrum/issues/6232 --- electrum/gui/qt/paytoedit.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index fc1cd7916..c0796f4f3 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -268,6 +268,7 @@ def _check_text(self, text, *, full_check: bool): if full_check: self.previous_payto = str(text).strip() self.errors = [] + errors = [] if self.disable_checks: return # filter out empty lines @@ -301,7 +302,7 @@ def _check_text(self, text, *, full_check: bool): try: self.payto_scriptpubkey = self.parse_output(data) except Exception as e: - self.errors.append(PayToLineError(line_content=data, exc=e)) + errors.append(PayToLineError(line_content=data, exc=e)) else: self.send_tab.set_onchain(True) self.send_tab.lock_amount(False) @@ -312,6 +313,9 @@ def _check_text(self, text, *, full_check: bool): if oa_data: self._set_openalias(key=data, data=oa_data) return + # all parsing attempts failed, so now expose the errors: + if errors: + self.errors = errors else: # there are multiple lines self._parse_as_multiline(lines, raise_errors=False) @@ -389,7 +393,7 @@ def _set_openalias(self, *, key: str, data: dict) -> bool: address = data.get('address') name = data.get('name') new_url = key + ' <' + address + '>' - self.setTextNoCheck(new_url) + self.setText(new_url) #if self.win.config.get('openalias_autoadd') == 'checked': self.win.contacts[key] = ('openalias', name) From 660a8ebc7f16af5e7f7cb28d54eb5213c7c289fb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 09:16:25 +0100 Subject: [PATCH 0377/1143] Qt: let user edit tx label from tx_dialog This allows users to edit labels from the utxo_dialog, without having to search for the transaction in history. Also, remove block hash from tx dialog: not very useful, and available through block explorers. (the situation where this could be useful is case of a chain fork, but in that case the tx might be mined in both branches of the fork, and we would want to know that). --- electrum/gui/qt/main_window.py | 1 + electrum/gui/qt/transaction_dialog.py | 29 ++++++++------- electrum/gui/qt/utxo_dialog.py | 51 +++++++++++++++------------ 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index ad6a8f74b..af381fb5f 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -164,6 +164,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): computing_privkeys_signal = pyqtSignal() show_privkeys_signal = pyqtSignal() show_error_signal = pyqtSignal(str) + labels_changed_signal = pyqtSignal() def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): QMainWindow.__init__(self) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 05aef8a46..b70ccdfee 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -419,8 +419,20 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav vbox.addLayout(toolbar) vbox.addWidget(QLabel(_("Transaction ID:"))) - self.tx_hash_e = ShowQRLineEdit('', self.config, title='Transaction ID') + self.tx_hash_e = ShowQRLineEdit('', self.config, title=_('Transaction ID')) vbox.addWidget(self.tx_hash_e) + self.tx_desc_label = QLabel(_("Description:")) + vbox.addWidget(self.tx_desc_label) + self.tx_desc = ButtonsLineEdit('') + def on_edited(): + text = self.tx_desc.text() + if self.wallet.set_label(txid, text): + self.main_window.history_list.update() + self.main_window.utxo_list.update() + self.main_window.labels_changed_signal.emit() + self.tx_desc.editingFinished.connect(on_edited) + self.tx_desc.addCopyButton() + vbox.addWidget(self.tx_desc) self.add_tx_stats(vbox) @@ -733,11 +745,13 @@ def update(self): # note: when not finalized, RBF and locktime changes do not trigger # a make_tx, so the txid is unreliable, hence: self.tx_hash_e.setText(_('Unknown')) - if not desc: + if not self.wallet.adb.get_transaction(txid): self.tx_desc.hide() + self.tx_desc_label.hide() else: - self.tx_desc.setText(_("Description") + ': ' + desc) + self.tx_desc.setText(desc) self.tx_desc.show() + self.tx_desc_label.show() self.status_label.setText(_('Status:') + ' ' + tx_details.status) if tx_mined_status.timestamp: @@ -761,12 +775,9 @@ def update(self): self.rbf_label.setText(_('Replace by fee') + f": {not self.tx.is_final()}") if tx_mined_status.header_hash: - self.block_hash_label.setText(_("Included in block: {}") - .format(tx_mined_status.header_hash)) self.block_height_label.setText(_("At block height: {}") .format(tx_mined_status.height)) else: - self.block_hash_label.hide() self.block_height_label.hide() if amount is None and ln_amount is None: amount_str = _("Transaction unrelated to your wallet") @@ -860,8 +871,6 @@ def add_tx_stats(self, vbox): # left column vbox_left = QVBoxLayout() - self.tx_desc = TxDetailLabel(word_wrap=True) - vbox_left.addWidget(self.tx_desc) self.status_label = TxDetailLabel() vbox_left.addWidget(self.status_label) self.date_label = TxDetailLabel() @@ -911,10 +920,6 @@ def add_tx_stats(self, vbox): vbox.addWidget(hbox_stats_w) - # below columns - self.block_hash_label = TxDetailLabel(word_wrap=True) - vbox.addWidget(self.block_hash_label) - # set visibility after parenting can be determined by Qt self.rbf_label.setVisible(True) self.locktime_final_label.setVisible(True) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 760cc100e..8d2d44273 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -41,8 +41,6 @@ from electrum.transaction import PartialTxInput from .main_window import ElectrumWindow -# todo: -# - edit label in tx detail window class UTXODialog(WindowModalDialog): @@ -54,11 +52,6 @@ def __init__(self, window: 'ElectrumWindow', utxo: 'PartialTxInput'): self.wallet = window.wallet self.utxo = utxo - txid = self.utxo.prevout.txid.hex() - parents = self.wallet.get_tx_parents(txid) - num_parents = len(parents) - parents_copy = copy.deepcopy(parents) - self.parents_list = QTextBrowserWithDefaultSize(800, 400) self.parents_list.setOpenLinks(False) # disable automatic link opening self.parents_list.anchorClicked.connect(self.open_tx) # send links to our handler @@ -70,6 +63,29 @@ def __init__(self, window: 'ElectrumWindow', utxo: 'PartialTxInput'): self.txo_color_uncle = TxOutputColoring( legend=_("Address reuse"), color=ColorScheme.RED, tooltip=_("Address reuse")) + vbox = QVBoxLayout() + vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) + vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) + self.stats_label = WWLabel() + vbox.addWidget(self.stats_label) + vbox.addWidget(self.parents_list) + legend_hbox = QHBoxLayout() + legend_hbox.setContentsMargins(0, 0, 0, 0) + legend_hbox.addStretch(2) + legend_hbox.addWidget(self.txo_color_parent.legend_label) + legend_hbox.addWidget(self.txo_color_uncle.legend_label) + vbox.addLayout(legend_hbox) + vbox.addLayout(Buttons(CloseButton(self))) + self.setLayout(vbox) + self.update() + self.main_window.labels_changed_signal.connect(self.update) + + def update(self): + + txid = self.utxo.prevout.txid.hex() + parents = self.wallet.get_tx_parents(txid) + num_parents = len(parents) + parents_copy = copy.deepcopy(parents) cursor = self.parents_list.textCursor() ext = QTextCharFormat() @@ -84,6 +100,10 @@ def __init__(self, window: 'ElectrumWindow', utxo: 'PartialTxInput'): ASCII_PIPE = '│' ASCII_SPACE = ' ' + # set cursor to top + cursor.setPosition(0) + self.parents_list.clear() + self.parents_list.setTextCursor(cursor) self.num_reuse = 0 def print_ascii_tree(_txid, prefix, is_last, is_uncle): if _txid not in parents: @@ -118,27 +138,12 @@ def print_ascii_tree(_txid, prefix, is_last, is_uncle): # recursively build the tree print_ascii_tree(txid, '', False, False) - vbox = QVBoxLayout() - vbox.addWidget(QLabel(_("Output point") + ": " + str(self.utxo.short_id))) - vbox.addWidget(QLabel(_("Amount") + ": " + self.main_window.format_amount_and_units(self.utxo.value_sats()))) msg = _("This UTXO has {} parent transactions in your wallet.").format(num_parents) if self.num_reuse: msg += '\n' + _('This does not include transactions that are downstream of address reuse.') - vbox.addWidget(WWLabel(msg)) - vbox.addWidget(self.parents_list) - legend_hbox = QHBoxLayout() - legend_hbox.setContentsMargins(0, 0, 0, 0) - legend_hbox.addStretch(2) - legend_hbox.addWidget(self.txo_color_parent.legend_label) - legend_hbox.addWidget(self.txo_color_uncle.legend_label) - vbox.addLayout(legend_hbox) + self.stats_label.setText(msg) self.txo_color_parent.legend_label.setVisible(True) self.txo_color_uncle.legend_label.setVisible(bool(self.num_reuse)) - vbox.addLayout(Buttons(CloseButton(self))) - self.setLayout(vbox) - # set cursor to top - cursor.setPosition(0) - self.parents_list.setTextCursor(cursor) def open_tx(self, txid): if isinstance(txid, QUrl): From 0bda808b29a84b56b3ea0d8d21072b0eca8aa33a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 10:35:37 +0100 Subject: [PATCH 0378/1143] Qt lists: always show item detail on double click No longer enter edit mode for editable columns. (that behaviour was difficult to learn, because it is not explicit which columns are editable) --- electrum/gui/qt/address_list.py | 4 ++++ electrum/gui/qt/channels_list.py | 5 +++++ electrum/gui/qt/history_list.py | 30 +++++++++++------------------- electrum/gui/qt/invoice_list.py | 16 ++++++++++++---- electrum/gui/qt/util.py | 15 ++++++++++++++- electrum/gui/qt/utxo_list.py | 5 +++++ 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 69868a4a0..c3ad540c4 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -115,6 +115,10 @@ def __init__(self, main_window: 'ElectrumWindow'): self.update() self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder) + def on_double_click(self, idx): + addr = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR) + self.main_window.show_address(addr) + def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') self.num_addr_label = toolbar.itemAt(0).widget() diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index fe4f90708..5ec8297b8 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -217,6 +217,11 @@ def on_rebalance(self): return self.main_window.rebalance_dialog(chan1, chan2) + def on_double_click(self, idx): + channel_id = idx.sibling(idx.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID) + chan = self.lnworker.get_channel_by_id(channel_id) or self.lnworker.channel_backups[channel_id] + self.main_window.show_channel_details(chan) + def create_menu(self, position): menu = QMenu() menu.setSeparatorsCollapsible(True) # consecutive separators are merged together diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index dd4a76056..feaf7d077 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -33,7 +33,7 @@ import enum from decimal import Decimal -from PyQt5.QtGui import QMouseEvent, QFont, QBrush, QColor +from PyQt5.QtGui import QFont, QBrush, QColor from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, QAbstractItemModel, QSortFilterProxyModel, QVariant, QItemSelectionModel, QDate, QPoint) from PyQt5.QtWidgets import (QMenu, QHeaderView, QLabel, QMessageBox, @@ -708,25 +708,17 @@ def on_edited(self, idx, edit_key, *, text): else: assert False - def mouseDoubleClickEvent(self, event: QMouseEvent): - org_idx: QModelIndex = self.indexAt(event.pos()) - idx = self.proxy.mapToSource(org_idx) - if not idx.isValid(): - # can happen e.g. before list is populated for the first time - return + def on_double_click(self, idx): tx_item = idx.internalPointer().get_data() - if self.hm.flags(idx) & Qt.ItemIsEditable: - super().mouseDoubleClickEvent(event) - else: - if tx_item.get('lightning'): - if tx_item['type'] == 'payment': - self.main_window.show_lightning_transaction(tx_item) - return - tx_hash = tx_item['txid'] - tx = self.wallet.adb.get_transaction(tx_hash) - if not tx: - return - self.main_window.show_transaction(tx) + if tx_item.get('lightning'): + if tx_item['type'] == 'payment': + self.main_window.show_lightning_transaction(tx_item) + return + tx_hash = tx_item['txid'] + tx = self.wallet.adb.get_transaction(tx_hash) + if not tx: + return + self.main_window.show_transaction(tx) def add_copy_menu(self, menu, idx): cc = menu.addMenu(_("Copy")) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 0ef2b8095..ba16f4470 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -82,6 +82,10 @@ def __init__(self, send_tab: 'SendTab'): self.setSortingEnabled(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) + def on_double_click(self, idx): + key = idx.sibling(idx.row(), self.Columns.DATE).data(ROLE_REQUEST_ID) + self.show_invoice(key) + def refresh_row(self, key, row): assert row is not None invoice = self.wallet.get_invoice(key) @@ -133,6 +137,13 @@ def update(self): self.sortByColumn(self.Columns.DATE, Qt.DescendingOrder) self.hide_if_empty() + def show_invoice(self, key): + invoice = self.wallet.get_invoice(key) + if invoice.is_lightning(): + self.main_window.show_lightning_invoice(invoice) + else: + self.main_window.show_onchain_invoice(invoice) + def hide_if_empty(self): b = self.std_model.rowCount() > 0 self.setVisible(b) @@ -163,10 +174,7 @@ def create_menu(self, position): address = invoice.get_address() if address: copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Address')) - if invoice.is_lightning(): - menu.addAction(_("Details"), lambda: self.main_window.show_lightning_invoice(invoice)) - else: - menu.addAction(_("Details"), lambda: self.main_window.show_onchain_invoice(invoice)) + menu.addAction(_("Details"), lambda: self.show_invoice(key)) status = wallet.get_invoice_status(invoice) if status == PR_UNPAID: menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 0ed206342..3985da79e 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -15,7 +15,7 @@ from PyQt5 import QtWidgets, QtCore from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, QImage, - QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent) + QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent, QMouseEvent) from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal, QCoreApplication, QItemSelectionModel, QThread, QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel, @@ -665,6 +665,7 @@ def __init__( self._forced_update = False self._default_bg_brush = QStandardItem().background() + self.proxy = None # history, and address tabs use a proxy def create_menu(self, position: QPoint) -> None: pass @@ -724,6 +725,18 @@ def keyPressEvent(self, event): return super().keyPressEvent(event) + def mouseDoubleClickEvent(self, event: QMouseEvent): + idx: QModelIndex = self.indexAt(event.pos()) + if self.proxy: + idx = self.proxy.mapToSource(idx) + if not idx.isValid(): + # can happen e.g. before list is populated for the first time + return + self.on_double_click(idx) + + def on_double_click(self, idx): + pass + def on_activated(self, idx): # on 'enter' we show the menu pt = self.visualRect(idx).bottomLeft() diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index e4361394b..b262efe7d 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -263,6 +263,11 @@ def pay_to_clipboard_address(self, coins): self.main_window.send_tab.pay_onchain_dialog(outputs) self.clear_coincontrol() + def on_double_click(self, idx): + outpoint = idx.sibling(idx.row(), self.Columns.OUTPOINT).data(self.ROLE_PREVOUT_STR) + utxo = self._utxo_dict[outpoint] + self.main_window.show_utxo(utxo) + def create_menu(self, position): selected = self.get_selected_outpoints() menu = QMenu() From 20e93af70c1aa9fb6e9bc64e8a2ad73733e8ffc6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 10:48:03 +0100 Subject: [PATCH 0379/1143] lightning_tx_dialog: add editable tx description --- electrum/gui/qt/lightning_tx_dialog.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/lightning_tx_dialog.py b/electrum/gui/qt/lightning_tx_dialog.py index 34a9ffacf..845aa1d4a 100644 --- a/electrum/gui/qt/lightning_tx_dialog.py +++ b/electrum/gui/qt/lightning_tx_dialog.py @@ -34,7 +34,7 @@ from electrum.lnworker import PaymentDirection from electrum.invoices import Invoice -from .util import WindowModalDialog, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, font_height +from .util import WindowModalDialog, ShowQRLineEdit, ColorScheme, Buttons, CloseButton, font_height, ButtonsLineEdit from .qrtextedit import ShowQRTextEdit if TYPE_CHECKING: @@ -46,7 +46,7 @@ class LightningTxDialog(WindowModalDialog): def __init__(self, parent: 'ElectrumWindow', tx_item: dict): WindowModalDialog.__init__(self, parent, _("Lightning Payment")) - self.parent = parent + self.main_window = parent self.config = parent.config self.is_sent = tx_item['direction'] == PaymentDirection.SENT self.label = tx_item['label'] @@ -55,22 +55,34 @@ def __init__(self, parent: 'ElectrumWindow', tx_item: dict): self.payment_hash = tx_item['payment_hash'] self.preimage = tx_item['preimage'] self.invoice = "" - invoice = self.parent.wallet.get_invoice(self.payment_hash) # only check outgoing invoices + invoice = self.main_window.wallet.get_invoice(self.payment_hash) # only check outgoing invoices if invoice: assert invoice.is_lightning(), f"{self.invoice!r}" self.invoice = invoice.lightning_invoice self.setMinimumWidth(700) vbox = QVBoxLayout() self.setLayout(vbox) - amount_str = self.parent.format_amount_and_units(self.amount, timestamp=self.timestamp) + amount_str = self.main_window.format_amount_and_units(self.amount, timestamp=self.timestamp) vbox.addWidget(QLabel(_("Amount") + f": {amount_str}")) if self.is_sent: fee_msat = tx_item['fee_msat'] fee_sat = Decimal(fee_msat) / 1000 if fee_msat is not None else None - fee_str = self.parent.format_amount_and_units(fee_sat, timestamp=self.timestamp) + fee_str = self.main_window.format_amount_and_units(fee_sat, timestamp=self.timestamp) vbox.addWidget(QLabel(_("Fee") + f": {fee_str}")) time_str = datetime.datetime.fromtimestamp(self.timestamp).isoformat(' ')[:-3] vbox.addWidget(QLabel(_("Date") + ": " + time_str)) + self.tx_desc_label = QLabel(_("Description:")) + vbox.addWidget(self.tx_desc_label) + self.tx_desc = ButtonsLineEdit(self.label) + def on_edited(): + text = self.tx_desc.text() + if self.main_window.wallet.set_label(self.payment_hash, text): + self.main_window.history_list.update() + self.main_window.utxo_list.update() + self.main_window.labels_changed_signal.emit() + self.tx_desc.editingFinished.connect(on_edited) + self.tx_desc.addCopyButton() + vbox.addWidget(self.tx_desc) vbox.addWidget(QLabel(_("Payment hash") + ":")) self.hash_e = ShowQRLineEdit(self.payment_hash, self.config, title=_("Payment hash")) vbox.addWidget(self.hash_e) @@ -83,4 +95,6 @@ def __init__(self, parent: 'ElectrumWindow', tx_item: dict): self.invoice_e.setMaximumHeight(max(150, 10 * font_height())) self.invoice_e.addCopyButton() vbox.addWidget(self.invoice_e) - vbox.addLayout(Buttons(CloseButton(self))) + self.close_button = CloseButton(self) + vbox.addLayout(Buttons(self.close_button)) + self.close_button.setFocus() From 2db0bc9f7334c911b70c5c1d3784a8a84ea76c32 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 11:00:27 +0100 Subject: [PATCH 0380/1143] tx in/out details: rewording 'receiving address' in the confirm tx dialog, 'receiving' could lead users to believe that the funds are going to be sent to this address. --- electrum/gui/qt/transaction_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index b70ccdfee..feb47d8a5 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -117,7 +117,7 @@ def __init__(self, main_window: 'ElectrumWindow', wallet: 'Abstract_Wallet'): self.inheader_hbox.addWidget(self.inputs_header) self.txo_color_recv = TxOutputColoring( - legend=_("Receiving Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receive address")) + legend=_("Wallet Address"), color=ColorScheme.GREEN, tooltip=_("Wallet receiving address")) self.txo_color_change = TxOutputColoring( legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address")) self.txo_color_2fa = TxOutputColoring( From 107a6f9080ce03003cc16379cf5770ac9406e824 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 11:11:11 +0100 Subject: [PATCH 0381/1143] utxo_dialog: fix minor regression (set cursor to top after drawing) --- electrum/gui/qt/utxo_dialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index 8d2d44273..c714e41fc 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -100,10 +100,7 @@ def update(self): ASCII_PIPE = '│' ASCII_SPACE = ' ' - # set cursor to top - cursor.setPosition(0) self.parents_list.clear() - self.parents_list.setTextCursor(cursor) self.num_reuse = 0 def print_ascii_tree(_txid, prefix, is_last, is_uncle): if _txid not in parents: @@ -144,6 +141,9 @@ def print_ascii_tree(_txid, prefix, is_last, is_uncle): self.stats_label.setText(msg) self.txo_color_parent.legend_label.setVisible(True) self.txo_color_uncle.legend_label.setVisible(bool(self.num_reuse)) + # set cursor to top + cursor.setPosition(0) + self.parents_list.setTextCursor(cursor) def open_tx(self, txid): if isinstance(txid, QUrl): From b431d39a8efb9355bf7709fb9a5bff66c323ab08 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 11:18:19 +0100 Subject: [PATCH 0382/1143] Qt lists: consistently show Details as first item in the contextual menu --- electrum/gui/qt/address_list.py | 2 +- electrum/gui/qt/channels_list.py | 2 +- electrum/gui/qt/history_list.py | 4 ++-- electrum/gui/qt/invoice_list.py | 2 +- electrum/gui/qt/utxo_list.py | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index c3ad540c4..9251faec1 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -282,10 +282,10 @@ def create_menu(self, position): if not item: return addr = addrs[0] + menu.addAction(_('Details'), lambda: self.main_window.show_address(addr)) addr_column_title = self.std_model.horizontalHeaderItem(self.Columns.LABEL).text() addr_idx = idx.sibling(idx.row(), self.Columns.LABEL) self.add_copy_menu(menu, idx) - menu.addAction(_('Details'), lambda: self.main_window.show_address(addr)) persistent = QPersistentModelIndex(addr_idx) menu.addAction(_("Edit {}").format(addr_column_title), lambda p=persistent: self.edit(QModelIndex(p))) #menu.addAction(_("Request payment"), lambda: self.main_window.receive_at(addr)) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 5ec8297b8..c47b16b67 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -245,7 +245,7 @@ def create_menu(self, position): return channel_id = idx.sibling(idx.row(), self.Columns.NODE_ALIAS).data(ROLE_CHANNEL_ID) chan = self.lnworker.get_channel_by_id(channel_id) or self.lnworker.channel_backups[channel_id] - menu.addAction(_("Details..."), lambda: self.main_window.show_channel_details(chan)) + menu.addAction(_("Details"), lambda: self.main_window.show_channel_details(chan)) menu.addSeparator() cc = self.add_copy_menu(menu, idx) cc.addAction(_("Node ID"), lambda: self.place_text_on_clipboard( diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index feaf7d077..d4fafe3b2 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -743,7 +743,7 @@ def create_menu(self, position: QPoint): tx_item = idx.internalPointer().get_data() if tx_item.get('lightning') and tx_item['type'] == 'payment': menu = QMenu() - menu.addAction(_("View Payment"), lambda: self.main_window.show_lightning_transaction(tx_item)) + menu.addAction(_("Details"), lambda: self.main_window.show_lightning_transaction(tx_item)) cc = self.add_copy_menu(menu, idx) cc.addAction(_("Payment Hash"), lambda: self.place_text_on_clipboard(tx_item['payment_hash'], title="Payment Hash")) cc.addAction(_("Preimage"), lambda: self.place_text_on_clipboard(tx_item['preimage'], title="Preimage")) @@ -761,6 +761,7 @@ def create_menu(self, position: QPoint): tx_details = self.wallet.get_tx_info(tx) is_unconfirmed = tx_details.tx_mined_status.height <= 0 menu = QMenu() + menu.addAction(_("Details"), lambda: self.main_window.show_transaction(tx)) if tx_details.can_remove: menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) copy_menu = self.add_copy_menu(menu, idx) @@ -772,7 +773,6 @@ def create_menu(self, position: QPoint): # TODO use siblingAtColumn when min Qt version is >=5.11 persistent = QPersistentModelIndex(org_idx.sibling(org_idx.row(), c)) menu_edit.addAction(_("{}").format(label), lambda p=persistent: self.edit(QModelIndex(p))) - menu.addAction(_("View Transaction"), lambda: self.main_window.show_transaction(tx)) channel_id = tx_item.get('channel_id') if channel_id and self.wallet.lnworker and (chan := self.wallet.lnworker.get_channel_by_id(bytes.fromhex(channel_id))): menu.addAction(_("View Channel"), lambda: self.main_window.show_channel_details(chan)) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index ba16f4470..9cc820b1b 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -170,11 +170,11 @@ def create_menu(self, position): key = item_col0.data(ROLE_REQUEST_ID) invoice = self.wallet.get_invoice(key) menu = QMenu(self) + menu.addAction(_("Details"), lambda: self.show_invoice(key)) copy_menu = self.add_copy_menu(menu, idx) address = invoice.get_address() if address: copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Address')) - menu.addAction(_("Details"), lambda: self.show_invoice(key)) status = wallet.get_invoice_status(invoice) if status == PR_UNPAID: menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index b262efe7d..a28b3ee82 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -281,13 +281,13 @@ def create_menu(self, position): return utxo = coins[0] txid = utxo.prevout.txid.hex() - cc = self.add_copy_menu(menu, idx) - cc.addAction(_("Long Output point"), lambda: self.place_text_on_clipboard(utxo.prevout.to_str(), title="Long Output point")) # "Details" tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) - menu.addAction(_("Privacy analysis"), lambda: self.main_window.show_utxo(utxo)) + menu.addAction(_("Details"), lambda: self.main_window.show_utxo(utxo)) + cc = self.add_copy_menu(menu, idx) + cc.addAction(_("Long Output point"), lambda: self.place_text_on_clipboard(utxo.prevout.to_str(), title="Long Output point")) # fully spend menu_spend = menu.addMenu(_("Fully spend") + '…') m = menu_spend.addAction(_("send to address in clipboard"), lambda: self.pay_to_clipboard_address(coins)) From 33a84f6be5c0e1ef80ebfd29a392992089ee3852 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 11:27:05 +0100 Subject: [PATCH 0383/1143] view menu: use checkable actions --- electrum/gui/qt/main_window.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index af381fb5f..4954f6260 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -341,8 +341,6 @@ def on_fx_quotes(self): def toggle_tab(self, tab): show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) self.config.set_key('show_{}_tab'.format(tab.tab_name), show) - item_text = (_("Hide {}") if show else _("Show {}")).format(tab.tab_description) - tab.menu_action.setText(item_text) if show: # Find out where to place the tab index = len(self.tabs) @@ -701,8 +699,9 @@ def init_menubar(self): def add_toggle_action(view_menu, tab): is_shown = self.config.get('show_{}_tab'.format(tab.tab_name), False) - item_name = (_("Hide {}") if is_shown else _("Show {}")).format(tab.tab_description) - tab.menu_action = view_menu.addAction(item_name, lambda: self.toggle_tab(tab)) + tab.menu_action = view_menu.addAction(tab.tab_description, lambda: self.toggle_tab(tab)) + tab.menu_action.setCheckable(True) + tab.menu_action.setChecked(is_shown) view_menu = menubar.addMenu(_("&View")) add_toggle_action(view_menu, self.addresses_tab) From 42a63643f35f62b2cea32f03d754c866b8250839 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 12:20:22 +0100 Subject: [PATCH 0384/1143] receive tab: move expiry to toolbar --- electrum/gui/qt/main_window.py | 10 +++--- electrum/gui/qt/receive_tab.py | 62 +++++++++++++++------------------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 4954f6260..608dfa7b4 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1291,14 +1291,16 @@ def on_open_channel_success(self, args): else: self.show_message(message) - def query_choice(self, msg, choices): + def query_choice(self, msg, choices, title=_('Question'), default_choice=None): # Needed by QtHandler for hardware wallets - dialog = WindowModalDialog(self.top_level_window(), title='Question') + dialog = WindowModalDialog(self.top_level_window(), title=title) dialog.setMinimumWidth(400) - clayout = ChoicesLayout(msg, choices) + clayout = ChoicesLayout(msg, choices, checked_index=default_choice) vbox = QVBoxLayout(dialog) vbox.addLayout(clayout.layout()) - vbox.addLayout(Buttons(CancelButton(dialog), OkButton(dialog))) + cancel_button = CancelButton(dialog) + vbox.addLayout(Buttons(cancel_button, OkButton(dialog))) + cancel_button.setFocus() if not dialog.exec_(): return None return clayout.selected_index() diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index f45d8d8fb..24f66ee87 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -58,38 +58,6 @@ def __init__(self, window: 'ElectrumWindow'): self.window.connect_fields(self.receive_amount_e, self.fiat_receive_e) - self.expires_combo = QComboBox() - evl = sorted(pr_expiration_values.items()) - evl_keys = [i[0] for i in evl] - evl_values = [i[1] for i in evl] - default_expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - try: - i = evl_keys.index(default_expiry) - except ValueError: - i = 0 - self.expires_combo.addItems(evl_values) - self.expires_combo.setCurrentIndex(i) - def on_expiry(i): - self.config.set_key('request_expiry', evl_keys[i]) - self.expires_combo.currentIndexChanged.connect(on_expiry) - msg = ''.join([ - _('Expiration date of your request.'), ' ', - _('This information is seen by the recipient if you send them a signed payment request.'), - '\n\n', - _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ', - _('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ', - _('You can reuse a bitcoin address any number of times but it is not good for your privacy.'), - '\n\n', - _('For Lightning requests, payments will not be accepted after the expiration.'), - ]) - grid.addWidget(HelpLabel(_('Expires after') + ' (?)', msg), 2, 0) - grid.addWidget(self.expires_combo, 2, 1) - self.expires_label = QLineEdit('') - self.expires_label.setReadOnly(1) - self.expires_label.setFocusPolicy(Qt.NoFocus) - self.expires_label.hide() - grid.addWidget(self.expires_label, 2, 1) - self.clear_invoice_button = QPushButton(_('Clear')) self.clear_invoice_button.clicked.connect(self.do_clear) self.create_invoice_button = QPushButton(_('Create Request')) @@ -185,6 +153,10 @@ def on_receive_swap(): self.qr_menu_action = menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) + + self.expiry_button = QPushButton('exp') + self.expiry_button.clicked.connect(self.expiry_dialog) + self.toolbar.insertWidget(2, self.expiry_button) # layout vbox_g = QVBoxLayout() vbox_g.addLayout(grid) @@ -204,6 +176,30 @@ def on_receive_swap(): vbox.setStretchFactor(hbox, 40) vbox.setStretchFactor(self.request_list, 60) self.request_list.update() # after parented and put into a layout, can update without flickering + self.update_expiry_text() + + def update_expiry_text(self): + expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + text = _('Expiry') + ': ' + pr_expiration_values[expiry] + self.expiry_button.setText(text) + + def expiry_dialog(self): + msg = ''.join([ + _('Expiration date of your request.'), ' ', + _('This information is seen by the recipient if you send them a signed payment request.'), + '\n\n', + _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ', + _('The bitcoin address never expires and will always be part of this electrum wallet.'), ' ', + _('You can reuse a bitcoin address any number of times but it is not good for your privacy.'), + '\n\n', + _('For Lightning requests, payments will not be accepted after the expiration.'), + ]) + expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + v = self.window.query_choice(msg, pr_expiration_values, title=_('Expiration date'), default_choice=expiry) + if v is None: + return + self.config.set_key('request_expiry', v) + self.update_expiry_text() def on_toggle_bolt11_fallback(self): if not self.wallet.lnworker: @@ -356,8 +352,6 @@ def do_clear(self): self.receive_tabs.setVisible(False) self.receive_message_e.setText('') self.receive_amount_e.setAmount(None) - self.expires_label.hide() - self.expires_combo.show() self.request_list.clearSelection() def update_textedit_warning(self, *, text_e: ButtonsTextEdit, warning_text: Optional[str]): From 5750c8954dc3f44e4f4dea44e07f7dbb84b40656 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 12:28:11 +0100 Subject: [PATCH 0385/1143] receive tab: move back Expiry to its previous location, but use dialog instead of ComboBox. The toolbar location is not good, because it can be perceived as being about the request currently displayed. --- electrum/gui/qt/receive_tab.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 24f66ee87..f304acec2 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -58,6 +58,11 @@ def __init__(self, window: 'ElectrumWindow'): self.window.connect_fields(self.receive_amount_e, self.fiat_receive_e) + self.expiry_button = QPushButton('') + self.expiry_button.clicked.connect(self.expiry_dialog) + grid.addWidget(QLabel(_('Expiry')), 2, 0) + grid.addWidget(self.expiry_button, 2, 1) + self.clear_invoice_button = QPushButton(_('Clear')) self.clear_invoice_button.clicked.connect(self.do_clear) self.create_invoice_button = QPushButton(_('Create Request')) @@ -154,9 +159,6 @@ def on_receive_swap(): menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) - self.expiry_button = QPushButton('exp') - self.expiry_button.clicked.connect(self.expiry_dialog) - self.toolbar.insertWidget(2, self.expiry_button) # layout vbox_g = QVBoxLayout() vbox_g.addLayout(grid) @@ -180,7 +182,7 @@ def on_receive_swap(): def update_expiry_text(self): expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - text = _('Expiry') + ': ' + pr_expiration_values[expiry] + text = pr_expiration_values[expiry] self.expiry_button.setText(text) def expiry_dialog(self): From abc8d1550e57ece86ab44ee6b13e3ec0ba2768e4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 12:35:58 +0100 Subject: [PATCH 0386/1143] Expiry: the setting is a period, not a date --- electrum/gui/qt/receive_tab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index f304acec2..242f0a6d0 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -187,7 +187,7 @@ def update_expiry_text(self): def expiry_dialog(self): msg = ''.join([ - _('Expiration date of your request.'), ' ', + _('Expiration period of your request.'), ' ', _('This information is seen by the recipient if you send them a signed payment request.'), '\n\n', _('For on-chain requests, the address gets reserved until expiration. After that, it might get reused.'), ' ', @@ -197,7 +197,7 @@ def expiry_dialog(self): _('For Lightning requests, payments will not be accepted after the expiration.'), ]) expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - v = self.window.query_choice(msg, pr_expiration_values, title=_('Expiration date'), default_choice=expiry) + v = self.window.query_choice(msg, pr_expiration_values, title=_('Expiry'), default_choice=expiry) if v is None: return self.config.set_key('request_expiry', v) From 206bacbcb38805012e6eb15850e5f004bf3d6bd9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 14:25:42 +0100 Subject: [PATCH 0387/1143] move MyTreeView and related classes to own submodule --- electrum/gui/qt/address_list.py | 3 +- electrum/gui/qt/channels_list.py | 3 +- electrum/gui/qt/contact_list.py | 3 +- electrum/gui/qt/history_list.py | 3 +- electrum/gui/qt/invoice_list.py | 4 +- electrum/gui/qt/my_treeview.py | 476 ++++++++++++++++++++++++++ electrum/gui/qt/new_channel_dialog.py | 4 +- electrum/gui/qt/request_list.py | 3 +- electrum/gui/qt/transaction_dialog.py | 4 +- electrum/gui/qt/util.py | 406 ---------------------- electrum/gui/qt/utxo_list.py | 3 +- electrum/gui/qt/watchtower_dialog.py | 3 +- 12 files changed, 497 insertions(+), 418 deletions(-) create mode 100644 electrum/gui/qt/my_treeview.py diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 9251faec1..2331359ca 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -37,7 +37,8 @@ from electrum.bitcoin import is_address from electrum.wallet import InternalAddressCorruption -from .util import MyTreeView, MONOSPACE_FONT, ColorScheme, webopen, MySortModel +from .util import MONOSPACE_FONT, ColorScheme, webopen +from .my_treeview import MyTreeView, MySortModel if TYPE_CHECKING: from .main_window import ElectrumWindow diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index c47b16b67..18c9dadc3 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -19,10 +19,11 @@ from electrum.lnworker import LNWallet from electrum.gui import messages -from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, +from .util import (WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, WaitingDialog, MONOSPACE_FONT, ColorScheme) from .amountedit import BTCAmountEdit, FreezableLineEdit from .util import read_QIcon, font_height +from .my_treeview import MyTreeView if TYPE_CHECKING: from .main_window import ElectrumWindow diff --git a/electrum/gui/qt/contact_list.py b/electrum/gui/qt/contact_list.py index 8dcb5a4b2..712d7f6f9 100644 --- a/electrum/gui/qt/contact_list.py +++ b/electrum/gui/qt/contact_list.py @@ -35,7 +35,8 @@ from electrum.util import block_explorer_URL from electrum.plugin import run_hook -from .util import MyTreeView, webopen +from .util import webopen +from .my_treeview import MyTreeView if TYPE_CHECKING: from .main_window import ElectrumWindow diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index d4fafe3b2..5dc01dc37 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -50,8 +50,9 @@ from .custom_model import CustomNode, CustomModel from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton, - filename_field, MyTreeView, AcceptFileDragDrop, WindowModalDialog, + filename_field, AcceptFileDragDrop, WindowModalDialog, CloseButton, webopen, WWLabel) +from .my_treeview import MyTreeView if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 9cc820b1b..cd5d59bbf 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -36,10 +36,12 @@ from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED from electrum.lnutil import HtlcLog -from .util import MyTreeView, read_QIcon, MySortModel, pr_icons +from .util import read_QIcon, pr_icons from .util import CloseButton, Buttons from .util import WindowModalDialog +from .my_treeview import MyTreeView, MySortModel + if TYPE_CHECKING: from .main_window import ElectrumWindow from .send_tab import SendTab diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py new file mode 100644 index 000000000..27af3a4a9 --- /dev/null +++ b/electrum/gui/qt/my_treeview.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import asyncio +import enum +import os.path +import time +import sys +import platform +import queue +import traceback +import os +import webbrowser +from decimal import Decimal +from functools import partial, lru_cache, wraps +from typing import (NamedTuple, Callable, Optional, TYPE_CHECKING, Union, List, Dict, Any, + Sequence, Iterable, Tuple, Type) + +from PyQt5 import QtWidgets, QtCore +from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, QImage, + QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent, QMouseEvent) +from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal, + QCoreApplication, QItemSelectionModel, QThread, + QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel, + QEvent, QRect, QPoint, QObject) +from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout, + QAbstractItemView, QVBoxLayout, QLineEdit, + QStyle, QDialog, QGroupBox, QButtonGroup, QRadioButton, + QFileDialog, QWidget, QToolButton, QTreeView, QPlainTextEdit, + QHeaderView, QApplication, QToolTip, QTreeWidget, QStyledItemDelegate, + QMenu, QStyleOptionViewItem, QLayout, QLayoutItem, QAbstractButton, + QGraphicsEffect, QGraphicsScene, QGraphicsPixmapItem, QSizePolicy) + +from electrum.i18n import _, languages +from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path +from electrum.util import EventListener, event_listener +from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED +from electrum.logging import Logger +from electrum.qrreader import MissingQrDetectionLib + +from .util import read_QIcon + + +class MyMenu(QMenu): + + def __init__(self, config): + QMenu.__init__(self) + self.setToolTipsVisible(True) + self.config = config + + def addToggle(self, text: str, callback, *, tooltip=''): + m = self.addAction(text, callback) + m.setCheckable(True) + m.setToolTip(tooltip) + return m + + def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=None): + b = self.config.get(name, default) + m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback)) + m.setCheckable(True) + m.setChecked(b) + m.setToolTip(tooltip) + return m + + def _do_toggle_config(self, name, default, callback): + b = self.config.get(name, default) + self.config.set_key(name, not b) + if callback: + callback() + + +def create_toolbar_with_menu(config, title): + menu = MyMenu(config) + toolbar_button = QToolButton() + toolbar_button.setIcon(read_QIcon("preferences.png")) + toolbar_button.setMenu(menu) + toolbar_button.setPopupMode(QToolButton.InstantPopup) + toolbar_button.setFocusPolicy(Qt.NoFocus) + toolbar = QHBoxLayout() + toolbar.addWidget(QLabel(title)) + toolbar.addStretch() + toolbar.addWidget(toolbar_button) + return toolbar, menu + + + +class MySortModel(QSortFilterProxyModel): + def __init__(self, parent, *, sort_role): + super().__init__(parent) + self._sort_role = sort_role + + def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): + item1 = self.sourceModel().itemFromIndex(source_left) + item2 = self.sourceModel().itemFromIndex(source_right) + data1 = item1.data(self._sort_role) + data2 = item2.data(self._sort_role) + if data1 is not None and data2 is not None: + return data1 < data2 + v1 = item1.text() + v2 = item2.text() + try: + return Decimal(v1) < Decimal(v2) + except: + return v1 < v2 + +class ElectrumItemDelegate(QStyledItemDelegate): + def __init__(self, tv: 'MyTreeView'): + super().__init__(tv) + self.tv = tv + self.opened = None + def on_closeEditor(editor: QLineEdit, hint): + self.opened = None + self.tv.is_editor_open = False + if self.tv._pending_update: + self.tv.update() + def on_commitData(editor: QLineEdit): + new_text = editor.text() + idx = QModelIndex(self.opened) + row, col = idx.row(), idx.column() + edit_key = self.tv.get_edit_key_from_coordinate(row, col) + assert edit_key is not None, (idx.row(), idx.column()) + self.tv.on_edited(idx, edit_key=edit_key, text=new_text) + self.closeEditor.connect(on_closeEditor) + self.commitData.connect(on_commitData) + + def createEditor(self, parent, option, idx): + self.opened = QPersistentModelIndex(idx) + self.tv.is_editor_open = True + return super().createEditor(parent, option, idx) + + def paint(self, painter: QPainter, option: QStyleOptionViewItem, idx: QModelIndex) -> None: + custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT) + if custom_data is None: + return super().paint(painter, option, idx) + else: + # let's call the default paint method first; to paint the background (e.g. selection) + super().paint(painter, option, idx) + # and now paint on top of that + custom_data.paint(painter, option.rect) + + def helpEvent(self, evt: QHelpEvent, view: QAbstractItemView, option: QStyleOptionViewItem, idx: QModelIndex) -> bool: + custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT) + if custom_data is None: + return super().helpEvent(evt, view, option, idx) + else: + if evt.type() == QEvent.ToolTip: + if custom_data.show_tooltip(evt): + return True + return super().helpEvent(evt, view, option, idx) + + def sizeHint(self, option: QStyleOptionViewItem, idx: QModelIndex) -> QSize: + custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT) + if custom_data is None: + return super().sizeHint(option, idx) + else: + default_size = super().sizeHint(option, idx) + return custom_data.sizeHint(default_size) + +class MyTreeView(QTreeView): + + ROLE_CLIPBOARD_DATA = Qt.UserRole + 100 + ROLE_CUSTOM_PAINT = Qt.UserRole + 101 + ROLE_EDIT_KEY = Qt.UserRole + 102 + ROLE_FILTER_DATA = Qt.UserRole + 103 + + filter_columns: Iterable[int] + + class BaseColumnsEnum(enum.IntEnum): + @staticmethod + def _generate_next_value_(name: str, start: int, count: int, last_values): + # this is overridden to get a 0-based counter + return count + + Columns: Type[BaseColumnsEnum] + + def __init__( + self, + *, + parent: Optional[QWidget] = None, + main_window: Optional['ElectrumWindow'] = None, + stretch_column: Optional[int] = None, + editable_columns: Optional[Sequence[int]] = None, + ): + parent = parent or main_window + super().__init__(parent) + self.main_window = main_window + self.config = self.main_window.config if self.main_window else None + self.stretch_column = stretch_column + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.create_menu) + self.setUniformRowHeights(True) + + # Control which columns are editable + if editable_columns is None: + editable_columns = [] + self.editable_columns = set(editable_columns) + self.setItemDelegate(ElectrumItemDelegate(self)) + self.current_filter = "" + self.is_editor_open = False + + self.setRootIsDecorated(False) # remove left margin + self.toolbar_shown = False + + # When figuring out the size of columns, Qt by default looks at + # the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents). + # This would be REALLY SLOW, and it's not perfect anyway. + # So to speed the UI up considerably, set it to + # only look at as many rows as currently visible. + self.header().setResizeContentsPrecision(0) + + self._pending_update = False + self._forced_update = False + + self._default_bg_brush = QStandardItem().background() + self.proxy = None # history, and address tabs use a proxy + + def create_menu(self, position: QPoint) -> None: + pass + + def set_editability(self, items): + for idx, i in enumerate(items): + i.setEditable(idx in self.editable_columns) + + def selected_in_column(self, column: int): + items = self.selectionModel().selectedIndexes() + return list(x for x in items if x.column() == column) + + def get_role_data_for_current_item(self, *, col, role) -> Any: + idx = self.selectionModel().currentIndex() + idx = idx.sibling(idx.row(), col) + item = self.item_from_index(idx) + if item: + return item.data(role) + + def item_from_index(self, idx: QModelIndex) -> Optional[QStandardItem]: + model = self.model() + if isinstance(model, QSortFilterProxyModel): + idx = model.mapToSource(idx) + return model.sourceModel().itemFromIndex(idx) + else: + return model.itemFromIndex(idx) + + def original_model(self) -> QAbstractItemModel: + model = self.model() + if isinstance(model, QSortFilterProxyModel): + return model.sourceModel() + else: + return model + + def set_current_idx(self, set_current: QPersistentModelIndex): + if set_current: + assert isinstance(set_current, QPersistentModelIndex) + assert set_current.isValid() + self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent) + + def update_headers(self, headers: Union[List[str], Dict[int, str]]): + # headers is either a list of column names, or a dict: (col_idx->col_name) + if not isinstance(headers, dict): # convert to dict + headers = dict(enumerate(headers)) + col_names = [headers[col_idx] for col_idx in sorted(headers.keys())] + self.original_model().setHorizontalHeaderLabels(col_names) + self.header().setStretchLastSection(False) + for col_idx in headers: + sm = QHeaderView.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeToContents + self.header().setSectionResizeMode(col_idx, sm) + + def keyPressEvent(self, event): + if self.itemDelegate().opened: + return + if event.key() in [Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter]: + self.on_activated(self.selectionModel().currentIndex()) + return + super().keyPressEvent(event) + + def mouseDoubleClickEvent(self, event: QMouseEvent): + idx: QModelIndex = self.indexAt(event.pos()) + if self.proxy: + idx = self.proxy.mapToSource(idx) + if not idx.isValid(): + # can happen e.g. before list is populated for the first time + return + self.on_double_click(idx) + + def on_double_click(self, idx): + pass + + def on_activated(self, idx): + # on 'enter' we show the menu + pt = self.visualRect(idx).bottomLeft() + pt.setX(50) + self.customContextMenuRequested.emit(pt) + + def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None): + """ + this is to prevent: + edit: editing failed + from inside qt + """ + return super().edit(idx, trigger, event) + + def on_edited(self, idx: QModelIndex, edit_key, *, text: str) -> None: + raise NotImplementedError() + + def should_hide(self, row): + """ + row_num is for self.model(). So if there is a proxy, it is the row number + in that! + """ + return False + + def get_text_from_coordinate(self, row, col) -> str: + idx = self.model().index(row, col) + item = self.item_from_index(idx) + return item.text() + + def get_role_data_from_coordinate(self, row, col, *, role) -> Any: + idx = self.model().index(row, col) + item = self.item_from_index(idx) + role_data = item.data(role) + return role_data + + def get_edit_key_from_coordinate(self, row, col) -> Any: + # overriding this might allow avoiding storing duplicate data + return self.get_role_data_from_coordinate(row, col, role=self.ROLE_EDIT_KEY) + + def get_filter_data_from_coordinate(self, row, col) -> str: + filter_data = self.get_role_data_from_coordinate(row, col, role=self.ROLE_FILTER_DATA) + if filter_data: + return filter_data + txt = self.get_text_from_coordinate(row, col) + txt = txt.lower() + return txt + + def hide_row(self, row_num): + """ + row_num is for self.model(). So if there is a proxy, it is the row number + in that! + """ + should_hide = self.should_hide(row_num) + if not self.current_filter and should_hide is None: + # no filters at all, neither date nor search + self.setRowHidden(row_num, QModelIndex(), False) + return + for column in self.filter_columns: + filter_data = self.get_filter_data_from_coordinate(row_num, column) + if self.current_filter in filter_data: + # the filter matched, but the date filter might apply + self.setRowHidden(row_num, QModelIndex(), bool(should_hide)) + break + else: + # we did not find the filter in any columns, hide the item + self.setRowHidden(row_num, QModelIndex(), True) + + def filter(self, p=None): + if p is not None: + p = p.lower() + self.current_filter = p + self.hide_rows() + + def hide_rows(self): + for row in range(self.model().rowCount()): + self.hide_row(row) + + def create_toolbar(self, config): + return + + def create_toolbar_buttons(self): + hbox = QHBoxLayout() + buttons = self.get_toolbar_buttons() + for b in buttons: + b.setVisible(False) + hbox.addWidget(b) + self.toolbar_buttons = buttons + return hbox + + def create_toolbar_with_menu(self, title): + return create_toolbar_with_menu(self.config, title) + + def show_toolbar(self, state, config=None): + if state == self.toolbar_shown: + return + self.toolbar_shown = state + for b in self.toolbar_buttons: + b.setVisible(state) + if not state: + self.on_hide_toolbar() + + def toggle_toolbar(self, config=None): + self.show_toolbar(not self.toolbar_shown, config) + + def add_copy_menu(self, menu: QMenu, idx) -> QMenu: + cc = menu.addMenu(_("Copy")) + for column in self.Columns: + if self.isColumnHidden(column): + continue + column_title = self.original_model().horizontalHeaderItem(column).text() + if not column_title: + continue + item_col = self.item_from_index(idx.sibling(idx.row(), column)) + clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA) + if clipboard_data is None: + clipboard_data = item_col.text().strip() + cc.addAction(column_title, + lambda text=clipboard_data, title=column_title: + self.place_text_on_clipboard(text, title=title)) + return cc + + def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: + self.main_window.do_copy(text, title=title) + + def showEvent(self, e: 'QShowEvent'): + super().showEvent(e) + if e.isAccepted() and self._pending_update: + self._forced_update = True + self.update() + self._forced_update = False + + def maybe_defer_update(self) -> bool: + """Returns whether we should defer an update/refresh.""" + defer = (not self._forced_update + and (not self.isVisible() or self.is_editor_open)) + # side-effect: if we decide to defer update, the state will become stale: + self._pending_update = defer + return defer + + def find_row_by_key(self, key) -> Optional[int]: + for row in range(0, self.std_model.rowCount()): + item = self.std_model.item(row, 0) + if item.data(self.key_role) == key: + return row + + def refresh_all(self): + if self.maybe_defer_update(): + return + for row in range(0, self.std_model.rowCount()): + item = self.std_model.item(row, 0) + key = item.data(self.key_role) + self.refresh_row(key, row) + + def refresh_row(self, key: str, row: int) -> None: + pass + + def refresh_item(self, key): + row = self.find_row_by_key(key) + if row is not None: + self.refresh_row(key, row) + + def delete_item(self, key): + row = self.find_row_by_key(key) + if row is not None: + self.std_model.takeRow(row) + self.hide_if_empty() + + diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index 717b53dbc..d48b661a2 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -15,7 +15,7 @@ EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit) from .amountedit import BTCAmountEdit - +from .my_treeview import create_toolbar_with_menu if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -34,7 +34,7 @@ def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, m self.trampoline_names = list(self.trampolines.keys()) self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT vbox = QVBoxLayout(self) - toolbar, menu = util.create_toolbar_with_menu(self.config, '') + toolbar, menu = create_toolbar_with_menu(self.config, '') recov_tooltip = messages.to_rtf(_(messages.MSG_RECOVERABLE_CHANNELS)) menu.addConfig( _("Create recoverable channels"), 'use_recoverable_channels', True, diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index f9fb2ff14..b3e96bbf5 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -35,7 +35,8 @@ from electrum.plugin import run_hook from electrum.invoices import Invoice -from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel +from .util import pr_icons, read_QIcon, webopen +from .my_treeview import MyTreeView, MySortModel if TYPE_CHECKING: from .main_window import ElectrumWindow diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index feb47d8a5..e66a861fe 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -65,7 +65,7 @@ BlockingWaitingDialog, getSaveFileName, ColorSchemeItem, get_iconname_qrcode) from .rate_limiter import rate_limited - +from .my_treeview import create_toolbar_with_menu if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -409,7 +409,7 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav vbox = QVBoxLayout() self.setLayout(vbox) - toolbar, menu = util.create_toolbar_with_menu(self.config, '') + toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( _('Download missing data'), 'tx_dialog_fetch_txin_data', False, tooltip=_( diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 3985da79e..a03a78cb5 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -516,413 +516,7 @@ def set_csv(v): return vbox, filename_e, b1 -class ElectrumItemDelegate(QStyledItemDelegate): - def __init__(self, tv: 'MyTreeView'): - super().__init__(tv) - self.tv = tv - self.opened = None - def on_closeEditor(editor: QLineEdit, hint): - self.opened = None - self.tv.is_editor_open = False - if self.tv._pending_update: - self.tv.update() - def on_commitData(editor: QLineEdit): - new_text = editor.text() - idx = QModelIndex(self.opened) - row, col = idx.row(), idx.column() - edit_key = self.tv.get_edit_key_from_coordinate(row, col) - assert edit_key is not None, (idx.row(), idx.column()) - self.tv.on_edited(idx, edit_key=edit_key, text=new_text) - self.closeEditor.connect(on_closeEditor) - self.commitData.connect(on_commitData) - - def createEditor(self, parent, option, idx): - self.opened = QPersistentModelIndex(idx) - self.tv.is_editor_open = True - return super().createEditor(parent, option, idx) - - def paint(self, painter: QPainter, option: QStyleOptionViewItem, idx: QModelIndex) -> None: - custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT) - if custom_data is None: - return super().paint(painter, option, idx) - else: - # let's call the default paint method first; to paint the background (e.g. selection) - super().paint(painter, option, idx) - # and now paint on top of that - custom_data.paint(painter, option.rect) - - def helpEvent(self, evt: QHelpEvent, view: QAbstractItemView, option: QStyleOptionViewItem, idx: QModelIndex) -> bool: - custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT) - if custom_data is None: - return super().helpEvent(evt, view, option, idx) - else: - if evt.type() == QEvent.ToolTip: - if custom_data.show_tooltip(evt): - return True - return super().helpEvent(evt, view, option, idx) - - def sizeHint(self, option: QStyleOptionViewItem, idx: QModelIndex) -> QSize: - custom_data = idx.data(MyTreeView.ROLE_CUSTOM_PAINT) - if custom_data is None: - return super().sizeHint(option, idx) - else: - default_size = super().sizeHint(option, idx) - return custom_data.sizeHint(default_size) - - -class MyMenu(QMenu): - - def __init__(self, config): - QMenu.__init__(self) - self.setToolTipsVisible(True) - self.config = config - - def addToggle(self, text: str, callback, *, tooltip=''): - m = self.addAction(text, callback) - m.setCheckable(True) - m.setToolTip(tooltip) - return m - - def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=None): - b = self.config.get(name, default) - m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback)) - m.setCheckable(True) - m.setChecked(b) - m.setToolTip(tooltip) - return m - - def _do_toggle_config(self, name, default, callback): - b = self.config.get(name, default) - self.config.set_key(name, not b) - if callback: - callback() - -def create_toolbar_with_menu(config, title): - menu = MyMenu(config) - toolbar_button = QToolButton() - toolbar_button.setIcon(read_QIcon("preferences.png")) - toolbar_button.setMenu(menu) - toolbar_button.setPopupMode(QToolButton.InstantPopup) - toolbar_button.setFocusPolicy(Qt.NoFocus) - toolbar = QHBoxLayout() - toolbar.addWidget(QLabel(title)) - toolbar.addStretch() - toolbar.addWidget(toolbar_button) - return toolbar, menu - -class MyTreeView(QTreeView): - ROLE_CLIPBOARD_DATA = Qt.UserRole + 100 - ROLE_CUSTOM_PAINT = Qt.UserRole + 101 - ROLE_EDIT_KEY = Qt.UserRole + 102 - ROLE_FILTER_DATA = Qt.UserRole + 103 - - filter_columns: Iterable[int] - - class BaseColumnsEnum(enum.IntEnum): - @staticmethod - def _generate_next_value_(name: str, start: int, count: int, last_values): - # this is overridden to get a 0-based counter - return count - - Columns: Type[BaseColumnsEnum] - - def __init__( - self, - *, - parent: Optional[QWidget] = None, - main_window: Optional['ElectrumWindow'] = None, - stretch_column: Optional[int] = None, - editable_columns: Optional[Sequence[int]] = None, - ): - parent = parent or main_window - super().__init__(parent) - self.main_window = main_window - self.config = self.main_window.config if self.main_window else None - self.stretch_column = stretch_column - self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.create_menu) - self.setUniformRowHeights(True) - - # Control which columns are editable - if editable_columns is None: - editable_columns = [] - self.editable_columns = set(editable_columns) - self.setItemDelegate(ElectrumItemDelegate(self)) - self.current_filter = "" - self.is_editor_open = False - - self.setRootIsDecorated(False) # remove left margin - self.toolbar_shown = False - - # When figuring out the size of columns, Qt by default looks at - # the first 1000 rows (at least if resize mode is QHeaderView.ResizeToContents). - # This would be REALLY SLOW, and it's not perfect anyway. - # So to speed the UI up considerably, set it to - # only look at as many rows as currently visible. - self.header().setResizeContentsPrecision(0) - - self._pending_update = False - self._forced_update = False - - self._default_bg_brush = QStandardItem().background() - self.proxy = None # history, and address tabs use a proxy - - def create_menu(self, position: QPoint) -> None: - pass - - def set_editability(self, items): - for idx, i in enumerate(items): - i.setEditable(idx in self.editable_columns) - - def selected_in_column(self, column: int): - items = self.selectionModel().selectedIndexes() - return list(x for x in items if x.column() == column) - - def get_role_data_for_current_item(self, *, col, role) -> Any: - idx = self.selectionModel().currentIndex() - idx = idx.sibling(idx.row(), col) - item = self.item_from_index(idx) - if item: - return item.data(role) - - def item_from_index(self, idx: QModelIndex) -> Optional[QStandardItem]: - model = self.model() - if isinstance(model, QSortFilterProxyModel): - idx = model.mapToSource(idx) - return model.sourceModel().itemFromIndex(idx) - else: - return model.itemFromIndex(idx) - - def original_model(self) -> QAbstractItemModel: - model = self.model() - if isinstance(model, QSortFilterProxyModel): - return model.sourceModel() - else: - return model - - def set_current_idx(self, set_current: QPersistentModelIndex): - if set_current: - assert isinstance(set_current, QPersistentModelIndex) - assert set_current.isValid() - self.selectionModel().select(QModelIndex(set_current), QItemSelectionModel.SelectCurrent) - - def update_headers(self, headers: Union[List[str], Dict[int, str]]): - # headers is either a list of column names, or a dict: (col_idx->col_name) - if not isinstance(headers, dict): # convert to dict - headers = dict(enumerate(headers)) - col_names = [headers[col_idx] for col_idx in sorted(headers.keys())] - self.original_model().setHorizontalHeaderLabels(col_names) - self.header().setStretchLastSection(False) - for col_idx in headers: - sm = QHeaderView.Stretch if col_idx == self.stretch_column else QHeaderView.ResizeToContents - self.header().setSectionResizeMode(col_idx, sm) - - def keyPressEvent(self, event): - if self.itemDelegate().opened: - return - if event.key() in [Qt.Key_F2, Qt.Key_Return, Qt.Key_Enter]: - self.on_activated(self.selectionModel().currentIndex()) - return - super().keyPressEvent(event) - - def mouseDoubleClickEvent(self, event: QMouseEvent): - idx: QModelIndex = self.indexAt(event.pos()) - if self.proxy: - idx = self.proxy.mapToSource(idx) - if not idx.isValid(): - # can happen e.g. before list is populated for the first time - return - self.on_double_click(idx) - - def on_double_click(self, idx): - pass - - def on_activated(self, idx): - # on 'enter' we show the menu - pt = self.visualRect(idx).bottomLeft() - pt.setX(50) - self.customContextMenuRequested.emit(pt) - - def edit(self, idx, trigger=QAbstractItemView.AllEditTriggers, event=None): - """ - this is to prevent: - edit: editing failed - from inside qt - """ - return super().edit(idx, trigger, event) - - def on_edited(self, idx: QModelIndex, edit_key, *, text: str) -> None: - raise NotImplementedError() - - def should_hide(self, row): - """ - row_num is for self.model(). So if there is a proxy, it is the row number - in that! - """ - return False - - def get_text_from_coordinate(self, row, col) -> str: - idx = self.model().index(row, col) - item = self.item_from_index(idx) - return item.text() - - def get_role_data_from_coordinate(self, row, col, *, role) -> Any: - idx = self.model().index(row, col) - item = self.item_from_index(idx) - role_data = item.data(role) - return role_data - - def get_edit_key_from_coordinate(self, row, col) -> Any: - # overriding this might allow avoiding storing duplicate data - return self.get_role_data_from_coordinate(row, col, role=self.ROLE_EDIT_KEY) - - def get_filter_data_from_coordinate(self, row, col) -> str: - filter_data = self.get_role_data_from_coordinate(row, col, role=self.ROLE_FILTER_DATA) - if filter_data: - return filter_data - txt = self.get_text_from_coordinate(row, col) - txt = txt.lower() - return txt - - def hide_row(self, row_num): - """ - row_num is for self.model(). So if there is a proxy, it is the row number - in that! - """ - should_hide = self.should_hide(row_num) - if not self.current_filter and should_hide is None: - # no filters at all, neither date nor search - self.setRowHidden(row_num, QModelIndex(), False) - return - for column in self.filter_columns: - filter_data = self.get_filter_data_from_coordinate(row_num, column) - if self.current_filter in filter_data: - # the filter matched, but the date filter might apply - self.setRowHidden(row_num, QModelIndex(), bool(should_hide)) - break - else: - # we did not find the filter in any columns, hide the item - self.setRowHidden(row_num, QModelIndex(), True) - - def filter(self, p=None): - if p is not None: - p = p.lower() - self.current_filter = p - self.hide_rows() - def hide_rows(self): - for row in range(self.model().rowCount()): - self.hide_row(row) - - def create_toolbar(self, config): - return - - def create_toolbar_buttons(self): - hbox = QHBoxLayout() - buttons = self.get_toolbar_buttons() - for b in buttons: - b.setVisible(False) - hbox.addWidget(b) - self.toolbar_buttons = buttons - return hbox - - def create_toolbar_with_menu(self, title): - return create_toolbar_with_menu(self.config, title) - - def show_toolbar(self, state, config=None): - if state == self.toolbar_shown: - return - self.toolbar_shown = state - for b in self.toolbar_buttons: - b.setVisible(state) - if not state: - self.on_hide_toolbar() - - def toggle_toolbar(self, config=None): - self.show_toolbar(not self.toolbar_shown, config) - - def add_copy_menu(self, menu: QMenu, idx) -> QMenu: - cc = menu.addMenu(_("Copy")) - for column in self.Columns: - if self.isColumnHidden(column): - continue - column_title = self.original_model().horizontalHeaderItem(column).text() - if not column_title: - continue - item_col = self.item_from_index(idx.sibling(idx.row(), column)) - clipboard_data = item_col.data(self.ROLE_CLIPBOARD_DATA) - if clipboard_data is None: - clipboard_data = item_col.text().strip() - cc.addAction(column_title, - lambda text=clipboard_data, title=column_title: - self.place_text_on_clipboard(text, title=title)) - return cc - - def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: - self.main_window.do_copy(text, title=title) - - def showEvent(self, e: 'QShowEvent'): - super().showEvent(e) - if e.isAccepted() and self._pending_update: - self._forced_update = True - self.update() - self._forced_update = False - - def maybe_defer_update(self) -> bool: - """Returns whether we should defer an update/refresh.""" - defer = (not self._forced_update - and (not self.isVisible() or self.is_editor_open)) - # side-effect: if we decide to defer update, the state will become stale: - self._pending_update = defer - return defer - - def find_row_by_key(self, key) -> Optional[int]: - for row in range(0, self.std_model.rowCount()): - item = self.std_model.item(row, 0) - if item.data(self.key_role) == key: - return row - - def refresh_all(self): - if self.maybe_defer_update(): - return - for row in range(0, self.std_model.rowCount()): - item = self.std_model.item(row, 0) - key = item.data(self.key_role) - self.refresh_row(key, row) - - def refresh_row(self, key: str, row: int) -> None: - pass - - def refresh_item(self, key): - row = self.find_row_by_key(key) - if row is not None: - self.refresh_row(key, row) - - def delete_item(self, key): - row = self.find_row_by_key(key) - if row is not None: - self.std_model.takeRow(row) - self.hide_if_empty() - - -class MySortModel(QSortFilterProxyModel): - def __init__(self, parent, *, sort_role): - super().__init__(parent) - self._sort_role = sort_role - - def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): - item1 = self.sourceModel().itemFromIndex(source_left) - item2 = self.sourceModel().itemFromIndex(source_right) - data1 = item1.data(self._sort_role) - data2 = item2.data(self._sort_role) - if data1 is not None and data2 is not None: - return data1 < data2 - v1 = item1.text() - v2 = item2.text() - try: - return Decimal(v1) < Decimal(v2) - except: - return v1 < v2 def get_iconname_qrcode() -> str: diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index a28b3ee82..e41704eae 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -36,7 +36,8 @@ from electrum.transaction import PartialTxInput, PartialTxOutput from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT -from .util import MyTreeView, ColorScheme, MONOSPACE_FONT, EnterButton +from .util import ColorScheme, MONOSPACE_FONT, EnterButton +from .my_treeview import MyTreeView from .new_channel_dialog import NewChannelDialog if TYPE_CHECKING: diff --git a/electrum/gui/qt/watchtower_dialog.py b/electrum/gui/qt/watchtower_dialog.py index 37e943d0b..79db87b90 100644 --- a/electrum/gui/qt/watchtower_dialog.py +++ b/electrum/gui/qt/watchtower_dialog.py @@ -30,7 +30,8 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QPushButton, QLabel) from electrum.i18n import _ -from .util import MyTreeView, Buttons +from .util import Buttons +from .my_treeview import MyTreeView class WatcherList(MyTreeView): From 2881c496718a47d3cce7ede8b39be35db881bd6e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 15 Mar 2023 15:18:48 +0100 Subject: [PATCH 0388/1143] qml: move technical details to bottom of InvoiceDialog, add routing hints --- electrum/gui/qml/components/InvoiceDialog.qml | 188 +++++++++++------- electrum/gui/qml/qeinvoice.py | 29 ++- 2 files changed, 134 insertions(+), 83 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index d20f35ff9..0bfb4bba0 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -118,82 +118,6 @@ ElDialog { } } - Label { - Layout.columnSpan: 2 - Layout.topMargin: constants.paddingSmall - visible: invoice.invoiceType == Invoice.LightningInvoice - text: qsTr('Remote Pubkey') - color: Material.accentColor - } - - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - - visible: invoice.invoiceType == Invoice.LightningInvoice - leftPadding: constants.paddingMedium - - RowLayout { - width: parent.width - Label { - id: pubkeyLabel - Layout.fillWidth: true - text: 'pubkey' in invoice.lnprops ? invoice.lnprops.pubkey : '' - font.family: FixedFont - wrapMode: Text.Wrap - } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - enabled: pubkeyLabel.text - onClicked: { - var dialog = app.genericShareDialog.createObject(app, - { title: qsTr('Node public key'), text: invoice.lnprops.pubkey } - ) - dialog.open() - } - } - } - } - - Label { - Layout.columnSpan: 2 - Layout.topMargin: constants.paddingSmall - visible: invoice.invoiceType == Invoice.LightningInvoice - text: qsTr('Payment hash') - color: Material.accentColor - } - - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true - - visible: invoice.invoiceType == Invoice.LightningInvoice - leftPadding: constants.paddingMedium - - RowLayout { - width: parent.width - Label { - id: paymenthashLabel - Layout.fillWidth: true - text: 'payment_hash' in invoice.lnprops ? invoice.lnprops.payment_hash : '' - font.family: FixedFont - wrapMode: Text.Wrap - } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - enabled: paymenthashLabel.text - onClicked: { - var dialog = app.genericShareDialog.createObject(app, - { title: qsTr('Payment hash'), text: invoice.lnprops.payment_hash } - ) - dialog.open() - } - } - } - } - Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall @@ -366,6 +290,118 @@ ElDialog { } + Heading { + Layout.columnSpan: 2 + text: qsTr('Technical properties') + } + + Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + visible: invoice.invoiceType == Invoice.LightningInvoice + text: qsTr('Remote Pubkey') + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + visible: invoice.invoiceType == Invoice.LightningInvoice + leftPadding: constants.paddingMedium + + RowLayout { + width: parent.width + Label { + id: pubkeyLabel + Layout.fillWidth: true + text: 'pubkey' in invoice.lnprops ? invoice.lnprops.pubkey : '' + font.family: FixedFont + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + enabled: pubkeyLabel.text + onClicked: { + var dialog = app.genericShareDialog.createObject(app, + { title: qsTr('Node public key'), text: invoice.lnprops.pubkey } + ) + dialog.open() + } + } + } + } + + Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + visible: invoice.invoiceType == Invoice.LightningInvoice + text: qsTr('Payment hash') + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + visible: invoice.invoiceType == Invoice.LightningInvoice + leftPadding: constants.paddingMedium + + RowLayout { + width: parent.width + Label { + id: paymenthashLabel + Layout.fillWidth: true + text: 'payment_hash' in invoice.lnprops ? invoice.lnprops.payment_hash : '' + font.family: FixedFont + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + enabled: paymenthashLabel.text + onClicked: { + var dialog = app.genericShareDialog.createObject(app, { + title: qsTr('Payment hash'), + text: invoice.lnprops.payment_hash + }) + dialog.open() + } + } + } + } + + Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + visible: 'r' in invoice.lnprops && invoice.lnprops.r.length + text: qsTr('Routing hints') + color: Material.accentColor + } + + Repeater { + visible: 'r' in invoice.lnprops && invoice.lnprops.r.length + model: invoice.lnprops.r + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + RowLayout { + width: parent.width + + Label { + text: modelData.scid + } + Label { + Layout.fillWidth: true + text: modelData.node + wrapMode: Text.Wrap + } + } + } + } } } diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 039151b24..179e658cc 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -16,6 +16,7 @@ from electrum.transaction import PartialTxOutput from electrum.util import (parse_URI, InvalidBitcoinURI, InvoiceError, maybe_extract_lightning_payment_identifier) +from electrum.lnutil import format_short_channel_id from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl from electrum.bitcoin import COIN from electrum.paymentrequest import PaymentRequest @@ -144,6 +145,7 @@ def __init__(self, parent=None): self._effectiveInvoice = None self._amount = QEAmount() self._userinfo = '' + self._lnprops = {} self._timer = QTimer(self) self._timer.setSingleShot(True) @@ -227,25 +229,36 @@ def status_str(self): status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice) return self._effectiveInvoice.get_status_str(status) - # single address only, TODO: n outputs @pyqtProperty(str, notify=invoiceChanged) def address(self): return self._effectiveInvoice.get_address() if self._effectiveInvoice else '' @pyqtProperty('QVariantMap', notify=invoiceChanged) def lnprops(self): + return self._lnprops + + def set_lnprops(self): + self._lnprops = {} if not self.invoiceType == QEInvoice.Type.LightningInvoice: - return {} + return + lnaddr = self._effectiveInvoice._lnaddr - self._logger.debug(str(lnaddr)) - self._logger.debug(str(lnaddr.get_routing_info('t'))) - return { + ln_routing_info = lnaddr.get_routing_info('r') + self._logger.debug(str(ln_routing_info)) + + self._lnprops = { 'pubkey': lnaddr.pubkey.serialize().hex(), 'payment_hash': lnaddr.paymenthash.hex(), - 't': '', #lnaddr.get_routing_info('t')[0][0].hex(), - 'r': '' #lnaddr.get_routing_info('r')[0][0][0].hex() + 'r': [{ + 'node': self.name_for_node_id(x[-1][0]), + 'scid': format_short_channel_id(x[-1][1]) + } for x in ln_routing_info] if ln_routing_info else [] } + def name_for_node_id(self, node_id): + node_info = self._wallet.wallet.lnworker.channel_db.get_node_info_for_node_id(node_id=node_id) + return node_info.alias if node_info.alias else node_id.hex() + @pyqtSlot() def clear(self): self.recipient = '' @@ -276,6 +289,8 @@ def set_effective_invoice(self, invoice: Invoice): else: self.setInvoiceType(QEInvoice.Type.OnchainInvoice) + self.set_lnprops() + self.canSave = True self.determine_can_pay() From c690c9c1bed4fbe52503267bdb1d2dc394a03884 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 15 Mar 2023 15:39:35 +0100 Subject: [PATCH 0389/1143] Revert "qml: ElDialog titlebar click moves focus, hack for android to remove onscreen keyboard" This reverts commit f0f320b119433152b01c469f0e270c9d0bb0709a. --- .../gui/qml/components/controls/ElDialog.qml | 73 ++++++++----------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 24f9cee17..ed7865bc1 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -29,57 +29,42 @@ Dialog { } } - header: Item { - implicitWidth: rootLayout.implicitWidth - implicitHeight: rootLayout.implicitHeight + header: ColumnLayout { + spacing: 0 - MouseArea { - anchors.fill: parent - onClicked: { - // hack to allow titlebar click to remove on screen keyboard by - // moving focus to label - titleLabel.forceActiveFocus() - } - } - - ColumnLayout { - id: rootLayout + RowLayout { spacing: 0 - RowLayout { - spacing: 0 - - Image { - visible: iconSource - source: iconSource - Layout.preferredWidth: constants.iconSizeXLarge - Layout.preferredHeight: constants.iconSizeXLarge - Layout.leftMargin: constants.paddingMedium - Layout.topMargin: constants.paddingMedium - Layout.bottomMargin: constants.paddingMedium - } - - Label { - id: titleLabel - text: title - elide: Label.ElideRight - Layout.fillWidth: true - leftPadding: constants.paddingXLarge - topPadding: constants.paddingXLarge - bottomPadding: constants.paddingXLarge - rightPadding: constants.paddingXLarge - font.bold: true - font.pixelSize: constants.fontSizeMedium - } + Image { + visible: iconSource + source: iconSource + Layout.preferredWidth: constants.iconSizeXLarge + Layout.preferredHeight: constants.iconSizeXLarge + Layout.leftMargin: constants.paddingMedium + Layout.topMargin: constants.paddingMedium + Layout.bottomMargin: constants.paddingMedium } - Rectangle { + Label { + text: title + elide: Label.ElideRight Layout.fillWidth: true - Layout.leftMargin: constants.paddingXXSmall - Layout.rightMargin: constants.paddingXXSmall - height: 1 - color: Qt.rgba(0,0,0,0.5) + leftPadding: constants.paddingXLarge + topPadding: constants.paddingXLarge + bottomPadding: constants.paddingXLarge + rightPadding: constants.paddingXLarge + font.bold: true + font.pixelSize: constants.fontSizeMedium } } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: constants.paddingXXSmall + Layout.rightMargin: constants.paddingXXSmall + height: 1 + color: Qt.rgba(0,0,0,0.5) + } } + } From 3ddffb97735da6c3d3c073f3ee3d2f89f76cc14d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 15:41:08 +0100 Subject: [PATCH 0390/1143] follow-up 206bacbcb38805012e6eb15850e5f004bf3d6bd9 --- electrum/gui/qt/swap_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index f2561d021..5fbeb07dd 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -14,6 +14,7 @@ EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit) from .amountedit import BTCAmountEdit from .fee_slider import FeeSlider, FeeComboBox +from .my_treeview import create_toolbar_with_menu if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -41,7 +42,7 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No self.channels = channels self.is_reverse = is_reverse if is_reverse is not None else True vbox = QVBoxLayout(self) - toolbar, menu = util.create_toolbar_with_menu(self.config, '') + toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( _("Allow instant swaps"), 'allow_instant_swaps', False, tooltip=messages.to_rtf(messages.MSG_CONFIG_INSTANT_SWAPS), From 91f36db8ef5859aab733455982789b2afac74bae Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 15:42:58 +0100 Subject: [PATCH 0391/1143] type checking, follow-up 206bacbcb38805012e6eb15850e5f004bf3d6bd9 --- electrum/gui/qt/my_treeview.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index 27af3a4a9..acbd560c0 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -62,6 +62,9 @@ from .util import read_QIcon +if TYPE_CHECKING: + from .main_window import ElectrumWindow + class MyMenu(QMenu): From 4872ec75ffe90287fac4cb1fe5c84898aa589fe3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 15 Mar 2023 16:31:13 +0100 Subject: [PATCH 0392/1143] add initial release notes for 4.4.0 --- RELEASE-NOTES | 18 ++++++++++++++++++ electrum/version.py | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 43cd0f729..6da49eedc 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,21 @@ +# Release 4.4 (not released yet) + * New Android app, using QML instead of Kivy + * Qt GUI improvements + - New onchain tx creation flow + - Advanced options have been moved to toolbars, where their effect + can be more directly observed. + * Privacy features: + - lightning: support for option scid_alias. + - UTXO privacy analysis that displays the parents of a coin (Qt + GUI). + - Option to fully spend a selection of UTXOs into a new channel or + submarine swap (Qt GUI). + * Internal: + - Lightning invoices are regenerated everytime routing hints are + deprecated due to liquidity changes. + - Script descriptors are used internally to sign transactions. + + # Release 4.3.4 - Copyright is Dubious (January 26, 2023) * Lightning: - make sending trampoline payments more reliable (5251e7f8) diff --git a/electrum/version.py b/electrum/version.py index 4f2b93b60..349e96364 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.3.4' # version of the client package -APK_VERSION = '4.3.4.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.4.0' # version of the client package +APK_VERSION = '4.4.0.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 4e2b7c6ab341ed1381c489a2b7e8523a29b2026e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 05:51:57 +0100 Subject: [PATCH 0393/1143] qml: remove requestExpiry from preferences dialog (redundant) --- electrum/gui/qml/components/Preferences.qml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 575b121ef..2dbefa60b 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -239,19 +239,6 @@ Pane { } } - Label { - text: qsTr('Default request expiry') - Layout.fillWidth: false - } - - RequestExpiryComboBox { - includeNever: false - onCurrentValueChanged: { - if (activeFocus) - Config.requestExpiry = currentValue - } - } - PrefsHeading { Layout.columnSpan: 2 text: qsTr('Lightning') From 43d6fd2aef11a10f816e9c26aa03b5ddf0c8acac Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 08:24:35 +0100 Subject: [PATCH 0394/1143] qml: use get_node_alias in name_for_node_id. (fixes crash caused by lnworker.channel_db being None with trampoline.) --- electrum/gui/qml/qeinvoice.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 179e658cc..259f91131 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -256,8 +256,7 @@ def set_lnprops(self): } def name_for_node_id(self, node_id): - node_info = self._wallet.wallet.lnworker.channel_db.get_node_info_for_node_id(node_id=node_id) - return node_info.alias if node_info.alias else node_id.hex() + node_alias = self._wallet.wallet.lnworker.get_node_alias(node_id) or node_id.hex() @pyqtSlot() def clear(self): From 09afacd51c2521281a599d309c3afa663fd1a475 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 08:29:36 +0100 Subject: [PATCH 0395/1143] qml: fix logical error with PIN code timeout. --- electrum/gui/qml/components/main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 9505a8914..ae5b59150 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -513,7 +513,7 @@ ApplicationWindow // no PIN configured qtobject.authProceed() } else { - if (Date.now() - _lastCorrectPinTime > _pinValidSeconds * 1000) { + if (Date.now() - _lastCorrectPinTime <= _pinValidSeconds * 1000) { // correct pin entered recently, accept. qtobject.authProceed() return From 337d2a32d8f166888c3bfa7974d08af5a7a9c47d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 09:11:48 +0100 Subject: [PATCH 0396/1143] qml PIN: do not lock inactive app, and remove timeout - the activity callback does not work properly on android (does not work on my phone). Also, it duplicates the lock screen function of most phones. - if we do not lock inactive app, then the PIN feature does not need a timeout, and is easier to understand without it. - in Preferences, explain what it does --- electrum/gui/qml/components/Preferences.qml | 2 +- electrum/gui/qml/components/main.qml | 32 --------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 2dbefa60b..bc090c9ea 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -191,7 +191,7 @@ Pane { } Label { Layout.fillWidth: true - text: qsTr('PIN') + text: qsTr('PIN protect payments') wrapMode: Text.Wrap } } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index ae5b59150..adc334025 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -485,9 +485,6 @@ ApplicationWindow } } - property var _lastCorrectPinTime: 0 - property int _pinValidSeconds: 5*60 - function handleAuthRequired(qtobject, method) { console.log('auth using method ' + method) if (method == 'wallet') { @@ -513,14 +510,8 @@ ApplicationWindow // no PIN configured qtobject.authProceed() } else { - if (Date.now() - _lastCorrectPinTime <= _pinValidSeconds * 1000) { - // correct pin entered recently, accept. - qtobject.authProceed() - return - } var dialog = app.pinDialog.createObject(app, {mode: 'check', pincode: Config.pinCode}) dialog.accepted.connect(function() { - _lastCorrectPinTime = Date.now() qtobject.authProceed() dialog.close() }) @@ -538,27 +529,4 @@ ApplicationWindow property var _lastActive: 0 // record time of last activity property bool _lockDialogShown: false - onActiveChanged: { - console.log('app active = ' + active) - if (active) { - if (!_lastActive) { - _lastActive = Date.now() - return - } - // activated - if (Date.now() - _lastCorrectPinTime > _pinValidSeconds * 1000) { - if (_lockDialogShown || Config.pinCode == '') - return - var dialog = app.pinDialog.createObject(app, {mode: 'check', canCancel: false, pincode: Config.pinCode}) - dialog.accepted.connect(function() { - _lastCorrectPinTime = Date.now() - dialog.close() - _lockDialogShown = false - }) - dialog.open() - _lockDialogShown = true - } - } - } - } From 8db2dcabe370e8772c1690428373e18cd9077d93 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 10:11:33 +0100 Subject: [PATCH 0397/1143] qml: Invoice Dialog technical details header only relevant for lightning --- electrum/gui/qml/components/InvoiceDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 0bfb4bba0..5f72de80a 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -292,6 +292,7 @@ ElDialog { Heading { Layout.columnSpan: 2 + visible: invoice.invoiceType == Invoice.LightningInvoice text: qsTr('Technical properties') } From bcb06e5075f22bcf7e4c452943fbb0035ef69c78 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 10:25:15 +0100 Subject: [PATCH 0398/1143] qml: set default minimum histogram if histogram empty --- electrum/gui/qml/qenetwork.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 4caa44f07..3e13b6f72 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -89,11 +89,11 @@ def on_event_status(self, *args): @event_listener def on_event_fee_histogram(self, histogram): self._logger.debug(f'fee histogram updated: {repr(histogram)}') - if histogram is None: - histogram = [] self.update_histogram(histogram) def update_histogram(self, histogram): + if not histogram: + histogram = [[FEERATE_DEFAULT_RELAY/1000,1]] # cap the histogram to a limited number of megabytes bytes_limit=10*1000*1000 bytes_current = 0 From c5dc133c4cb579bd7fcc9756d74c872460db1a63 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 10:18:11 +0100 Subject: [PATCH 0399/1143] qml: use daemon threads Without this, if a user starts a lightning payment and quits the app before the payment succeeds or fails, the app hangs indefinitely and needs to be killed, because the future never returns. --- electrum/gui/qml/qewallet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 9893061c0..0985f8737 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -530,7 +530,7 @@ def request_otp(self, on_submit): def submitOtp(self, otp): def submit_otp_task(): self._otp_on_submit(otp) - threading.Thread(target=submit_otp_task).start() + threading.Thread(target=submit_otp_task, daemon=True).start() def broadcast(self, tx): assert tx.is_complete() @@ -550,7 +550,7 @@ def broadcast_thread(): self.broadcastSucceeded.emit(tx.txid()) self.historyModel.requestRefresh.emit() # via qt thread - threading.Thread(target=broadcast_thread).start() + threading.Thread(target=broadcast_thread, daemon=True).start() #TODO: properly catch server side errors, e.g. bad-txns-inputs-missingorspent @@ -576,7 +576,7 @@ def pay_thread(): except Exception as e: self.paymentFailed.emit(invoice.get_id(), repr(e)) - threading.Thread(target=pay_thread).start() + threading.Thread(target=pay_thread, daemon=True).start() def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, ignore_gap: bool = False, reuse_address: bool = False) -> Optional[Tuple]: addr = self.wallet.get_unused_address() From 6d67f51e44eefe90f6620eda026f56504e718bfc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 11:29:07 +0100 Subject: [PATCH 0400/1143] qml: show fiat state only for non-lightning wallets --- electrum/gui/qml/components/controls/BalanceSummary.qml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index e79631e66..7ded4952f 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -150,7 +150,7 @@ Item { MouseArea { anchors.fill: parent onClicked: { - root.state = root.state == 'fiat' ? 'btc' : 'fiat' + root.state = root.state == 'fiat' && Daemon.currentWallet.isLightning ? 'btc' : 'fiat' } } @@ -164,7 +164,11 @@ Item { Connections { target: Daemon - function onWalletLoaded() { setBalances() } + function onWalletLoaded() { + setBalances() + if (!Daemon.currentWallet.isLightning) + root.state = 'fiat' + } } Connections { From 46d5fdbc860fab8c5ebd53ac6bf0826bb522b8d6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 11:45:28 +0100 Subject: [PATCH 0401/1143] qml: PIN protect wallet seed display --- electrum/gui/qml/components/WalletDetails.qml | 12 +++++++++-- electrum/gui/qml/qewallet.py | 21 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 80acb6227..7e5f60c56 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -259,8 +259,12 @@ Pane { MouseArea { anchors.fill: parent onClicked: { - seedText.visible = true - showSeedText.visible = false + if (showSeedText.visible) { + Daemon.currentWallet.requestShowSeed() + } else { + seedText.visible = false + showSeedText.visible = true + } } } } @@ -590,6 +594,10 @@ Pane { function onBalanceChanged() { piechart.updateSlices() } + function onSeedRetrieved() { + seedText.visible = true + showSeedText.visible = false + } } Component { diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 0985f8737..52784947b 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -71,6 +71,7 @@ def getInstanceFor(cls, wallet): otpSuccess = pyqtSignal() otpFailed = pyqtSignal([str,str], arguments=['code','message']) peersUpdated = pyqtSignal() + seedRetrieved = pyqtSignal() _network_signal = pyqtSignal(str, object) @@ -97,6 +98,7 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self._lightningcanreceive = QEAmount() self._lightningcansend = QEAmount() + self._seed = '' self.tx_notification_queue = queue.Queue() self.tx_notification_last_time = 0 @@ -330,10 +332,7 @@ def hasSeed(self): @pyqtProperty(str, notify=dataChanged) def seed(self): - try: - return self.wallet.get_seed(self.password) - except: - return '' + return self._seed @pyqtProperty(str, notify=dataChanged) def txinType(self): @@ -736,3 +735,17 @@ def isValidChannelBackup(self, backup_str): return True except Exception as e: return False + + @pyqtSlot() + def requestShowSeed(self): + self.retrieve_seed() + + @auth_protect + def retrieve_seed(self): + try: + self._seed = self.wallet.get_seed(self.password) + self.seedRetrieved.emit() + except: + self._seed = '' + + self.dataChanged.emit() From b59a1410a2beb42a5a320f8902d21af325d7e5d5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 11:57:35 +0100 Subject: [PATCH 0402/1143] qml: fix network histogram gradient fee range to 600-1 --- electrum/gui/qml/components/NetworkOverview.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index f8ba8a6cc..8096c0001 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -98,7 +98,7 @@ Pane { Layout.preferredWidth: 300 * (modelData[1] / Network.feeHistogram.total) Layout.fillWidth: true height: parent.height - color: Qt.hsva(2/3-(2/3*(Math.log(modelData[0])/Math.log(Math.max(25, Network.feeHistogram.max_fee)))), 0.8, 1, 1) + color: Qt.hsva(2/3-(2/3*(Math.log(Math.min(600, modelData[0]))/Math.log(600))), 0.8, 1, 1) } } } From f0d44d06817dcd78d948e754704bf8816cd7505e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 12:09:57 +0100 Subject: [PATCH 0403/1143] qml: expand clickable area to full toolbar height and a bit more padding for right-side menu --- electrum/gui/qml/components/main.qml | 82 +++++++++++++++------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index adc334025..dbf6ec4b9 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -134,7 +134,6 @@ ApplicationWindow font.pixelSize: constants.fontSizeMedium font.bold: true MouseArea { - // height: toolbarTopLayout.height anchors.fill: parent onClicked: { stack.getRoot().menu.open() @@ -144,48 +143,57 @@ ApplicationWindow } Item { - visible: Network.isTestNet - width: column.width - height: column.height + implicitHeight: 48 + implicitWidth: statusIconsLayout.width - ColumnLayout { - id: column - spacing: 0 - Image { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - source: "../../icons/info.png" - } - - Label { - id: networkNameLabel - text: Network.networkName - color: Material.accentColor - font.pixelSize: constants.fontSizeXSmall - } - } - } - - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - visible: Daemon.currentWallet && Daemon.currentWallet.isWatchOnly - source: '../../icons/eye1.png' - scale: 1.5 - } - - LightningNetworkStatusIndicator { MouseArea { anchors.fill: parent onClicked: openAppMenu() } - } - OnchainNetworkStatusIndicator { - MouseArea { - anchors.fill: parent - onClicked: openAppMenu() + RowLayout { + id: statusIconsLayout + anchors.verticalCenter: parent.verticalCenter + + Item { + Layout.preferredWidth: constants.paddingLarge + Layout.preferredHeight: 1 + } + + Item { + visible: Network.isTestNet + width: column.width + height: column.height + + ColumnLayout { + id: column + spacing: 0 + Image { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + source: "../../icons/info.png" + } + + Label { + id: networkNameLabel + text: Network.networkName + color: Material.accentColor + font.pixelSize: constants.fontSizeXSmall + } + } + } + + Image { + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + visible: Daemon.currentWallet && Daemon.currentWallet.isWatchOnly + source: '../../icons/eye1.png' + scale: 1.5 + } + + LightningNetworkStatusIndicator {} + OnchainNetworkStatusIndicator {} } } } From 5b8fdacac910a411efc40a203226664360256bd5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 12:13:14 +0100 Subject: [PATCH 0404/1143] qml: ensure that the slider neutral position is in the middle of the screen --- electrum/gui/qml/components/SwapDialog.qml | 5 ++-- electrum/gui/qml/qeswaphelper.py | 35 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index f421924e0..094d692c2 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -159,8 +159,9 @@ ElDialog { id: swapslider Layout.topMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingLarge - Layout.leftMargin: constants.paddingXXLarge - Layout.rightMargin: constants.paddingXXLarge + Layout.leftMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid + Layout.rightMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid + Layout.fillWidth: true from: swaphelper.rangeMin diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 34282809d..ecaa442af 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -45,6 +45,9 @@ def __init__(self, parent=None): self._send_amount = 0 self._receive_amount = 0 + self._leftVoid = 0 + self._rightVoid = 0 + walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) def wallet(self): @@ -91,6 +94,28 @@ def rangeMax(self, rangeMax): self._rangeMax = rangeMax self.rangeMaxChanged.emit() + leftVoidChanged = pyqtSignal() + @pyqtProperty(float, notify=leftVoidChanged) + def leftVoid(self): + return self._leftVoid + + @leftVoid.setter + def leftVoid(self, leftVoid): + if self._leftVoid != leftVoid: + self._leftVoid = leftVoid + self.leftVoidChanged.emit() + + rightVoidChanged = pyqtSignal() + @pyqtProperty(float, notify=rightVoidChanged) + def rightVoid(self): + return self._rightVoid + + @rightVoid.setter + def rightVoid(self, rightVoid): + if self._rightVoid != rightVoid: + self._rightVoid = rightVoid + self.rightVoidChanged.emit() + validChanged = pyqtSignal() @pyqtProperty(bool, notify=validChanged) def valid(self): @@ -215,6 +240,16 @@ def init_swap_slider_range(self): self._logger.debug(f'Slider range {-reverse} - {forward}') self.rangeMin = -reverse self.rangeMax = forward + # percentage of void, right or left + if reverse < forward: + self.leftVoid = 0.5 * (forward - reverse) / forward + self.rightVoid = 0 + elif reverse > forward: + self.leftVoid = 0 + self.rightVoid = - 0.5 * (forward - reverse) / reverse + else: + self.leftVoid = 0 + self.rightVoid = 0 self.swap_slider_moved() From 64dde8bc0bde63f8b0f32fafb7607e121aeb9634 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 12:19:18 +0100 Subject: [PATCH 0405/1143] qml: show disconnected state in BalanceSummary --- .../gui/qml/components/controls/BalanceSummary.qml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 7ded4952f..c27bc2d57 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -42,7 +42,7 @@ Item { GridLayout { id: balanceLayout columns: 3 - opacity: Daemon.currentWallet.synchronizing ? 0 : 1 + opacity: Daemon.currentWallet.synchronizing || Network.status == 'disconnected' ? 0 : 1 Label { font.pixelSize: constants.fontSizeXLarge @@ -140,13 +140,21 @@ Item { } Label { - opacity: Daemon.currentWallet.synchronizing ? 1 : 0 + opacity: Daemon.currentWallet.synchronizing && Network.status != 'disconnected' ? 1 : 0 anchors.centerIn: balancePane text: Daemon.currentWallet.synchronizingProgress color: Material.accentColor font.pixelSize: constants.fontSizeLarge } + Label { + opacity: Network.status == 'disconnected' ? 1 : 0 + anchors.centerIn: balancePane + text: qsTr('Disconnected') + color: Material.accentColor + font.pixelSize: constants.fontSizeLarge + } + MouseArea { anchors.fill: parent onClicked: { From 3334b2f731cc98237535ceda0b8d24eefa11572a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 12:28:36 +0100 Subject: [PATCH 0406/1143] follow-up qml slider: remove unneeded setters --- electrum/gui/qml/qeswaphelper.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index ecaa442af..83ea5bcb8 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -99,23 +99,11 @@ def rangeMax(self, rangeMax): def leftVoid(self): return self._leftVoid - @leftVoid.setter - def leftVoid(self, leftVoid): - if self._leftVoid != leftVoid: - self._leftVoid = leftVoid - self.leftVoidChanged.emit() - rightVoidChanged = pyqtSignal() @pyqtProperty(float, notify=rightVoidChanged) def rightVoid(self): return self._rightVoid - @rightVoid.setter - def rightVoid(self, rightVoid): - if self._rightVoid != rightVoid: - self._rightVoid = rightVoid - self.rightVoidChanged.emit() - validChanged = pyqtSignal() @pyqtProperty(bool, notify=validChanged) def valid(self): @@ -242,14 +230,16 @@ def init_swap_slider_range(self): self.rangeMax = forward # percentage of void, right or left if reverse < forward: - self.leftVoid = 0.5 * (forward - reverse) / forward - self.rightVoid = 0 + self._leftVoid = 0.5 * (forward - reverse) / forward + self._rightVoid = 0 elif reverse > forward: - self.leftVoid = 0 - self.rightVoid = - 0.5 * (forward - reverse) / reverse + self._leftVoid = 0 + self._rightVoid = - 0.5 * (forward - reverse) / reverse else: - self.leftVoid = 0 - self.rightVoid = 0 + self._leftVoid = 0 + self._rightVoid = 0 + self.leftVoidChanged.emit() + self.rightVoidChanged.emit() self.swap_slider_moved() From e2867b7fe8415dfeb4cc23a8d113097be2a6c01a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 13:02:49 +0100 Subject: [PATCH 0407/1143] qml: move Pay button to the right the default action should always be to the right side, because right-handed people are dominating the world :-). --- electrum/gui/qml/components/InvoiceDialog.qml | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 5f72de80a..b0a012147 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -409,19 +409,6 @@ ElDialog { ButtonContainer { Layout.fillWidth: true - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Pay') - icon.source: '../../icons/confirmed.png' - enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay && !amountContainer.editmode - onClicked: { - if (invoice_key == '') // save invoice if not retrieved from key - invoice.save_invoice() - dialog.close() - doPay() // only signal here - } - } FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -433,7 +420,6 @@ ElDialog { dialog.close() } } - FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -447,6 +433,19 @@ ElDialog { dialog.close() } } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Pay') + icon.source: '../../icons/confirmed.png' + enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay && !amountContainer.editmode + onClicked: { + if (invoice_key == '') // save invoice if not retrieved from key + invoice.save_invoice() + dialog.close() + doPay() // only signal here + } + } } } From f49ef14de8ee6441b0510a1a4883696a0c42c0d8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 14:14:46 +0100 Subject: [PATCH 0408/1143] qml: SwapDialog slider styling; add zero tick and fill slider range from zero --- electrum/gui/qml/components/SwapDialog.qml | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 094d692c2..208f0661a 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -164,6 +164,42 @@ ElDialog { Layout.fillWidth: true + background: Rectangle { + x: swapslider.leftPadding + y: swapslider.topPadding + swapslider.availableHeight / 2 - height / 2 + implicitWidth: 200 + implicitHeight: 4 + width: swapslider.availableWidth + height: implicitHeight + radius: 2 + color: Color.transparent(control.Material.accentColor, 0.33) + + // full width somehow misaligns with handle, define rangeWidth + property int rangeWidth: width - swapslider.leftPadding + + Rectangle { + x: swapslider.visualPosition > swapslider.scenter + ? swapslider.scenter * parent.rangeWidth + : swapslider.visualPosition * parent.rangeWidth + width: swapslider.visualPosition > swapslider.scenter + ? (swapslider.visualPosition-swapslider.scenter) * parent.rangeWidth + : (swapslider.scenter-swapslider.visualPosition) * parent.rangeWidth + height: parent.height + color: Material.accentColor + radius: 2 + } + + Rectangle { + x: swapslider.scenter * parent.rangeWidth + y: -4 + width: 1 + height: parent.height + 2*4 + color: parent.color + } + } + + property real scenter: -swapslider.from/(swapslider.to-swapslider.from) + from: swaphelper.rangeMin to: swaphelper.rangeMax From 67cb08a8355091bf075bcb0051cd6c15f1969970 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 15:03:05 +0100 Subject: [PATCH 0409/1143] qml: slider render voids --- electrum/gui/qml/components/SwapDialog.qml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 208f0661a..213a134d2 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -172,7 +172,7 @@ ElDialog { width: swapslider.availableWidth height: implicitHeight radius: 2 - color: Color.transparent(control.Material.accentColor, 0.33) + color: Color.transparent(Material.accentColor, 0.33) // full width somehow misaligns with handle, define rangeWidth property int rangeWidth: width - swapslider.leftPadding @@ -189,6 +189,15 @@ ElDialog { radius: 2 } + Rectangle { + x: - (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid + z: -1 + // width makes rectangle go outside the control, into the Layout margins + width: parent.width + (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid + height: parent.height + color: Material.sliderDisabledColor + } + Rectangle { x: swapslider.scenter * parent.rangeWidth y: -4 From d985c9eecc7f34bddc4402be26dd3de6f41ae17c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 15:10:42 +0100 Subject: [PATCH 0410/1143] qml: use InfoTextArea for help text in GenericShareDialog --- electrum/gui/qml/components/GenericShareDialog.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index 734c597ef..edab35c96 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -57,6 +57,7 @@ ElDialog { Layout.leftMargin: constants.paddingMedium Layout.rightMargin: constants.paddingMedium Layout.fillWidth: true + visible: dialog.text Label { width: parent.width text: dialog.text @@ -68,15 +69,13 @@ ElDialog { } } - Label { + InfoTextArea { Layout.leftMargin: constants.paddingMedium Layout.rightMargin: constants.paddingMedium visible: dialog.text_help text: dialog.text_help - wrapMode: Text.Wrap Layout.fillWidth: true } - } } From 39eaf9d871d8b7e8989b84dddcc50faf90f8c3fb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 15:12:51 +0100 Subject: [PATCH 0411/1143] qml: sharing channel backup only shows QR, not the data as text --- electrum/gui/qml/components/ChannelDetails.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index fcd69b8f1..9b52680c8 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -216,7 +216,7 @@ Pane { onClicked: { var dialog = app.genericShareDialog.createObject(root, { title: qsTr('Channel Backup for %1').arg(channeldetails.short_cid), - text: channeldetails.channelBackup(), + text_qr: channeldetails.channelBackup(), text_help: channeldetails.channelBackupHelpText(), iconSource: Qt.resolvedUrl('../../icons/file.png') }) From 7a86d8dc9e1114ed368a78ff2aa97901f3ab72c6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 16 Mar 2023 15:42:24 +0100 Subject: [PATCH 0412/1143] qml: ask user whether to configure Tor or other proxy before presenting proxy detail config screen --- .../gui/qml/components/wizard/WCProxyAsk.qml | 43 +++++++++++++++++++ .../qml/components/wizard/WCProxyConfig.qml | 3 +- electrum/gui/qml/qewizard.py | 1 + electrum/wizard.py | 5 ++- 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 electrum/gui/qml/components/wizard/WCProxyAsk.qml diff --git a/electrum/gui/qml/components/wizard/WCProxyAsk.qml b/electrum/gui/qml/components/wizard/WCProxyAsk.qml new file mode 100644 index 000000000..02dd9d3bc --- /dev/null +++ b/electrum/gui/qml/components/wizard/WCProxyAsk.qml @@ -0,0 +1,43 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.0 + +import "../controls" + +WizardComponent { + valid: true + + function apply() { + wizard_data['want_proxy'] = wantproxygroup.checkedButton.wantproxy + } + + ColumnLayout { + width: parent.width + + Label { + Layout.fillWidth: true + text: qsTr('Do you want to connect through TOR, or through another service to reach to the internet?') + wrapMode: Text.Wrap + } + + ButtonGroup { + id: wantproxygroup + onCheckedButtonChanged: checkIsLast() + } + + RadioButton { + ButtonGroup.group: wantproxygroup + property bool wantproxy: true + text: qsTr('Yes') + } + RadioButton { + ButtonGroup.group: wantproxygroup + property bool wantproxy: false + text: qsTr('No') + checked: true + } + + } + +} diff --git a/electrum/gui/qml/components/wizard/WCProxyConfig.qml b/electrum/gui/qml/components/wizard/WCProxyConfig.qml index 6a4018ff8..94b7c6ec4 100644 --- a/electrum/gui/qml/components/wizard/WCProxyConfig.qml +++ b/electrum/gui/qml/components/wizard/WCProxyConfig.qml @@ -21,7 +21,8 @@ WizardComponent { ProxyConfig { id: pc - width: parent.width + Layout.fillWidth: true + proxy_enabled: true } } } diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index f00217327..77f09e459 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -122,6 +122,7 @@ def __init__(self, daemon, parent = None): # attach view names self.navmap_merge({ 'autoconnect': { 'gui': 'WCAutoConnect' }, + 'proxy_ask': { 'gui': 'WCProxyAsk' }, 'proxy_config': { 'gui': 'WCProxyConfig' }, 'server_config': { 'gui': 'WCServerConfig' }, }) diff --git a/electrum/wizard.py b/electrum/wizard.py index e37ec185e..9e8358982 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -435,9 +435,12 @@ class ServerConnectWizard(AbstractWizard): def __init__(self, daemon): self.navmap = { 'autoconnect': { - 'next': 'proxy_config', + 'next': 'proxy_ask', 'last': lambda v,d: d['autoconnect'] }, + 'proxy_ask': { + 'next': lambda d: 'proxy_config' if d['want_proxy'] else 'server_config' + }, 'proxy_config': { 'next': 'server_config' }, From 3a7bc82881ac06b2802a69c766da3f013f8df310 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 16 Mar 2023 15:22:10 +0000 Subject: [PATCH 0413/1143] icons: add "cloud_yes.png", and rename existing "nocloud" --- electrum/gui/icons/{nocloud.png => cloud_no.png} | Bin electrum/gui/icons/cloud_yes.png | Bin 0 -> 8329 bytes .../qml/components/controls/ChannelDelegate.qml | 2 +- electrum/gui/qt/channels_list.py | 2 +- electrum/gui/qt/main_window.py | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) rename electrum/gui/icons/{nocloud.png => cloud_no.png} (100%) create mode 100644 electrum/gui/icons/cloud_yes.png diff --git a/electrum/gui/icons/nocloud.png b/electrum/gui/icons/cloud_no.png similarity index 100% rename from electrum/gui/icons/nocloud.png rename to electrum/gui/icons/cloud_no.png diff --git a/electrum/gui/icons/cloud_yes.png b/electrum/gui/icons/cloud_yes.png new file mode 100644 index 0000000000000000000000000000000000000000..dd30292affbf5799aac3eac1a005530396c956f1 GIT binary patch literal 8329 zcmd6N`8(8o^gcpQM0SaykS!EZwkRQFi&+dqO`#0QGS;j;l_kkCWDiNqm@yc#jh2zE zEXCN0>@v12BaP+znx4=1x6dE&ab0t{u6e(`=e*9j&wbzLj5u3sGd`XpJZx-ieCAg! zUt?oqhqGS)aD#Wmyrzu6uLJk4IQp}(@d~qE>}**%huPR<*vv0qbO_E_%#HK;HXAbQ zdh`{)i;z-Ey*Yo9gfb?Js@C%{;f+bodG9QwSMpikkvBipXt%Sx6VLO>rKFTP$(h8R za63A6FiB$AJKg+2@S=D0#!1ZZ*&(l-zB{?G#@7mi5BhYKw$%_EpBeuWF#na6ImV;UKZp4!Tlfjfxg|3+!x2yb=~fU{oEwi=V%Y!+-Py0+~}JRyMAn4 z@>p?0`>#YgX7BS6(cV9LDu8?JNPf&r`yHO$mlaAYAE{^eQy@K3w^hAX&xvX8_vFpm z*}s;(FA~}#@;kt#XjvB8N({Wat6zS#9{dCAV8k()SQbX1` zT%r45yx982+I`{RIgzuUpPTMAZJ%r|d2wc?Ddywb;+R)&U8m3eF}&d$x_M-0>>sx4 zcQm{Y?9_ag2z{rWXOvsuI+*iLPaCG_<-7 zS!pG^P^^z~C{m@p@I>VvPMF*cUY^mtZ&-1@v0I$RRr||>`?}vM`n{x=>bc$OHqk#9?K-a& z$HP4mFnJ{AD0#4nZ)k&ZNMdL2Fx$i@$}7o{);&_$YxzMBchUuYvRXPkL%WWD*-2+( z)I%D_YxrJTH|nE^_TvliDW2WLa`|9OC`rwg;1`d%L}2{-WqsAUsa9cNNp;kFvqWMi zh>gweWn0MPhnjR-bh1jYjWmfnO@RI&_2%Xyzr|6%%e*hnLt#IPYSMoMwHDV4kvi)A ziVNYhu{EQTZROH>^_zu}xZX$wwu*yZgdXCMa{(+v9apHO20d|C{AAkt7f1!>5b?ek-oyXlHPU+`Lf z{l1;0;_Fm+QA4M43B1H(Vb{66R61;Uu33pKwy`SWRhwx25p|O17fOm9-aZw1POOWf z|7UnK)Kx8Vip|tu|C>uhNfp`)CF+r6bPiQF?Cw&zqyd#*)u{2G#{&Q#v+>RaQFs+t zET$h7e%*A>n;1TICgK(wY1bvH9bNv*sZK+Vq;@`xZtAmGcQ)|(SzdvCUIfYRVdnw? zlvW13gfnbw_sKRhuTwT@O?F=_hjhS0jReG{J$qBqJOYii#~0aG&3`LxG{cr31`|NT;587P-=QslUB}CWvOZ@b3T(3dxpkcR-)YXA=R!wJ`hx%HH#Fvv?rZW1T5y7v#s}f5wf8-pg{K-v=f?IK z^(lsKxibQyjYd=0$WRF(y79SvP?>>oovO9s0@#}!F2r@;hZ^359?{Ge&pZb9j19XT zye=Lzo;l-vk6TfsBpdGimw794J%^z&-ZTO79)Y`vN|YOH*D(x;$v>~yPN5aZ;WU32 z&28^X{}hL1VEs_JImPyeaqsL)4zp!4LugJBd8+>UVQ5sAZGtTVl~&~%jiVGj%b z+PFaTo&@OxZ7?1qT8im9_K8T!dcG%KT7B8FJS-@&ol>Yuq}DyhO6@ zgS{73@XyXdv90z-eNX~ah32UFJEzxBmF)(Gn_m`M6_0T+Ch@QvdQJX9`C@{v@~ALqLdi+A7=0V^*ens%-rvx_?ZURG0DaM6rQOB^lNWO}m7ePyi5le^2Eb}+A_OkyxU zR3v?a-6?U3CxWpJ3$&< z8_lVX20uFRKO@zeXg7_|olc0Eaa~lt&Cdx@InYD7W=Jyim{uN+!`$;Fb!kfUCl5it z8$R=`*^`v;;NCq&UpG2ge@2JPD%$4+G}nSdBLM?OWlKS$$M z@y_k1pm$w=D$?Yes!UDrJBOakGQcCpto7(ya*{k**LtSZP4Kahat8JZ>L~mXoH={- zPv$Pe8?vKr`@Mf*D)5HA%nM6%nPYMNpB7c9N5V;WvsH)g4QEoWheBMfLQOWUi+0#> za?NLKOCG@8n&e(2z}>#PL3F@03Mx^d(qagP9 zRd4P&c^bB{%G4PD)Bp0wCa$`Svf4LKP&L)tmwxeL(H_LJyqYM0%RyO1+E$ks+`Lm_ z5GmjBo-UX>HKq0E!nK~c=8~CBjyK_MHPreP1g8pYRrc$rq_ zayOy<^2O9WtzlSbKf(=J2f(d@tg}eUg)6wzf0k|qemU?3KQ5xp<;`Wa+t7z_6U5~; z;6rPP6RObe4^&+PY?CpFW0%nT^o$m`h1;`ywcPnQl$uqu-KU#>YuUSe0Vn9TEO&VE z$k$}_A-CyszelmtS%p*qq)|_~%sk|AaH_ zRvuptSC~RbWcGFBji!AyXcmOEKgWibkN>-EI`-mV92TJh$~GUD39ofvSX+6zEjKtR zhc@4?j;vN7_U?t~Knb6=n%F$c>(e-=tng09p3(ap*1uVn&lz}CKd3(0+`DH$?XbHz z4EtD3{DP99J-kLYer4C!v=Q5@_Qg+2rTJZu&BGKUeIXpa0S}UJ6_1G3%0UQbrp1pg z$7V73O!drFR*!5?S0-Q?qGntWF}kuP$2dFzLrdG-{jj8Iko|;_a_rv^XmArU7DAya z(>p{LRjgVNXJp&cHz}ER+G)3{^U@TW-!Wfi)e>(-@Vt;SDu-B;?mEUe%YGHMR$AUN z)ECDKSw3iXQ6>2~*tP;PCEFY!jpnDV=H2Tk&4X=G7I;GNYTH*_9*iZx`&oUd;mY(x z>$m8Jdp%Ch1?5M&~ToN6eAyfVfwaz zC9^v*FkKhKe2U-o(4tD9quUOkI2`5mU2c6vnp?Zp>aj4DUk;o0rZ~$3r}+0KLkAGk zh`KMua#8T_S* zPPGZ#yJ~wGez_uWY_pVHmdh%A%BGUhBm-h1Mb*8TVjWmZRpKo(OGnF`8CVL>Pl@tG zVZ$;+NoDG^wAIH68_`=Sy=ti``pm(S+pczKh&kSGp|T?f9yX~&i!;~v-7#J59pxwr zKj|8Z36}h9i^gK6Ei$MUOp9?WZWWuL^>b=nE^lF1y!VKU!l#Z@xA6@z=Af`x!)D8` zpn=B*e70y7g{&cIt8=j%(MqE3Cz?+-z5_nG8{Fem7wBsHXql@m)Z8e8g(M3OrC;Lc zTh;;183BczCj%6^mm*IMjN{tLVUOLm1QLKY&mf^sdCKzPsTn5*N=sr0kzZEBxR@?S zEGmJ6o*iz2Z73aRI|a4ii0yw<64iGY?_6$*H&-CqT%CxckAGGm?42v%tz+1xw~XuE z^$l)a7X(cJ5mq5Xx1fZSNjVy`YU}aEnZ zKO;)t@ra?1Cn^vOkH1vg|HN?rN)y{1AWnB&kAs7npAn(&3XK zSB`}zjvlntB!wQ_YE^Lza!j2_gU?#Sf+tC6X<>~slx|NF^X@gF(`KmRa1*Ao$qiDM z`T(Ic`DBaxIgIxwa#*$i4y1cq4Wws780{wlBS2FSvzdn^lm}vFdXZv}H(Se=D$9B) zg|v7$B!Xwo&JLd}5{B=*^W1rD{^JGkd~N|8LIqYY2RbK>Q|P7wtxamNo?MC#5BfHp z<+RLPKsj;vu}n!Uq)BtInn-9pOX^CYSKmsaLI?mKNg($ly?;7H$FM8-$IvRrvjG^kT%t? z)j_HNxaXN7GW3rAnZt1y)A${Cuj%=fT`p9=4-3}u@M)(yi!Vm_ zbpi%5`ZLpq01n~^1C{_}WEf^XpMVMFcV&Kk-KkaEwI?aou*_}7C5W4sj}Gob=qL|* z-)1VGMCInqMY!Q!yz?QQZ+!5LCHU7sM<^R|$r0Q$FdL}EPs!-oYaO9q3Qr7dr}w5w zH4Lb3s^Nxh*eeOnc4&E0S)(6WX&9BeakXdbssm@X{c<=LsiQ(73J$CBbL(O;BBXF0 zm1{wQWkXeA3E!D+I>NY2C}CabKTHD)Gc)NcK<`D5^O+GI3O{u|3hxi`vCQ4Zl*6O7v`M-$pBHP;Tmn&tBF$r=-NBf`ti2@HmRj;TIIB-?{Deua_NQ#@M%|; zOtSdJ8C@Jsc05V4wthd}^=^8ItktMCaDibTE(8Lhg5i$k6)aKT15O2^a@nhcu16tJh0du47q}0<-vXAvHG|Sb^3?waek;06DUWM_z2zTp3 zLXkq~!bhlFAuq?-U-GYDJ_d^_qEr^F&A9r-S#6pvB1jL2S-g+2t8;fhYL`ny02Rk!)p$fl~|U{2`rSQ*LBgvI1JDlj=3eICnMC_^*r zIGOj)dpbXIXT8zy`{d!FOr&8TFK`}(;clj+j;79fJ3o;44fVkLZo9Pu^`sB1t^)ZM z#({~oJ{I#KxgP*hS9!K=qrGzFPtG6Gg{?e}+pb_d+fp*>MnZFkVF&Dl>9=B3>9_N; zT^5TXftg5J=IWso0_7pLAo2EVs8+N;S6Ijbp9GBXhE^6~9#s@GuK>**fgR`-qNiQg zi#%&dXi!gF>Y(U{9{LOA`D)@5VLHY>R_pjRDL4%u>3cun#Rmh+iZJ!0`z=?2HI!%f zEUz3uC>`)}0Gkp2*I%pfv+Po4M>~s_{T*z(Su+m> ztAZ55Suf(u(zm+mqu|R5gFR1X*_ZQsC=;)*H(FOyC%;Am6NruxMCCqzy7I^P=yG@P zvBNvkeTYe5gbaG$v&f58Wl1e_d_pJrW+*>-EO%>n{><7Y$F0Bg3&jRGo>Ly6#K!j> z&B6I}&fBgSQzTfZFjbS0=T`OV%fTp)STelc!brqm0INTQ6P1(U(m7iD_0$p;5{mY? zNFV()jb>P`uSy=UZtfXz`tXBfNB^Gi%L|nAemMZJiPm^`0y(Qr?wO1eQ<(>a466wD z`{pVXruK9v3N2t|;Zr92->L_ynZbEp`E>;9`@PGN zBxi1caNl)>G)@8BA5{NZ3Y!_1-Xsu8kn;Tfqr&;TL`m@l(P5azU-Q9|HLNh& zL#jnrr+6vfs`?GX`N&dlDNT3JKkCyM?+;*t8t{Is2FU$V?yPEwPpx`Y{axqyAX5BM ztAGkh%f~#5A-afrdOIG2x?IX6Dsbtos)2^TieJs(yd{RKZ@{}Z)9PKisJiIO=B-~- z^OdD2Dj!(hl3F&?fptMa7G7UJuZ>!Sli&)ksNt|QjCY*eV1n#22o1!k0Ig#UhoHdL zA7F%ZIoi*yt-jLd-^Cw2s6Po(TSj1+`Qg$+7G@K^#x$MRcmijg%d>ojK&~OW;ER{y z1Y5i#HRM6iX1cds+kcCIe%t_vr-;k6s-YjRqY|%<82s8$(JSF>SSucvZm;RIDDOW- zh8hGp3W7W$8#wCXQ-0k^5IOh?oUcBU3KkcvjuJr!zzDYhFyrGee(@vLT|x^4USqyk zhyv)?;&8Y0qL`zru^YQ-^@iGd?_3T?BqM-SkbykvbG6lX#>4d{)M;&NZz~j;ts7Os z3zo*NX_H~>Qle=$Ga%kz=hs;Vwsh(TswHJ;%-+R2e{EL|I(78WG!qmuKJnB20ha--BE~{qEQd zt53tJ%A0B`sKp*LeDeCA9j@skf(t(s925`SbFkeh`H!)205=*6G{#)q#;(+D=7cd< z_R?YVQLs(nM`_iwrYkc*x%0L@)1K{LwG}(+wJ$hS2rJbDt|(N>L)ZEc|3MRH+JKku z%$qP_eMXP(Q=TL0P++kzAw16w$-q6Ak_lkhH|cLI_1o>wH7+LS0D@&2c33h=M`#Jt zKj|o>Uv&)LVGI}8f!#+n@sqx4=aIYdDXvTK`s)t`M$nHJ{ticE0QNEk z8vnxQrG#!dP&7($O^9c|d5L%C6UT&y!q$SmE6% z;i5_B{_|(JZE#RIT8dKLsAvl;!i*ZTWq%5VwZa2S_45SnH&W=CBffsIvOYss^aON4 zB~0OXm3u|P-)Y!WjWq8hbWdHOY1bjGDOCn#4&8V=i(xGY{>Qud3N7&2eRCK7CA0SJ z?WQ0uOP(DN!Ue85QfCa#_aTtYU>#8t^9=-5eL}zENxrTbXVc0Xk!uKx!GoJ}PYH zn^5sm$-f7u2C97q0|S4!f{AgB<%L;y;Z8dfyn`;2n7o%x*5uMv+s=Ao;^3v5fjF9uR9%0VyZvX0Ew;u84F9}?7M=m zT&nC~id=t{64iymnC)J~QFr7EjyQETmHzbj6X`k1=%1&0&ZK~?p#-%ol-berTcp8w z!ic%d3mJ0xn$JqT_{@Js(gW2$^I$;UE*af?{@(PN0^K_vp@GG@zuw$&O$IpwT8+X; z$_&=jn%rqUR=}G%%U$)QOy5VcN#gA$2b3u#15Co3a1%g8CIn(Eu_;$EKz9174I&gf z+Zqd50&oj*tP5|kkAi^8cl<=|l*1xoIOy@y?#r4IZ!r zuE|@{EBRv|#WHOy0mD1T&HY1@?-;iKcj$`VSLOokYhE@hNhr4r$Yn$F)3|G!n(sfo zgl>eNUkn(7gun<_fHO(+pN>z7V&3rEc~l_Xo*q}7)tgrRHC#0o>`_=28d@9CUmoBq zlkQc%s$aDckpFjPU$W$ogC1xJwNHDK6sytgUSx1mO9G1=0qdXeR0f!`Sf=aLqO9te z0F$_+HH@?%6_+g=-Ys8fGBr0EUFN#y@C<<~C?yZ>Y!*&+Oqw=WI97JnYv0xR?v#TV zlAJ)`2!F@Pj;5=mnx^9x#QS}>4exAtJwx=`ry_=!Y91A(WWVx$PHB`Ygi8_Pm|@{z77|g zZNsBi{_~HM&k$o-=k?p5QPF+XK37WTLQWPJ&Nh@z8*C~butjSz5GwM~{l)z?k)kc$ z`>E`;Emz9l>c7mJuX&jkl<_KUb32F8Z;`+6)~*Y1C0gOI9S^$+HL?8>j>~VK+`Ou+}=Z`psfz z;hV+DHj$ZYVlLSMg@!k`UIqFRp zsr;vIpX-CK{#-RXb!WIQbDwuUiybceGE;@kI9m4vJD!Zexc(1(&HcZGFnX-J7QC&_ k`d@(m3Yon148*EO#lD@ literal 0 HcmV?d00001 diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index 4728a0865..bbeb8e797 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -38,7 +38,7 @@ ItemDelegate { id: walleticon source: model.is_backup ? model.is_imported - ? '../../../icons/nocloud.png' + ? '../../../icons/cloud_no.png' : '../../../icons/lightning_disconnected.png' : model.is_trampoline ? '../../../icons/kangaroo.png' diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 18c9dadc3..351cdef76 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -443,7 +443,7 @@ class ChanFeatNoOnchainBackup(ChannelFeature): def tooltip(self) -> str: return _("This channel cannot be recovered from your seed. You must back it up manually.") def icon(self) -> QIcon: - return read_QIcon("nocloud") + return read_QIcon("cloud_no") class ChannelFeatureIcons: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 608dfa7b4..42e20a89c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1761,7 +1761,7 @@ def show_wallet_info(self): grid.addWidget(WWLabel(_('Enabled')), cur_row, 1) else: label = IconLabel(text='Enabled, non-recoverable channels') - label.setIcon(read_QIcon('nocloud')) + label.setIcon(read_QIcon('cloud_no')) grid.addWidget(label, cur_row, 1) if self.wallet.db.get('seed_type') == 'segwit': msg = _("Your channels cannot be recovered from seed, because they were created with an old version of Electrum. " From 7207f13e97cb3a15971488e7590e40124f606c00 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 16:48:12 +0100 Subject: [PATCH 0414/1143] Qt: set history_rates both through settings_dialog and history_list follow-up 503776c0dee2a544c0d746e9b853f29fe0b54279 --- electrum/exchange_rate.py | 5 ++++- electrum/gui/qt/history_list.py | 7 ++++--- electrum/gui/qt/settings_dialog.py | 16 ++++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index fd3280c9c..87b3996ba 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -597,9 +597,12 @@ def set_enabled(self, b): self.config.set_key('use_exchange_rate', bool(b)) self.trigger_update() - def has_history(self): + def can_have_history(self): return self.is_enabled() and self.ccy in self.exchange.history_ccys() + def has_history(self): + return self.can_have_history() and self.config.get('history_rates', False) + def get_currency(self) -> str: '''Use when dynamic fetching is needed''' return self.config.get("currency", DEFAULT_CURRENCY) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 5dc01dc37..ec3422d3f 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -562,9 +562,10 @@ def create_toolbar(self, config): def update_toolbar_menu(self): fx = self.main_window.fx - b = fx and fx.is_enabled() and fx.has_history() - self.menu_fiat.setEnabled(b) - self.menu_capgains.setEnabled(b) + self.menu_fiat.setEnabled(fx and fx.can_have_history()) + # setChecked because has_history can be modified through settings dialog + self.menu_fiat.setChecked(fx and fx.has_history()) + self.menu_capgains.setEnabled(fx and fx.has_history()) def get_toolbar_buttons(self): return self.period_combo, self.start_button, self.end_button diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 6aeaf4a59..3ae4044e4 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -300,14 +300,15 @@ def on_be_edit(): block_ex_hbox_w.setLayout(block_ex_hbox) # Fiat Currency - self.require_history_checkbox = QCheckBox() + self.history_rates_cb = QCheckBox(_('Download historical rates')) ccy_combo = QComboBox() ex_combo = QComboBox() def update_currencies(): if not self.fx: return - currencies = sorted(self.fx.get_currencies(self.require_history_checkbox.isChecked())) + h = self.config.get('history_rates', False) + currencies = sorted(self.fx.get_currencies(h)) ccy_combo.clear() ccy_combo.addItems([_('None')] + currencies) if self.fx.is_enabled(): @@ -318,7 +319,7 @@ def update_exchanges(): b = self.fx.is_enabled() ex_combo.setEnabled(b) if b: - h = self.require_history_checkbox.isChecked() + h = self.config.get('history_rates', False) c = self.fx.get_currency() exchanges = self.fx.get_exchanges_by_ccy(c, h) else: @@ -345,15 +346,18 @@ def on_exchange(idx): self.fx.set_exchange(exchange) self.app.update_fiat_signal.emit() - def on_require_history(checked): + def on_history_rates(checked): + self.config.set_key('history_rates', bool(checked)) if not self.fx: return update_exchanges() + window.app.update_fiat_signal.emit() update_currencies() update_exchanges() ccy_combo.currentIndexChanged.connect(on_currency) - self.require_history_checkbox.stateChanged.connect(on_require_history) + self.history_rates_cb.setChecked(self.config.get('history_rates', False)) + self.history_rates_cb.stateChanged.connect(on_history_rates) ex_combo.currentIndexChanged.connect(on_exchange) gui_widgets = [] @@ -371,7 +375,7 @@ def on_require_history(checked): fiat_widgets = [] fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo)) fiat_widgets.append((QLabel(_('Source')), ex_combo)) - fiat_widgets.append((QLabel(_('Show sources with historical data')), self.require_history_checkbox)) + fiat_widgets.append((self.history_rates_cb, None)) misc_widgets = [] misc_widgets.append((updatecheck_cb, None)) misc_widgets.append((filelogging_cb, None)) From 0a5d18634c8a2c2ba42c083d7e8d566d9353978d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 16 Mar 2023 16:11:02 +0000 Subject: [PATCH 0415/1143] exchange_rate: guard against garbage hist data coming from exchange See discussion at https://github.com/spesmilo/electrum/commit/583089d57b517ab001721cfc9e4d883f3d4c4f7c#r104678577 CoinGecko for PLN gives "None" str as rate (instead of null) for two months mid-2014: ``` 2.29 | D | exchange_rate.CoinGecko | found corrupted historical_rate: rate='None'. for ccy='PLN' at 2014-05-10 ``` Thanks to @lukasz1992 for reporting. --- electrum/exchange_rate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 87b3996ba..55c9edee0 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -161,8 +161,13 @@ def history_ccys(self) -> Sequence[str]: return [] def historical_rate(self, ccy: str, d_t: datetime) -> Decimal: - rate = self._history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d')) or 'NaN' - return Decimal(rate) + date_str = d_t.strftime('%Y-%m-%d') + rate = self._history.get(ccy, {}).get(date_str) or 'NaN' + try: + return Decimal(rate) + except Exception: # guard against garbage coming from exchange + #self.logger.debug(f"found corrupted historical_rate: {rate=!r}. for {ccy=} at {date_str}") + return Decimal('NaN') async def request_history(self, ccy: str) -> Dict[str, Union[str, float]]: raise NotImplementedError() # implemented by subclasses From 2ef60b906ffb75e68ab0e50c7cd5cc5321acb226 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 17:09:16 +0100 Subject: [PATCH 0416/1143] Reword proxy question. The second alternative in previous phrase can be misinterpreted as: 'Do you want to connect to the internet through an ISP?' --- electrum/gui/qml/components/wizard/WCProxyAsk.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/wizard/WCProxyAsk.qml b/electrum/gui/qml/components/wizard/WCProxyAsk.qml index 02dd9d3bc..23cf51182 100644 --- a/electrum/gui/qml/components/wizard/WCProxyAsk.qml +++ b/electrum/gui/qml/components/wizard/WCProxyAsk.qml @@ -17,7 +17,7 @@ WizardComponent { Label { Layout.fillWidth: true - text: qsTr('Do you want to connect through TOR, or through another service to reach to the internet?') + text: qsTr('Do you use a local proxy service such as TOR to reach the internet?') wrapMode: Text.Wrap } From 13a9d1e2fba3b5786a22d3a77eee358c113fe3f6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 17:24:07 +0100 Subject: [PATCH 0417/1143] Add info on how to scan channel backups --- electrum/gui/qml/qechanneldetails.py | 4 +++- electrum/gui/qt/channels_list.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 8647066c7..12afb7794 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -210,7 +210,9 @@ def channelBackup(self): @pyqtSlot(result=str) def channelBackupHelpText(self): return ' '.join([ - _("Channel backups can be imported in another instance of the same wallet, by scanning this QR code."), + _("Channel backups can be imported in another instance of the same wallet."), + _("In the Electrum mobile app, use the 'Send' button to scan this QR code."), + '\n\n', _("Please note that channel backups cannot be used to restore your channels."), _("If you lose your wallet file, the only thing you can do with a backup is to request your channel to be closed, so that your funds will be sent on-chain."), ]) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 351cdef76..0046564ef 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -173,7 +173,9 @@ def remove_channel_backup(self, channel_id): def export_channel_backup(self, channel_id): msg = ' '.join([ - _("Channel backups can be imported in another instance of the same wallet, by scanning this QR code."), + _("Channel backups can be imported in another instance of the same wallet."), + _("In the Electrum mobile app, use the 'Send' button to scan this QR code."), + '\n\n', _("Please note that channel backups cannot be used to restore your channels."), _("If you lose your wallet file, the only thing you can do with a backup is to request your channel to be closed, so that your funds will be sent on-chain."), ]) From d4d6d05d9feb6d4c729678e0616bcab8cb69d3d3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 16 Mar 2023 17:40:30 +0000 Subject: [PATCH 0418/1143] qml wizard: enable restore from "2fa" legacy seeds not sure why it was disabled, there should be no meaningful distinction at that point --- electrum/wizard.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 9e8358982..10a34f457 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -9,6 +9,8 @@ from electrum.bip32 import normalize_bip32_derivation, xpub_type from electrum import keystore from electrum import bitcoin +from electrum.mnemonic import is_any_2fa_seed_type + class WizardViewState(NamedTuple): view: str @@ -365,7 +367,7 @@ def create_storage(self, path, data): derivation = normalize_bip32_derivation(data['derivation_path']) script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard' k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script) - elif data['seed_type'] == '2fa_segwit': # TODO: legacy 2fa '2fa' + elif is_any_2fa_seed_type(data['seed_type']): self._logger.debug('creating keystore from 2fa seed') k = keystore.from_xprv(data['x1/']['xprv']) else: From 6890268b1d15ddb50305e94f68b8ed9f59113515 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 19:29:56 +0100 Subject: [PATCH 0419/1143] qml: fix display of server fee in swap dialog The previously displayed amount was not the percentage, but the mining fee plus the percentage. --- electrum/gui/qml/components/SwapDialog.qml | 4 ++-- electrum/gui/qml/qeswaphelper.py | 28 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 213a134d2..51057709b 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -119,7 +119,7 @@ ElDialog { Layout.preferredWidth: 1 Layout.fillWidth: true Label { - text: Config.formatSats(swaphelper.serverfee) + text: Config.formatSats(swaphelper.server_miningfee) font.family: FixedFont } Label { @@ -128,7 +128,7 @@ ElDialog { } Label { text: swaphelper.serverfeeperc - ? '(' + swaphelper.serverfeeperc + ')' + ? '+ ' + swaphelper.serverfeeperc : '' } } diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 83ea5bcb8..dcea0173e 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -37,7 +37,7 @@ def __init__(self, parent=None): self._tosend = QEAmount() self._toreceive = QEAmount() self._serverfeeperc = '' - self._serverfee = QEAmount() + self._server_miningfee = QEAmount() self._miningfee = QEAmount() self._isReverse = False @@ -148,16 +148,16 @@ def toreceive(self, toreceive): self._toreceive = toreceive self.toreceiveChanged.emit() - serverfeeChanged = pyqtSignal() - @pyqtProperty(QEAmount, notify=serverfeeChanged) - def serverfee(self): - return self._serverfee + server_miningfeeChanged = pyqtSignal() + @pyqtProperty(QEAmount, notify=server_miningfeeChanged) + def server_miningfee(self): + return self._server_miningfee - @serverfee.setter - def serverfee(self, serverfee): - if self._serverfee != serverfee: - self._serverfee = serverfee - self.serverfeeChanged.emit() + @server_miningfee.setter + def server_miningfee(self, server_miningfee): + if self._server_miningfee != server_miningfee: + self._server_miningfee = server_miningfee + self.server_miningfeeChanged.emit() serverfeepercChanged = pyqtSignal() @pyqtProperty(str, notify=serverfeepercChanged) @@ -285,8 +285,8 @@ def swap_slider_moved(self): # fee breakdown self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' - serverfee = math.ceil(swap_manager.percentage * pay_amount / 100) + swap_manager.lockup_fee - self.serverfee = QEAmount(amount_sat=serverfee) + server_miningfee = swap_manager.lockup_fee + self.server_miningfee = QEAmount(amount_sat=server_miningfee) self.miningfee = QEAmount(amount_sat=swap_manager.get_claim_fee()) else: # forward (normal) swap @@ -305,8 +305,8 @@ def swap_slider_moved(self): # fee breakdown self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' - serverfee = math.ceil(swap_manager.percentage * pay_amount / 100) + swap_manager.normal_fee - self.serverfee = QEAmount(amount_sat=serverfee) + server_miningfee = swap_manager.normal_fee + self.server_miningfee = QEAmount(amount_sat=server_miningfee) self.miningfee = QEAmount(amount_sat=self._tx.get_fee()) if self._tx else QEAmount() if pay_amount and receive_amount: From 57a4cbb98414c3aafe812133f3c48c78bd21ca0b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 20:07:58 +0100 Subject: [PATCH 0420/1143] follow-up 7a86d8d: ask proxy first --- electrum/wizard.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 10a34f457..28173d84c 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -437,14 +437,14 @@ class ServerConnectWizard(AbstractWizard): def __init__(self, daemon): self.navmap = { 'autoconnect': { - 'next': 'proxy_ask', + 'next': 'server_config', 'last': lambda v,d: d['autoconnect'] }, 'proxy_ask': { - 'next': lambda d: 'proxy_config' if d['want_proxy'] else 'server_config' + 'next': lambda d: 'proxy_config' if d['want_proxy'] else 'autoconnect' }, 'proxy_config': { - 'next': 'server_config' + 'next': 'autoconnect' }, 'server_config': { 'last': True @@ -454,5 +454,5 @@ def __init__(self, daemon): def start(self, initial_data = {}): self.reset() - self._current = WizardViewState('autoconnect', initial_data, {}) + self._current = WizardViewState('proxy_ask', initial_data, {}) return self._current From 849d987d0d4282f60f6cd5d6e88bbad20ab1a2fc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 20:23:29 +0100 Subject: [PATCH 0421/1143] qml: fix #8247 --- electrum/gui/qml/qedaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 16e97bcea..a1b217ab2 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -262,7 +262,7 @@ def delete_wallet(self, wallet): self._logger.debug('deleting wallet with path %s' % path) self._current_wallet = None # TODO walletLoaded signal is confusing - self.walletLoaded.emit() + self.walletLoaded.emit(None, None) if not self.daemon.delete_wallet(path): self.walletDeleteError.emit('error', _('Problem deleting wallet')) From ff2da7ceb9967ede1fc51266504265f34dce72b0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 16 Mar 2023 19:07:33 +0000 Subject: [PATCH 0422/1143] crash reporter: hardcode gui text. do not trust the server with it paranoia. --- electrum/base_crash_reporter.py | 30 +++++++++++++++++-- .../gui/kivy/uix/dialogs/crash_reporter.py | 14 ++++----- electrum/gui/qml/qeapp.py | 6 +++- electrum/gui/qt/exception_window.py | 11 +++---- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index b3441afc1..b2c95be80 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -25,6 +25,7 @@ import traceback import sys import queue +from typing import NamedTuple, Optional from .version import ELECTRUM_VERSION from . import constants @@ -33,6 +34,12 @@ from .logging import describe_os_version, Logger, get_git_version +class CrashReportResponse(NamedTuple): + status: Optional[str] + text: str + url: Optional[str] + + class BaseCrashReporter(Logger): report_server = "https://crashhub.electrum.org" config_key = "show_crash_reporter" @@ -63,16 +70,33 @@ def __init__(self, exctype, value, tb): Logger.__init__(self) self.exc_args = (exctype, value, tb) - def send_report(self, asyncio_loop, proxy, endpoint="/crash", *, timeout=None): + def send_report(self, asyncio_loop, proxy, *, timeout=None) -> CrashReportResponse: + # FIXME the caller needs to catch generic "Exception", as this method does not have a well-defined API... if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server: # Gah! Some kind of altcoin wants to send us crash reports. raise Exception(_("Missing report URL.")) report = self.get_traceback_info() report.update(self.get_additional_info()) report = json.dumps(report) - coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report) + coro = self.do_post(proxy, BaseCrashReporter.report_server + "/crash.json", data=report) response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout) - return response + self.logger.info(f"Crash report sent. Got response [DO NOT TRUST THIS MESSAGE]: {response!r}") + response = json.loads(response) + assert isinstance(response, dict), type(response) + # sanitize URL + if location := response.get("location"): + assert isinstance(location, str) + base_issues_url = constants.GIT_REPO_ISSUES_URL + if not base_issues_url.endswith("/"): + base_issues_url = base_issues_url + "/" + if not location.startswith(base_issues_url): + location = None + ret = CrashReportResponse( + status=response.get("status"), + url=location, + text=_("Thanks for reporting this issue!"), + ) + return ret async def do_post(self, proxy, url, data): async with make_aiohttp_session(proxy) as session: diff --git a/electrum/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py index 49b23b78d..f5aa0a9da 100644 --- a/electrum/gui/kivy/uix/dialogs/crash_reporter.py +++ b/electrum/gui/kivy/uix/dialogs/crash_reporter.py @@ -121,16 +121,16 @@ def send_report(self): loop = self.main_window.network.asyncio_loop proxy = self.main_window.network.proxy # FIXME network request in GUI thread... - response = json.loads(BaseCrashReporter.send_report(self, loop, proxy, - "/crash.json", timeout=10)) - except (ValueError, ClientError) as e: + response = BaseCrashReporter.send_report(self, loop, proxy, timeout=10) + except Exception as e: self.logger.warning(f"Error sending crash report. exc={e!r}") self.show_popup(_('Unable to send report'), _("Please check your network connection.")) else: - self.show_popup(_('Report sent'), response["text"]) - location = response["location"] - if location: - self.logger.info(f"Crash report sent. location={location!r}") + text = response.text + if response.url: + text += f" You can track further progress on GitHub." + self.show_popup(_('Report sent'), text) + if location := response.url: self.open_url(location) self.dismiss() diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index d151f1a27..8dc9077e3 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -247,13 +247,17 @@ def sendReport(self): def report_task(): try: response = BaseCrashReporter.send_report(self, network.asyncio_loop, proxy) - self.sendingBugreportSuccess.emit(response) except Exception as e: self.logger.error('There was a problem with the automatic reporting', exc_info=e) self.sendingBugreportFailure.emit(_('There was a problem with the automatic reporting:') + '
' + repr(e)[:120] + '

' + _("Please report this issue manually") + f' on GitHub.') + else: + text = response.text + if response.url: + text += f" You can track further progress on GitHub." + self.sendingBugreportSuccess.emit(text) self.sendingBugreport.emit() threading.Thread(target=report_task).start() diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py index 1a920aa15..d549d75d1 100644 --- a/electrum/gui/qt/exception_window.py +++ b/electrum/gui/qt/exception_window.py @@ -31,7 +31,7 @@ QMessageBox, QHBoxLayout, QVBoxLayout) from electrum.i18n import _ -from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue +from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue, CrashReportResponse from electrum.logging import Logger from electrum import constants from electrum.network import Network @@ -103,12 +103,13 @@ def __init__(self, config: 'SimpleConfig', exctype, value, tb): self.show() def send_report(self): - def on_success(response): - # note: 'response' coming from (remote) crash reporter server. - # It contains a URL to the GitHub issue, so we allow rich text. + def on_success(response: CrashReportResponse): + text = response.text + if response.url: + text += f" You can track further progress on GitHub." self.show_message(parent=self, title=_("Crash report"), - msg=response, + msg=text, rich_text=True) self.close() def on_failure(exc_info): From 3574c99275662179fece647972ecb8a534bd48e2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 20:51:10 +0100 Subject: [PATCH 0423/1143] qml: in the password dialogs, disable the password confirmation line if the first entered password is too short. Without that, a user may enter two passwords that are identical but too short, and then click on the eye icon in order to discover that they actuall are identical.. and only at this point guess that the size might be the problem. Also, raise the minimum length to 6, because that is what is was on Kivy. One of the password dialogs still had two eye icons; that was only fixed in the wizard. I guess that could be avoided if both dialogs used the same code. --- electrum/gui/qml/components/PasswordDialog.qml | 5 ++++- electrum/gui/qml/components/wizard/WCWalletPassword.qml | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index 743333e48..a15ff532f 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -67,6 +67,9 @@ ElDialog { id: pw_2 Layout.leftMargin: constants.paddingXLarge visible: confirmPassword + showReveal: false + echoMode: pw_1.echoMode + enabled: pw_1.text.length >= 6 } } @@ -74,7 +77,7 @@ ElDialog { Layout.fillWidth: true text: qsTr("Ok") icon.source: '../../icons/confirmed.png' - enabled: confirmPassword ? pw_1.text.length > 4 && pw_1.text == pw_2.text : true + enabled: confirmPassword ? pw_1.text.length >= 6 && pw_1.text == pw_2.text : true onClicked: { password = pw_1.text passworddialog.accept() diff --git a/electrum/gui/qml/components/wizard/WCWalletPassword.qml b/electrum/gui/qml/components/wizard/WCWalletPassword.qml index 3da0cec99..73a7e46c0 100644 --- a/electrum/gui/qml/components/wizard/WCWalletPassword.qml +++ b/electrum/gui/qml/components/wizard/WCWalletPassword.qml @@ -5,7 +5,7 @@ import QtQuick.Controls 2.1 import "../controls" WizardComponent { - valid: password1.text === password2.text && password1.text.length > 4 + valid: password1.text === password2.text && password1.text.length >= 6 function apply() { wizard_data['password'] = password1.text @@ -25,6 +25,7 @@ WizardComponent { id: password2 showReveal: false echoMode: password1.echoMode + enabled: password1.text.length >= 6 } } } From 3e5c69266067fef6e6e480ae75284a6f514f835f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 00:13:16 +0100 Subject: [PATCH 0424/1143] qml: don't log (potentially) sensitive data, closes #8124 --- electrum/gui/qml/qeqr.py | 2 +- electrum/gui/qml/qewizard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeqr.py b/electrum/gui/qml/qeqr.py index 0f76290bd..fdc612c8d 100644 --- a/electrum/gui/qml/qeqr.py +++ b/electrum/gui/qml/qeqr.py @@ -138,7 +138,7 @@ def requestImage(self, qstr, size): uri = uri._replace(query=query) qstr = urllib.parse.urlunparse(uri) - self._logger.debug('QR requested for %s' % qstr) + #self._logger.debug('QR requested for %s' % qstr) qr = qrcode.QRCode(version=1, border=2) qr.add_data(qstr) diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 77f09e459..0f85144d8 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -92,7 +92,7 @@ def hasDuplicateKeys(self, js_data): def createStorage(self, js_data, single_password_enabled, single_password): self._logger.info('Creating wallet from wizard data') data = js_data.toVariant() - self._logger.debug(str(data)) + #self._logger.debug(str(data)) if single_password_enabled and single_password: data['encrypt'] = True From 0bb41a32c849ee9bcdb2c3a8be90e59716068b2c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 00:23:50 +0100 Subject: [PATCH 0425/1143] qml: fix layout issues in ShowConfirmOTP. fixes #8249 --- electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml index 849111a12..f979acb8b 100644 --- a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml +++ b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml @@ -25,11 +25,13 @@ WizardComponent { InfoTextArea { id: errorBox + Layout.fillWidth: true iconStyle: InfoTextArea.IconStyle.Error visible: !otpVerified && plugin.remoteKeyState == 'error' } InfoTextArea { + Layout.fillWidth: true iconStyle: InfoTextArea.IconStyle.Warn visible: plugin.remoteKeyState == 'wallet_known' text: qsTr('This wallet is already registered with TrustedCoin. ') @@ -55,15 +57,15 @@ WizardComponent { } Label { + Layout.fillWidth: true visible: !otpVerified && plugin.otpSecret - Layout.preferredWidth: parent.width wrapMode: Text.Wrap text: qsTr('Enter or scan into authenticator app. Then authenticate below') } Label { + Layout.fillWidth: true visible: !otpVerified && plugin.remoteKeyState == 'wallet_known' - Layout.preferredWidth: parent.width wrapMode: Text.Wrap text: qsTr('If you still have your OTP secret, then authenticate below') } From 49683d6ff11cc72d74830e8133ff4f1a75ed390e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 17 Mar 2023 09:22:35 +0100 Subject: [PATCH 0426/1143] qml: do not set oneserver based on auto_connect. --- electrum/gui/qml/qenetwork.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 3e13b6f72..0211215cd 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -161,7 +161,7 @@ def server(self, server): if not server: raise Exception("failed to parse") except Exception: return - net_params = net_params._replace(server=server, auto_connect=self._qeconfig.autoConnect, oneserver=not self._qeconfig.autoConnect) + net_params = net_params._replace(server=server, auto_connect=self._qeconfig.autoConnect) self.network.run_from_another_thread(self.network.set_parameters(net_params)) @pyqtProperty(str, notify=statusChanged) From fcbd25c1fd7e0ffa76c6249dae1630448fa8810e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 17 Mar 2023 10:15:07 +0100 Subject: [PATCH 0427/1143] qml: display network status and history server status separately. Also, show network fees on full screen width --- .../gui/qml/components/NetworkOverview.qml | 37 ++++++++----------- .../OnchainNetworkStatusIndicator.qml | 2 +- electrum/gui/qml/qenetwork.py | 20 +++++++--- electrum/gui/qt/network_dialog.py | 4 +- electrum/network.py | 4 ++ 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 8096c0001..e5c13fa2c 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -32,12 +32,10 @@ Pane { id: contentLayout width: parent.width columns: 2 - Heading { Layout.columnSpan: 2 text: qsTr('On-chain') } - Label { text: qsTr('Network:'); color: Material.accentColor @@ -45,42 +43,37 @@ Pane { Label { text: Network.networkName } - - Label { - text: qsTr('Server:'); - color: Material.accentColor - } Label { - text: Network.server - } - - Label { - text: qsTr('Local Height:'); + text: qsTr('Status:'); color: Material.accentColor } Label { - text: Network.height + text: Network.status } - Label { - text: qsTr('Status:'); + text: qsTr('Server:'); color: Material.accentColor } - RowLayout { - OnchainNetworkStatusIndicator {} - Label { - text: Network.status + text: Network.server } + OnchainNetworkStatusIndicator {} } - Label { - text: qsTr('Network fees:'); + text: qsTr('Local Height:'); color: Material.accentColor } + Label { + text: Network.height + } + Heading { + Layout.columnSpan: 2 + text: qsTr('Mempool fees') + } Item { id: histogramRoot + Layout.columnSpan: 2 Layout.fillWidth: true implicitHeight: histogramLayout.height @@ -189,7 +182,7 @@ Pane { Heading { Layout.columnSpan: 2 - text: qsTr('Network') + text: qsTr('Proxy') } Label { diff --git a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml index 5e7bbf95a..690639e63 100644 --- a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml +++ b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml @@ -6,7 +6,7 @@ Image { sourceSize.width: constants.iconSizeMedium sourceSize.height: constants.iconSizeMedium - property bool connected: Network.status == 'connected' + property bool connected: Network.server_status == 'connected' property bool lagging: connected && Network.isLagging property bool fork: connected && Network.chaintips > 1 property bool syncing: connected && Daemon.currentWallet && Daemon.currentWallet.synchronizing diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 0211215cd..234451911 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -27,7 +27,8 @@ class QENetwork(QObject, QtEventListener): dataChanged = pyqtSignal() _height = 0 - _status = "" + _server_status = "" + _network_status = "" _chaintips = 1 _islagging = False _fee_histogram = [] @@ -71,9 +72,14 @@ def on_event_proxy_set(self, *args): @event_listener def on_event_status(self, *args): - self._logger.debug('status updated: %s' % self.network.connection_status) - if self._status != self.network.connection_status: - self._status = self.network.connection_status + network_status = self.network.get_status() + if self._network_status != network_status: + self._network_status = network_status + self.statusChanged.emit() + server_status = self.network.connection_status + self._logger.debug('server_status updated: %s' % server_status) + if self._server_status != server_status: + self._server_status = server_status self.statusChanged.emit() chains = len(self.network.get_blockchains()) if chains != self._chaintips: @@ -166,7 +172,11 @@ def server(self, server): @pyqtProperty(str, notify=statusChanged) def status(self): - return self._status + return self._network_status + + @pyqtProperty(str, notify=statusChanged) + def server_status(self): + return self._server_status @pyqtProperty(int, notify=chaintipsChanged) def chaintips(self): diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index 1d523f0ca..ddc78adaa 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -349,9 +349,7 @@ def update(self): height_str = "%d "%(self.network.get_local_height()) + _('blocks') self.height_label.setText(height_str) - n = len(self.network.get_interfaces()) - status = _("Connected to {0} nodes.").format(n) if n > 1 else _("Connected to {0} node.").format(n) if n == 1 else _("Not connected") - self.status_label.setText(status) + self.status_label.setText(self.network.get_status()) chains = self.network.get_blockchains() if len(chains) > 1: chain = self.network.blockchain() diff --git a/electrum/network.py b/electrum/network.py index bc2057062..e5242bc89 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -518,6 +518,10 @@ def get_interfaces(self) -> List[ServerAddr]: with self.interfaces_lock: return list(self.interfaces) + def get_status(self): + n = len(self.get_interfaces()) + return _("Connected to {0} nodes.").format(n) if n > 1 else _("Connected to {0} node.").format(n) if n == 1 else _("Not connected") + def get_fee_estimates(self): from statistics import median from .simple_config import FEE_ETA_TARGETS From a57145117959b19c1dfdbd2a6082a1535239d5a1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 11:50:41 +0100 Subject: [PATCH 0428/1143] qml: allow pay while amount in edit mode --- electrum/gui/qml/components/InvoiceDialog.qml | 48 ++++++++++++------- electrum/gui/qml/qeinvoice.py | 38 ++++++++++++--- electrum/gui/qml/qetypes.py | 8 ++-- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index b0a012147..cd4fb415b 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -214,15 +214,10 @@ ElDialog { } ToolButton { - visible: !amountContainer.editmode && invoice.canPay + visible: !amountContainer.editmode icon.source: '../../icons/pen.png' icon.color: 'transparent' - onClicked: { - amountBtc.text = invoice.amount.satsInt == 0 ? '' : Config.formatSats(invoice.amount) - amountMax.checked = invoice.amount.isMax - amountContainer.editmode = true - amountBtc.focus = true - } + onClicked: enterAmountEdit() } GridLayout { visible: amountContainer.editmode @@ -232,6 +227,9 @@ ElDialog { id: amountBtc fiatfield: amountFiat enabled: !amountMax.checked + onTextAsSatsChanged: { + invoice.amountOverride = textAsSats + } } Label { @@ -250,7 +248,7 @@ ElDialog { checked: false onCheckedChanged: { if (activeFocus) - invoice.amount.isMax = checked + invoice.amountOverride.isMax = checked } } @@ -269,22 +267,18 @@ ElDialog { } } ToolButton { - visible: amountContainer.editmode Layout.fillWidth: false + visible: amountContainer.editmode icon.source: '../../icons/confirmed.png' icon.color: 'transparent' - onClicked: { - amountContainer.editmode = false - invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) - invoiceAmountChanged() - } + onClicked: applyAmountEdit() } ToolButton { - visible: amountContainer.editmode Layout.fillWidth: false + visible: amountContainer.editmode icon.source: '../../icons/closebutton.png' icon.color: 'transparent' - onClicked: amountContainer.editmode = false + onClicked: cancelAmountEdit() } } @@ -438,8 +432,10 @@ ElDialog { Layout.preferredWidth: 1 text: qsTr('Pay') icon.source: '../../icons/confirmed.png' - enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay && !amountContainer.editmode + enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay onClicked: { + if (amountContainer.editmode) + applyAmountEdit() if (invoice_key == '') // save invoice if not retrieved from key invoice.save_invoice() dialog.close() @@ -450,6 +446,24 @@ ElDialog { } + function enterAmountEdit() { + amountBtc.text = invoice.amount.satsInt == 0 ? '' : Config.formatSats(invoice.amount) + amountMax.checked = invoice.amount.isMax + amountContainer.editmode = true + amountBtc.focus = true + } + + function applyAmountEdit() { + amountContainer.editmode = false + invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) + invoiceAmountChanged() + } + + function cancelAmountEdit() { + amountContainer.editmode = false + invoice.amountOverride.clear() + } + Component.onCompleted: { if (invoice_key != '') { invoice.initFromKey(invoice_key) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 259f91131..e568247b3 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -135,6 +135,8 @@ class QEInvoiceParser(QEInvoice): lnurlRetrieved = pyqtSignal() lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) + amountOverrideChanged = pyqtSignal() + _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['pr']) def __init__(self, parent=None): @@ -151,6 +153,9 @@ def __init__(self, parent=None): self._timer.setSingleShot(True) self._timer.timeout.connect(self.updateStatusString) + self._amountOverride = QEAmount() + self._amountOverride.valueChanged.connect(self._on_amountoverride_value_changed) + self._bip70PrResolvedSignal.connect(self._bip70_payment_request_resolved) self.clear() @@ -203,6 +208,22 @@ def amount(self, new_amount): self.determine_can_pay() self.invoiceChanged.emit() + @pyqtProperty(QEAmount, notify=amountOverrideChanged) + def amountOverride(self): + return self._amountOverride + + @amountOverride.setter + def amountOverride(self, new_amount): + self._logger.debug(f'set new override amount {repr(new_amount)}') + self._amountOverride.copyFrom(new_amount) + + self.determine_can_pay() + self.amountOverrideChanged.emit() + + @pyqtSlot() + def _on_amountoverride_value_changed(self): + self.determine_can_pay() + @pyqtProperty('quint64', notify=invoiceChanged) def time(self): return self._effectiveInvoice.time if self._effectiveInvoice else 0 @@ -316,18 +337,23 @@ def determine_can_pay(self): self.canPay = False self.userinfo = '' - if self.amount.isEmpty: # unspecified amount + if not self.amountOverride.isEmpty: + amount = self.amountOverride + else: + amount = self.amount + + if amount.isEmpty: # unspecified amount return if self.invoiceType == QEInvoice.Type.LightningInvoice: if self.status in [PR_UNPAID, PR_FAILED]: - if self.get_max_spendable_lightning() >= self.amount.satsInt: + if self.get_max_spendable_lightning() >= amount.satsInt: lnaddr = self._effectiveInvoice._lnaddr - if lnaddr.amount and self.amount.satsInt < lnaddr.amount * COIN: + if lnaddr.amount and amount.satsInt < lnaddr.amount * COIN: self.userinfo = _('Cannot pay less than the amount specified in the invoice') else: self.canPay = True - elif self.address and self.get_max_spendable_onchain() > self.amount.satsInt: + elif self.address and self.get_max_spendable_onchain() > amount.satsInt: # TODO: validate address? # TODO: subtract fee? self.canPay = True @@ -343,10 +369,10 @@ def determine_can_pay(self): }[self.status] elif self.invoiceType == QEInvoice.Type.OnchainInvoice: if self.status in [PR_UNPAID, PR_FAILED]: - if self.amount.isMax and self.get_max_spendable_onchain() > 0: + if amount.isMax and self.get_max_spendable_onchain() > 0: # TODO: dust limit? self.canPay = True - elif self.get_max_spendable_onchain() >= self.amount.satsInt: + elif self.get_max_spendable_onchain() >= amount.satsInt: # TODO: subtract fee? self.canPay = True else: diff --git a/electrum/gui/qml/qetypes.py b/electrum/gui/qml/qetypes.py index 895b686d3..d41a5cd8a 100644 --- a/electrum/gui/qml/qetypes.py +++ b/electrum/gui/qml/qetypes.py @@ -77,10 +77,12 @@ def isMax(self, ismax): def isEmpty(self): return not(self._is_max or self._amount_sat or self._amount_msat) + @pyqtSlot() def clear(self): - self.satsInt = 0 - self.msatsInt = 0 - self.isMax = False + self._amount_sat = 0 + self._amount_msat = 0 + self._is_max = False + self.valueChanged.emit() def copyFrom(self, amount): if not amount: From 8528907a5b61680fb2c4e0f0b53cb61d9c05ee9d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 11:51:10 +0100 Subject: [PATCH 0429/1143] qml: trsutedcoin layout consistency --- electrum/plugins/trustedcoin/qml/Disclaimer.qml | 2 +- electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/trustedcoin/qml/Disclaimer.qml b/electrum/plugins/trustedcoin/qml/Disclaimer.qml index b1a1f2884..53f5b555e 100644 --- a/electrum/plugins/trustedcoin/qml/Disclaimer.qml +++ b/electrum/plugins/trustedcoin/qml/Disclaimer.qml @@ -15,7 +15,7 @@ WizardComponent { width: parent.width Label { - Layout.preferredWidth: parent.width + Layout.fillWidth: true text: plugin ? plugin.disclaimer : '' wrapMode: Text.Wrap } diff --git a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml index f979acb8b..0e963978f 100644 --- a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml +++ b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml @@ -87,8 +87,8 @@ WizardComponent { } Label { + Layout.fillWidth: true visible: !otpVerified && plugin.remoteKeyState == 'wallet_known' - Layout.preferredWidth: parent.width wrapMode: Text.Wrap text: qsTr('Otherwise, you can request your OTP secret from the server, by pressing the button below') } From 0b3279820a1cfec24ea248b07f3d67bae95307e5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 12:01:55 +0100 Subject: [PATCH 0430/1143] rm log --- electrum/gui/qml/qeqr.py | 1 - electrum/gui/qml/qewizard.py | 1 - 2 files changed, 2 deletions(-) diff --git a/electrum/gui/qml/qeqr.py b/electrum/gui/qml/qeqr.py index fdc612c8d..1bd9b1e35 100644 --- a/electrum/gui/qml/qeqr.py +++ b/electrum/gui/qml/qeqr.py @@ -138,7 +138,6 @@ def requestImage(self, qstr, size): uri = uri._replace(query=query) qstr = urllib.parse.urlunparse(uri) - #self._logger.debug('QR requested for %s' % qstr) qr = qrcode.QRCode(version=1, border=2) qr.add_data(qstr) diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 0f85144d8..50da4b0dc 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -92,7 +92,6 @@ def hasDuplicateKeys(self, js_data): def createStorage(self, js_data, single_password_enabled, single_password): self._logger.info('Creating wallet from wizard data') data = js_data.toVariant() - #self._logger.debug(str(data)) if single_password_enabled and single_password: data['encrypt'] = True From 7c2f13a76e181cd00f714d749e420f1c9423218a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 17 Mar 2023 12:30:58 +0100 Subject: [PATCH 0431/1143] follow-up fcbd25c1fd7e0ffa76c6249dae1630448fa8810e. fixes #8253 --- electrum/gui/qml/components/controls/BalanceSummary.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index c27bc2d57..9bab81a27 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -42,7 +42,7 @@ Item { GridLayout { id: balanceLayout columns: 3 - opacity: Daemon.currentWallet.synchronizing || Network.status == 'disconnected' ? 0 : 1 + opacity: Daemon.currentWallet.synchronizing || Network.server_status == 'disconnected' ? 0 : 1 Label { font.pixelSize: constants.fontSizeXLarge @@ -148,7 +148,7 @@ Item { } Label { - opacity: Network.status == 'disconnected' ? 1 : 0 + opacity: Network.server_status == 'disconnected' ? 1 : 0 anchors.centerIn: balancePane text: qsTr('Disconnected') color: Material.accentColor From 8eca3e0aaf8cc82c3c7533a59765f4b30ce2cc5e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 12:33:53 +0100 Subject: [PATCH 0432/1143] follow up 7c2f13a76e181cd00f714d749e420f1c9423218a --- electrum/gui/qml/components/controls/BalanceSummary.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 9bab81a27..f16d6e913 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -140,7 +140,7 @@ Item { } Label { - opacity: Daemon.currentWallet.synchronizing && Network.status != 'disconnected' ? 1 : 0 + opacity: Daemon.currentWallet.synchronizing && Network.server_status != 'disconnected' ? 1 : 0 anchors.centerIn: balancePane text: Daemon.currentWallet.synchronizingProgress color: Material.accentColor From c3a0f9c078afaa1d85b88e4fa710e97525b28742 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 16 Mar 2023 20:11:25 +0100 Subject: [PATCH 0433/1143] Qt swaps_dialog: do not use side effects to update tx. Use the app timer instead, so that the tx is not recomputed on every slider move (like in ConfirmTxDialog). A similar modification is needed for QML. I started with Qt in order to get a sense of how it should be done. --- electrum/gui/qt/swap_dialog.py | 85 ++++++++++++++++------------------ 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 5fbeb07dd..4b8902843 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -38,7 +38,6 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No self.lnworker = self.window.wallet.lnworker self.swap_manager = self.lnworker.swap_manager self.network = window.network - self.tx = None # for the forward-swap only self.channels = channels self.is_reverse = is_reverse if is_reverse is not None else True vbox = QVBoxLayout(self) @@ -102,6 +101,14 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No if recv_amount_sat: self.init_recv_amount(recv_amount_sat) self.update() + self.needs_tx_update = True + self.window.gui_object.timer.timeout.connect(self.timer_actions) + + def timer_actions(self): + if self.needs_tx_update: + self.update_tx() + self.update_ok_button() + self.needs_tx_update = False def init_recv_amount(self, recv_amount_sat): if recv_amount_sat == '!': @@ -137,32 +144,23 @@ def spend_max(self): if self.is_reverse: self._spend_max_reverse_swap() else: - self._spend_max_forward_swap() + # spend_max_forward_swap will be called in update_tx + pass else: self.send_amount_e.setAmount(None) - self.update_fee() - self.update_ok_button() + self.needs_tx_update = True def uncheck_max(self): self.max_button.setChecked(False) self.update() - def _spend_max_forward_swap(self): - self._update_tx('!') - if self.tx: - amount = self.tx.output_value_for_address(ln_dummy_address()) - max_amt = self.swap_manager.max_amount_forward_swap() - if max_amt is None: - self.send_amount_e.setAmount(None) - self.max_button.setChecked(False) - return - if amount > max_amt: - amount = max_amt - self._update_tx(amount) - if self.tx: - amount = self.tx.output_value_for_address(ln_dummy_address()) - assert amount <= max_amt - self.send_amount_e.setAmount(amount) + def _spend_max_forward_swap(self, tx): + if tx: + amount = tx.output_value_for_address(ln_dummy_address()) + self.send_amount_e.setAmount(amount) + else: + self.send_amount_e.setAmount(None) + self.max_button.setChecked(False) def _spend_max_reverse_swap(self): amount = min(self.lnworker.num_sats_can_send(), self.swap_manager.get_max_amount()) @@ -185,9 +183,7 @@ def on_send_edited(self): self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) self.recv_amount_e.follows = False self.send_follows = False - self._update_tx(send_amount) - self.update_fee() - self.update_ok_button() + self.needs_tx_update = True def on_recv_edited(self): if self.recv_amount_e.follows: @@ -202,9 +198,7 @@ def on_recv_edited(self): self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet()) self.send_amount_e.follows = False self.send_follows = True - self._update_tx(send_amount) - self.update_fee() - self.update_ok_button() + self.needs_tx_update = True def update(self): from .util import IconLabel @@ -219,17 +213,15 @@ def update(self): server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(server_mining_fee) + ' ' + self.window.base_unit() self.server_fee_label.setText(server_fee_str) self.server_fee_label.repaint() # macOS hack for #6269 - self.update_tx() - self.update_fee() - self.update_ok_button() + self.needs_tx_update = True - def update_fee(self): + def update_fee(self, tx): """Updates self.fee_label. No other side-effects.""" if self.is_reverse: sm = self.swap_manager fee = sm.get_claim_fee() else: - fee = self.tx.get_fee() if self.tx else None + fee = tx.get_fee() if tx else None fee_text = self.window.format_amount(fee) + ' ' + self.window.base_unit() if fee else '' self.fee_label.setText(fee_text) self.fee_label.repaint() # macOS hack for #6269 @@ -261,42 +253,45 @@ def run(self): def update_tx(self): if self.is_reverse: + self.update_fee(None) return is_max = self.max_button.isChecked() if is_max: - self._spend_max_forward_swap() + tx = self._create_tx('!') + self._spend_max_forward_swap(tx) else: onchain_amount = self.send_amount_e.get_amount() - self._update_tx(onchain_amount) + tx = self._create_tx(onchain_amount) + self.update_fee(tx) - def _update_tx(self, onchain_amount): - """Updates self.tx. No other side-effects.""" + def _create_tx(self, onchain_amount): if self.is_reverse: return if onchain_amount is None: - self.tx = None return - outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)] coins = self.window.get_coins() + if onchain_amount == '!': + max_amount = sum(c.value_sats() for c in coins) + max_swap_amount = self.swap_manager.max_amount_forward_swap() + if max_amount > max_swap_amount: + onchain_amount = max_swap_amount + outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)] try: - self.tx = self.window.wallet.make_unsigned_transaction( + tx = self.window.wallet.make_unsigned_transaction( coins=coins, outputs=outputs) except (NotEnoughFunds, NoDynamicFeeEstimates) as e: - self.tx = None + return + return tx def update_ok_button(self): """Updates self.ok_button. No other side-effects.""" send_amount = self.send_amount_e.get_amount() recv_amount = self.recv_amount_e.get_amount() - self.ok_button.setEnabled( - (send_amount is not None) - and (recv_amount is not None) - and (self.tx is not None or self.is_reverse) - ) + self.ok_button.setEnabled(bool(send_amount) and bool(recv_amount)) def do_normal_swap(self, lightning_amount, onchain_amount, password): - tx = self.tx + tx = self._create_tx(onchain_amount) assert tx coro = self.swap_manager.normal_swap( lightning_amount_sat=lightning_amount, From 24a3d6e10f1722adcf5d6302258bf4b2531eb0c7 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 16:44:26 +0100 Subject: [PATCH 0434/1143] qml: remove editmode toggle, now enabled only on amount-less invoices --- electrum/gui/qml/components/InvoiceDialog.qml | 69 ++++++------------- electrum/gui/qml/qeinvoice.py | 3 + 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index cd4fb415b..693fedd48 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -169,17 +169,26 @@ ElDialog { columns: 2 Label { - visible: invoice.amount.isMax Layout.columnSpan: 2 + Layout.fillWidth: true + visible: invoice.amount.isMax font.pixelSize: constants.fontSizeXLarge font.bold: true - Layout.fillWidth: true text: qsTr('All on-chain funds') } + Label { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: invoice.amount.isEmpty + font.pixelSize: constants.fontSizeXLarge + color: constants.mutedForeground + text: qsTr('not specified') + } + Label { Layout.alignment: Qt.AlignRight - visible: !invoice.amount.isMax + visible: !invoice.amount.isMax && !invoice.amount.isEmpty font.pixelSize: constants.fontSizeXLarge font.family: FixedFont font.bold: true @@ -187,8 +196,8 @@ ElDialog { } Label { - visible: !invoice.amount.isMax Layout.fillWidth: true + visible: !invoice.amount.isMax && !invoice.amount.isEmpty text: Config.baseUnit color: Material.accentColor font.pixelSize: constants.fontSizeXLarge @@ -197,15 +206,15 @@ ElDialog { Label { id: fiatValue Layout.alignment: Qt.AlignRight - visible: Daemon.fx.enabled && !invoice.amount.isMax + visible: Daemon.fx.enabled && !invoice.amount.isMax && !invoice.amount.isEmpty text: Daemon.fx.fiatValue(invoice.amount, false) font.pixelSize: constants.fontSizeMedium color: constants.mutedForeground } Label { - visible: Daemon.fx.enabled && !invoice.amount.isMax Layout.fillWidth: true + visible: Daemon.fx.enabled && !invoice.amount.isMax && !invoice.amount.isEmpty text: Daemon.fx.fiatCurrency font.pixelSize: constants.fontSizeMedium color: constants.mutedForeground @@ -213,16 +222,13 @@ ElDialog { } - ToolButton { - visible: !amountContainer.editmode - icon.source: '../../icons/pen.png' - icon.color: 'transparent' - onClicked: enterAmountEdit() - } GridLayout { - visible: amountContainer.editmode Layout.fillWidth: true + visible: amountContainer.editmode + enabled: !(invoice.status == Invoice.Expired && invoice.amount.isEmpty) + columns: 3 + BtcField { id: amountBtc fiatfield: amountFiat @@ -239,6 +245,7 @@ ElDialog { text: Config.baseUnit color: Material.accentColor } + Switch { id: amountMax Layout.fillWidth: true @@ -266,20 +273,6 @@ ElDialog { color: Material.accentColor } } - ToolButton { - Layout.fillWidth: false - visible: amountContainer.editmode - icon.source: '../../icons/confirmed.png' - icon.color: 'transparent' - onClicked: applyAmountEdit() - } - ToolButton { - Layout.fillWidth: false - visible: amountContainer.editmode - icon.source: '../../icons/closebutton.png' - icon.color: 'transparent' - onClicked: cancelAmountEdit() - } } } @@ -434,8 +427,6 @@ ElDialog { icon.source: '../../icons/confirmed.png' enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay onClicked: { - if (amountContainer.editmode) - applyAmountEdit() if (invoice_key == '') // save invoice if not retrieved from key invoice.save_invoice() dialog.close() @@ -446,29 +437,11 @@ ElDialog { } - function enterAmountEdit() { - amountBtc.text = invoice.amount.satsInt == 0 ? '' : Config.formatSats(invoice.amount) - amountMax.checked = invoice.amount.isMax - amountContainer.editmode = true - amountBtc.focus = true - } - - function applyAmountEdit() { - amountContainer.editmode = false - invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) - invoiceAmountChanged() - } - - function cancelAmountEdit() { - amountContainer.editmode = false - invoice.amountOverride.clear() - } - Component.onCompleted: { if (invoice_key != '') { invoice.initFromKey(invoice_key) } - if (invoice.amount.isEmpty) { + if (invoice.amount.isEmpty && !invoice.status == Invoice.Expired) { amountContainer.editmode = true } else if (invoice.amount.isMax) { amountMax.checked = true diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index e568247b3..d5497b1f5 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -179,6 +179,7 @@ def recipient(self, recipient: str): self.canPay = False self._recipient = recipient self._lnurlData = None + self.amountOverride = QEAmount() if recipient: self.validateRecipient(recipient) self.recipientChanged.emit() @@ -327,6 +328,8 @@ def set_status_timer(self): if interval > 0: self._timer.setInterval(interval) # msec self._timer.start() + else: + self.determine_can_pay() # status went to PR_EXPIRED @pyqtSlot() def updateStatusString(self): From 7d2ba3cc39e1364067aac1809c72e3831539fb7c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 16:46:35 +0100 Subject: [PATCH 0435/1143] qml: fix 43d6fd2aef11a10f816e9c26aa03b5ddf0c8acac --- electrum/gui/qml/qeinvoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index d5497b1f5..7188eda36 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -279,6 +279,7 @@ def set_lnprops(self): def name_for_node_id(self, node_id): node_alias = self._wallet.wallet.lnworker.get_node_alias(node_id) or node_id.hex() + return node_alias @pyqtSlot() def clear(self): From 8db1c3814be4d60ec367c3e24916f666da0c8439 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Mar 2023 15:47:42 +0000 Subject: [PATCH 0436/1143] qt export history: let util.filename_field decide default path which uses: directory = config.get('io_dir', os.path.expanduser('~')) --- electrum/gui/qt/history_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index ec3422d3f..ae0985ced 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -826,7 +826,7 @@ def export_history_dialog(self): d = WindowModalDialog(self, _('Export History')) d.setMinimumSize(400, 200) vbox = QVBoxLayout(d) - defaultname = os.path.expanduser('~/electrum-history.csv') + defaultname = f'electrum-history.csv' select_msg = _('Select file to export your wallet transactions to') hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg) vbox.addLayout(hbox) From 55da7276d3b3a4c7294c3e9fbedf6a57840120d0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Mar 2023 15:49:58 +0000 Subject: [PATCH 0437/1143] qt export history/privkeys: put wallet name in path closes https://github.com/spesmilo/electrum/issues/8255 --- electrum/gui/qt/history_list.py | 2 +- electrum/gui/qt/main_window.py | 2 +- electrum/wallet.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index ae0985ced..c274b997f 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -826,7 +826,7 @@ def export_history_dialog(self): d = WindowModalDialog(self, _('Export History')) d.setMinimumSize(400, 200) vbox = QVBoxLayout(d) - defaultname = f'electrum-history.csv' + defaultname = f'electrum-history-{self.wallet.basename()}.csv' select_msg = _('Select file to export your wallet transactions to') hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg) vbox.addLayout(hbox) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 42e20a89c..848fa2624 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -2251,7 +2251,7 @@ def export_privkeys_dialog(self, password): e.setReadOnly(True) vbox.addWidget(e) - defaultname = 'electrum-private-keys.csv' + defaultname = f'electrum-private-keys-{self.wallet.basename()}.csv' select_msg = _('Select file to export your private keys to') hbox, filename_e, csv_button = filename_field(self, self.config, defaultname, select_msg) vbox.addLayout(hbox) diff --git a/electrum/wallet.py b/electrum/wallet.py index 441e1e6a7..b0db8b3e4 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -548,7 +548,7 @@ def get_master_public_keys(self): return [] def basename(self) -> str: - return self.storage.basename() if self.storage else 'no name' + return self.storage.basename() if self.storage else 'no_name' def test_addresses_sanity(self) -> None: addrs = self.get_receiving_addresses() From 231ea5d03b744957fdfd8a2774cd7bd075d95a0a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 17 Mar 2023 17:32:27 +0100 Subject: [PATCH 0438/1143] qml: status icon InvoiceDialog --- electrum/gui/qml/components/InvoiceDialog.qml | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 693fedd48..c559801cc 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -91,8 +91,27 @@ ElDialog { color: Material.accentColor } - Label { - text: invoice.status_str + RowLayout { + Image { + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + source: invoice.status == Invoice.Expired + ? '../../icons/expired.png' + : invoice.status == Invoice.Unpaid + ? '../../icons/unpaid.png' + : invoice.status == Invoice.Failed || invoice.status == Invoice.Unknown + ? '../../icons/warning.png' + : invoice.status == Invoice.Inflight || invoice.status == Invoice.Routing + ? '../../icons/status_waiting.png' + : invoice.status == Invoice.Unconfirmed + ? '../../icons/unconfirmed.png' + : invoice.status == Invoice.Paid + ? '../../icons/confirmed.png' + : '' + } + Label { + text: invoice.status_str + } } Label { From a90bff4586249f6e16b42fea2b05b36b2dc61c65 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Mar 2023 17:09:01 +0000 Subject: [PATCH 0439/1143] qml: mark masterkey/wif/addr input fields as sensitive related: https://github.com/spesmilo/electrum/issues/8256 --- electrum/gui/qml/components/ImportAddressesKeysDialog.qml | 1 + electrum/gui/qml/components/wizard/WCHaveMasterKey.qml | 1 + electrum/gui/qml/components/wizard/WCImport.qml | 1 + 3 files changed, 3 insertions(+) diff --git a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml index 9320cbffe..61940b67a 100644 --- a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml +++ b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml @@ -64,6 +64,7 @@ ElDialog { focus: true wrapMode: TextEdit.WrapAnywhere onTextChanged: valid = verify(text) + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText } ColumnLayout { Layout.alignment: Qt.AlignTop diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index b35b8df07..558615573 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -119,6 +119,7 @@ WizardComponent { if (activeFocus) verifyMasterKey(text) } + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText } ColumnLayout { ToolButton { diff --git a/electrum/gui/qml/components/wizard/WCImport.qml b/electrum/gui/qml/components/wizard/WCImport.qml index baa0546b6..533308d71 100644 --- a/electrum/gui/qml/components/wizard/WCImport.qml +++ b/electrum/gui/qml/components/wizard/WCImport.qml @@ -40,6 +40,7 @@ WizardComponent { focus: true wrapMode: TextEdit.WrapAnywhere onTextChanged: valid = verify(text) + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText } ColumnLayout { Layout.alignment: Qt.AlignTop From 48e37696b3b28afd67437e4b21590e6101928585 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Mar 2023 18:31:50 +0000 Subject: [PATCH 0440/1143] qml wizard: fix creating wallet from master key fixes https://github.com/spesmilo/electrum/issues/8260 --- electrum/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 28173d84c..c8c59b5d7 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -235,7 +235,7 @@ def is_single_password(self): raise NotImplementedError() def is_bip39_seed(self, wizard_data): - return wizard_data['seed_variant'] == 'bip39' + return wizard_data.get('seed_variant') == 'bip39' def is_multisig(self, wizard_data): return wizard_data['wallet_type'] == 'multisig' From adca13a86cf4e94c37347f7590c5b15fdb089783 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 17 Mar 2023 19:21:01 +0000 Subject: [PATCH 0441/1143] android readme: update "access datadir on Android from desktop" was getting `cp: /sdcard/some_path/my_wallet: Operation not permitted` adb no longer has permissions to write to the sdcard New command allows dumping the file directly to local pc via usb. related: https://stackoverflow.com/q/72714568 https://stackoverflow.com/q/18471780 --- contrib/android/Readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/android/Readme.md b/contrib/android/Readme.md index dc0f44041..ad77a9fdf 100644 --- a/contrib/android/Readme.md +++ b/contrib/android/Readme.md @@ -114,7 +114,8 @@ of Android does not let you access the internal storage of an app without root. ``` $ adb shell $ run-as org.electrum.electrum ls /data/data/org.electrum.electrum/files/data -$ run-as org.electrum.electrum cp /data/data/org.electrum.electrum/files/data/wallets/my_wallet /sdcard/some_path/my_wallet +$ exit # to exit adb +$ adb exec-out run-as org.electrum.electrum cat /data/data/org.electrum.electrum/files/data/wallets/my_wallet > my_wallet ``` Or use Android Studio: "Device File Explorer", which can download/upload data directly from device (via adb). From fed5fe59911d680d683789c81be37096331e2fa7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 17 Mar 2023 23:02:43 +0100 Subject: [PATCH 0442/1143] Qml: new receive flow. fixes #8258 --- .../qml/components/ReceiveDetailsDialog.qml | 5 +- electrum/gui/qml/components/ReceiveDialog.qml | 61 ------------------- .../gui/qml/components/WalletMainView.qml | 51 +++++++++++++++- 3 files changed, 52 insertions(+), 65 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 290e6593e..f74998937 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -11,7 +11,7 @@ import "controls" ElDialog { id: dialog - title: qsTr('Edit payment request') + title: qsTr('Receive payment') property alias amount: amountBtc.text property alias description: message.text @@ -19,7 +19,6 @@ ElDialog { parent: Overlay.overlay modal: true - iconSource: Qt.resolvedUrl('../../icons/pen.png') Overlay.modal: Rectangle { color: "#aa000000" @@ -130,7 +129,7 @@ ElDialog { FlatButton { Layout.fillWidth: true - text: qsTr('Apply') + text: qsTr('Create request') icon.source: '../../icons/confirmed.png' onClicked: accept() } diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index d8cb65229..80336e307 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -22,8 +22,6 @@ ElDialog { property bool _render_qr: false // delay qr rendering until dialog is shown property bool _ispaid: false - property bool _ignore_gaplimit: false - property bool _reuse_address: false parent: Overlay.overlay modal: true @@ -283,14 +281,6 @@ ElDialog { enabled = true } } - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - icon.source: '../../icons/pen.png' - text: qsTr('Edit') - onClicked: receiveDetailsDialog.open() - } } } @@ -334,41 +324,6 @@ ElDialog { FocusScope { id: parkFocus } } - function createRequest() { - var qamt = Config.unitsToSats(receiveDetailsDialog.amount) - Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, _ignore_gaplimit, _reuse_address) - } - - function createDefaultRequest() { - console.log('Creating default request') - Daemon.currentWallet.createDefaultRequest(_ignore_gaplimit, _reuse_address) - } - - Connections { - target: Daemon.currentWallet - function onRequestCreateSuccess(key) { - request.key = key - } - function onRequestCreateError(code, error) { - if (code == 'gaplimit') { - var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) - dialog.yesClicked.connect(function() { - _ignore_gaplimit = true - createDefaultRequest() - }) - } else if (code == 'non-deterministic') { - var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) - dialog.yesClicked.connect(function() { - _reuse_address = true - createDefaultRequest() - }) - } else { - console.log(error) - var dialog = app.messageDialog.createObject(app, {text: error}) - } - dialog.open() - } - } RequestDetails { id: request @@ -396,22 +351,6 @@ ElDialog { } } - ReceiveDetailsDialog { - id: receiveDetailsDialog - - width: parent.width * 0.9 - anchors.centerIn: parent - - onAccepted: { - console.log('accepted') - Daemon.currentWallet.delete_request(request.key) - createRequest() - } - onRejected: { - console.log('rejected') - } - } - Toaster { id: toaster } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index a4d64daac..4d02dcd2f 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -16,6 +16,9 @@ Item { property var _sendDialog property string _intentUri + property bool _ignore_gaplimit: false + property bool _reuse_address: false + function openInvoice(key) { var dialog = invoiceDialog.createObject(app, { invoice: invoiceParser, invoice_key: key }) dialog.open() @@ -168,7 +171,7 @@ Item { icon.source: '../../icons/tab_receive.png' text: qsTr('Receive') onClicked: { - var dialog = receiveDialog.createObject(mainView) + var dialog = receiveDetails.createObject(mainView) dialog.open() } } @@ -238,6 +241,31 @@ Item { } } + Connections { + target: Daemon.currentWallet + function onRequestCreateSuccess(key) { + openRequest(key) + } + function onRequestCreateError(code, error) { + if (code == 'gaplimit') { + var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) + dialog.yesClicked.connect(function() { + _ignore_gaplimit = true + createDefaultRequest() + }) + } else if (code == 'non-deterministic') { + var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) + dialog.yesClicked.connect(function() { + _reuse_address = true + createDefaultRequest() + }) + } else { + console.log(error) + var dialog = app.messageDialog.createObject(app, {text: error}) + } + dialog.open() + } + } Connections { target: Daemon.currentWallet function onOtpRequested() { @@ -326,6 +354,27 @@ Item { } } + function createRequest(receiveDetailsDialog) { + var qamt = Config.unitsToSats(receiveDetailsDialog.amount) + Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, _ignore_gaplimit, _reuse_address) + } + + Component { + id: receiveDetails + ReceiveDetailsDialog { + id: receiveDetailsDialog + width: parent.width * 0.9 + anchors.centerIn: parent + onAccepted: { + console.log('accepted') + createRequest(receiveDetailsDialog) + } + onRejected: { + console.log('rejected') + } + } + } + Component { id: receiveDialog ReceiveDialog { From 39ac484ec7ddc319514ab76caa3649555fef9ddb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 18 Mar 2023 00:52:42 +0100 Subject: [PATCH 0443/1143] qml: password change requires password, not PIN. fixes #8257 --- electrum/gui/qml/qedaemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index a1b217ab2..569b24ad8 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -128,6 +128,7 @@ class QEDaemon(AuthMixin, QObject): serverConnectWizardChanged = pyqtSignal() loadingChanged = pyqtSignal() passwordChangeFailed = pyqtSignal() + requestNewPassword = pyqtSignal() walletLoaded = pyqtSignal([str,str], arguments=['name','path']) walletRequiresPassword = pyqtSignal([str,str], arguments=['name','path']) @@ -305,9 +306,8 @@ def suggestWalletName(self): i = i + 1 return f'wallet_{i}' - requestNewPassword = pyqtSignal() @pyqtSlot() - @auth_protect + @auth_protect(method='wallet') def startChangePassword(self): if self._use_single_password: self.requestNewPassword.emit() From 30034847a2b51183d084cce1d1f49182fc034eb4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 18 Mar 2023 00:57:39 +0100 Subject: [PATCH 0444/1143] qml: remove Never as request expiry option --- .../gui/qml/components/controls/RequestExpiryComboBox.qml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml b/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml index 30d24feea..b7d84d2a0 100644 --- a/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml +++ b/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml @@ -6,8 +6,6 @@ import org.electrum 1.0 ElComboBox { id: expires - property bool includeNever: true - textRole: 'text' valueRole: 'value' @@ -20,8 +18,6 @@ ElComboBox { expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60}) expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60}) expiresmodel.append({'text': qsTr('1 month'), 'value': 31*24*60*60}) - if (includeNever) - expiresmodel.append({'text': qsTr('Never'), 'value': 0}) expires.currentIndex = 0 for (let i=0; i < expiresmodel.count; i++) { if (expiresmodel.get(i).value == Config.requestExpiry) { From f6699e01c385d57ddd631f5445b49dd84b31b7b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 18 Mar 2023 04:29:28 +0100 Subject: [PATCH 0445/1143] qml: minor text change --- electrum/gui/qml/components/SendDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index edf438446..d6e3471a8 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -59,7 +59,7 @@ ElDialog { Layout.fillWidth: true Layout.preferredWidth: 1 icon.source: '../../icons/tab_receive.png' - text: qsTr('Invoices') + text: qsTr('Saved Invoices') enabled: Daemon.currentWallet.invoiceModel.rowCount() // TODO: only count non-expired onClicked: { dialog.close() From d8abab34d81a1c67b23f9c408770690f538417ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 18 Mar 2023 04:15:09 +0000 Subject: [PATCH 0446/1143] build: rm "non-free" from debian apt sources lists was not needed, and better to avoid :) --- contrib/android/apt.sources.list | 4 ++-- contrib/build-linux/appimage/apt.sources.list | 4 ++-- contrib/build-wine/apt.sources.list | 4 ++-- contrib/freeze_containers_distro.sh | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contrib/android/apt.sources.list b/contrib/android/apt.sources.list index fe4030017..93da282e5 100644 --- a/contrib/android/apt.sources.list +++ b/contrib/android/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib -deb-src https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib +deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib +deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib \ No newline at end of file diff --git a/contrib/build-linux/appimage/apt.sources.list b/contrib/build-linux/appimage/apt.sources.list index e19552491..50b885a85 100644 --- a/contrib/build-linux/appimage/apt.sources.list +++ b/contrib/build-linux/appimage/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20230226T090712Z/ buster main non-free contrib -deb-src https://snapshot.debian.org/archive/debian/20230226T090712Z/ buster main non-free contrib +deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ buster main contrib +deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ buster main contrib \ No newline at end of file diff --git a/contrib/build-wine/apt.sources.list b/contrib/build-wine/apt.sources.list index fe4030017..93da282e5 100644 --- a/contrib/build-wine/apt.sources.list +++ b/contrib/build-wine/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib -deb-src https://snapshot.debian.org/archive/debian/20230226T090712Z/ bullseye main non-free contrib +deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib +deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib \ No newline at end of file diff --git a/contrib/freeze_containers_distro.sh b/contrib/freeze_containers_distro.sh index d36715e87..0da3a9969 100755 --- a/contrib/freeze_containers_distro.sh +++ b/contrib/freeze_containers_distro.sh @@ -32,15 +32,15 @@ wget -O /dev/null ${DEBIAN_SNAPSHOT} 2>/dev/null echo "Valid!" # build-linux -echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main non-free contrib" >$contrib/build-linux/appimage/apt.sources.list -echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main non-free contrib" >>$contrib/build-linux/appimage/apt.sources.list +echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main contrib" >$contrib/build-linux/appimage/apt.sources.list +echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main contrib" >>$contrib/build-linux/appimage/apt.sources.list # build-wine -echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main non-free contrib" >$contrib/build-wine/apt.sources.list -echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main non-free contrib" >>$contrib/build-wine/apt.sources.list +echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main contrib" >$contrib/build-wine/apt.sources.list +echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main contrib" >>$contrib/build-wine/apt.sources.list # android -echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main non-free contrib" >$contrib/android/apt.sources.list -echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main non-free contrib" >>$contrib/android/apt.sources.list +echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main contrib" >$contrib/android/apt.sources.list +echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main contrib" >>$contrib/android/apt.sources.list echo "updated APT sources to ${DEBIAN_SNAPSHOT}" From 8cc610298b700e91825889e560cf618ca885f9f4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 18 Mar 2023 09:59:18 +0100 Subject: [PATCH 0447/1143] QML: auto-delete expired requests. Add action to Qt menu --- electrum/gui/qml/qewallet.py | 7 +++++++ electrum/gui/qt/receive_tab.py | 1 + electrum/gui/qt/request_list.py | 9 +++++++-- electrum/wallet.py | 11 +++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 52784947b..5f48a1d89 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -611,11 +611,17 @@ def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, return req_key, addr + def _delete_expired_requests(self): + keys = self.wallet.delete_expired_requests() + for key in keys: + self.requestModel.delete_invoice(key) + @pyqtSlot(QEAmount, str, int) @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) def createRequest(self, amount: QEAmount, message: str, expiration: int, ignore_gap: bool = False, reuse_address: bool = False): + self._delete_expired_requests() try: if self.wallet.lnworker and self.wallet.lnworker.channels: # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) @@ -639,6 +645,7 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, ignore_ @pyqtSlot(bool) @pyqtSlot(bool, bool) def createDefaultRequest(self, ignore_gap: bool = False, reuse_address: bool = False): + self._delete_expired_requests() try: default_expiry = self.wallet.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) if self.wallet.lnworker and self.wallet.lnworker.channels: diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 242f0a6d0..438c6ea19 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -158,6 +158,7 @@ def on_receive_swap(): self.qr_menu_action = menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) + menu.addAction(_("Delete expired requests"), self.request_list.delete_expired_requests) # layout vbox_g = QVBoxLayout() diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index b3e96bbf5..8d6c49d73 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -208,10 +208,15 @@ def create_menu(self, position): menu.exec_(self.viewport().mapToGlobal(position)) def delete_requests(self, keys): + self.wallet.delete_requests(keys) + for key in keys: + self.delete_item(key) + self.receive_tab.do_clear() + + def delete_expired_requests(self): + keys = self.wallet.delete_expired_requests() for key in keys: - self.wallet.delete_request(key, write_to_disk=False) self.delete_item(key) - self.wallet.save_db() self.receive_tab.do_clear() def set_visibility_of_columns(self): diff --git a/electrum/wallet.py b/electrum/wallet.py index b0db8b3e4..33c12ceb5 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2611,6 +2611,17 @@ def get_unpaid_requests(self): out.sort(key=lambda x: x.time) return out + def delete_expired_requests(self): + keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED] + self.delete_requests(keys) + return keys + + def delete_requests(self, keys): + for key in keys: + self.delete_request(key, write_to_disk=False) + if keys: + self.save_db() + @abstractmethod def get_fingerprint(self) -> str: """Returns a string that can be used to identify this wallet. From cb4c99dc1971efa8940f7adefc7f292eb5778ba3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 18 Mar 2023 11:25:57 +0100 Subject: [PATCH 0448/1143] qml: styling CloseChannelDialog error text --- electrum/gui/qml/components/CloseChannelDialog.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index 176277486..129bb44b3 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -141,12 +141,12 @@ ElDialog { Layout.columnSpan: 2 Layout.maximumWidth: parent.width - Label { + InfoTextArea { id: errorText Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width - visible: !_closing && errorText - wrapMode: Text.Wrap + visible: !_closing && errorText.text + iconStyle: InfoTextArea.IconStyle.Error } Label { Layout.alignment: Qt.AlignHCenter From 2836dccfbbf5640ffbd7805c6fc6f7edf994cbfa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 18 Mar 2023 10:56:25 +0100 Subject: [PATCH 0449/1143] qml: Handle situation where no more addresses are available without creating addresses beyond the gap limit. - if lightning is enabled, propose to create a lightning-only invoice - otherwise, propose to reuse an address - never generate addresses beyond the gap limit Implementation: - createDefaultRequest is removed - create_bitcoin_address is called whether the wallet has lightning or not --- electrum/gui/qml/components/ReceiveDialog.qml | 7 +- .../gui/qml/components/WalletMainView.qml | 38 ++++---- electrum/gui/qml/qewallet.py | 87 +++++-------------- 3 files changed, 43 insertions(+), 89 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 80336e307..1372b9a72 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -356,12 +356,7 @@ ElDialog { } Component.onCompleted: { - if (dialog.key) { - request.key = dialog.key - } else { - // callLater to make sure any popups are on top of the dialog stacking order - Qt.callLater(createDefaultRequest) - } + request.key = dialog.key } // hack. delay qr rendering until dialog is shown diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 4d02dcd2f..38d94360e 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -16,8 +16,9 @@ Item { property var _sendDialog property string _intentUri - property bool _ignore_gaplimit: false - property bool _reuse_address: false + property string _request_amount + property string _request_description + property string _request_expiry function openInvoice(key) { var dialog = invoiceDialog.createObject(app, { invoice: invoiceParser, invoice_key: key }) @@ -171,7 +172,7 @@ Item { icon.source: '../../icons/tab_receive.png' text: qsTr('Receive') onClicked: { - var dialog = receiveDetails.createObject(mainView) + var dialog = receiveDetailsDialog.createObject(mainView) dialog.open() } } @@ -244,20 +245,18 @@ Item { Connections { target: Daemon.currentWallet function onRequestCreateSuccess(key) { - openRequest(key) + openRequest(key) } function onRequestCreateError(code, error) { - if (code == 'gaplimit') { + if (code == 'ln') { var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) dialog.yesClicked.connect(function() { - _ignore_gaplimit = true - createDefaultRequest() + createRequest(true, false) }) - } else if (code == 'non-deterministic') { + } else if (code == 'reuse_addr') { var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) dialog.yesClicked.connect(function() { - _reuse_address = true - createDefaultRequest() + createRequest(false, true) }) } else { console.log(error) @@ -354,25 +353,30 @@ Item { } } - function createRequest(receiveDetailsDialog) { - var qamt = Config.unitsToSats(receiveDetailsDialog.amount) - Daemon.currentWallet.createRequest(qamt, receiveDetailsDialog.description, receiveDetailsDialog.expiry, _ignore_gaplimit, _reuse_address) + function createRequest(lightning_only, reuse_address) { + var qamt = Config.unitsToSats(_request_amount) + Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning_only, reuse_address) } Component { - id: receiveDetails + id: receiveDetailsDialog + ReceiveDetailsDialog { - id: receiveDetailsDialog + id: _receiveDetailsDialog width: parent.width * 0.9 anchors.centerIn: parent onAccepted: { console.log('accepted') - createRequest(receiveDetailsDialog) + _request_amount = _receiveDetailsDialog.amount + _request_description = _receiveDetailsDialog.description + _request_expiry = _receiveDetailsDialog.expiry + createRequest(false, false) } onRejected: { console.log('rejected') } - } + onClosed: destroy() + } } Component { diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 5f48a1d89..0c467fdb4 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -577,39 +577,26 @@ def pay_thread(): threading.Thread(target=pay_thread, daemon=True).start() - def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, ignore_gap: bool = False, reuse_address: bool = False) -> Optional[Tuple]: + def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, lightning_only: bool = False, reuse_address: bool = False) -> Optional[Tuple]: addr = self.wallet.get_unused_address() if addr is None: - if not self.wallet.is_deterministic(): # imported wallet - if not reuse_address: - msg = [ - _('No more addresses in your wallet.'), ' ', - _('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ', - _('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n', - _('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'), - ] - self.requestCreateError.emit('non-deterministic',''.join(msg)) - return + if reuse_address: addr = self.wallet.get_receiving_address() - else: # deterministic wallet - if not ignore_gap: - self.requestCreateError.emit('gaplimit',_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")) - return - addr = self.wallet.create_new_address(False) + elif lightning_only: + addr = None + else: + has_lightning = self.wallet.has_lightning() + msg = [ + _('No more unused addresses in your wallet.'), + _('All your addresses are used by unpaid requests.'), + ] + msg.append(_('Do you wish to create a lightning-only request?') if has_lightning else _('Do you want to reuse an address?')) + return req_key = self.wallet.create_request(amount, message, expiration, addr) - self._logger.debug(f'created request with key {req_key}') - #try: - #self.wallet.add_payment_request(req) - #except Exception as e: - #self.logger.exception('Error adding payment request') - #self.requestCreateError.emit('fatal',_('Error adding payment request') + ':\n' + repr(e)) - #else: - ## TODO: check this flow. Only if alias is defined in config. OpenAlias? - #pass - ##self.sign_payment_request(addr) - - return req_key, addr + self._logger.debug(f'created request with key {req_key} addr {addr}') + + return req_key def _delete_expired_requests(self): keys = self.wallet.delete_expired_requests() @@ -620,46 +607,14 @@ def _delete_expired_requests(self): @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) - def createRequest(self, amount: QEAmount, message: str, expiration: int, ignore_gap: bool = False, reuse_address: bool = False): - self._delete_expired_requests() - try: - if self.wallet.lnworker and self.wallet.lnworker.channels: - # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) - # TODO fallback address robustness - addr = self.wallet.get_unused_address() - key = self.wallet.create_request(amount.satsInt, message, expiration, addr) - else: - key, addr = self.create_bitcoin_request(amount.satsInt, message, expiration, ignore_gap=ignore_gap, reuse_address=reuse_address) - if not key: - return - self.addressModel.setDirty() - except InvoiceError as e: - self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e)) - return - - assert key is not None - self.requestModel.add_invoice(self.wallet.get_request(key)) - self.requestCreateSuccess.emit(key) - - @pyqtSlot() - @pyqtSlot(bool) - @pyqtSlot(bool, bool) - def createDefaultRequest(self, ignore_gap: bool = False, reuse_address: bool = False): + def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning_only: bool = False, reuse_address: bool = False): self._delete_expired_requests() try: - default_expiry = self.wallet.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - if self.wallet.lnworker and self.wallet.lnworker.channels: - addr = self.wallet.get_unused_address() - # if addr is None, we ran out of addresses - if addr is None: - # TODO: remove oldest unpaid request having a fallback address and try again - pass - key = self.wallet.create_request(None, None, default_expiry, addr) - else: - req = self.create_bitcoin_request(None, None, default_expiry, ignore_gap=ignore_gap, reuse_address=reuse_address) - if not req: - return - key, addr = req + key = self.create_bitcoin_request(amount.satsInt, message, expiration, lightning_only=lightning_only, reuse_address=reuse_address) + if not key: + self.requestCreateError.emit('ln' if self.wallet.has_lightning() else 'reuse_addr', ' '.join(msg)) + return + self.addressModel.setDirty() except InvoiceError as e: self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e)) return From dd2dced2963df939acb189375176c2382a0679b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 18 Mar 2023 13:14:14 +0100 Subject: [PATCH 0450/1143] follow-up 2836dccfbbf5640ffbd7805c6fc6f7edf994cbfa --- electrum/gui/qml/qewallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 0c467fdb4..fcd9fb08d 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -591,6 +591,7 @@ def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, _('All your addresses are used by unpaid requests.'), ] msg.append(_('Do you wish to create a lightning-only request?') if has_lightning else _('Do you want to reuse an address?')) + self.requestCreateError.emit('ln' if has_lightning else 'reuse_addr', ' '.join(msg)) return req_key = self.wallet.create_request(amount, message, expiration, addr) @@ -612,7 +613,6 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, lightni try: key = self.create_bitcoin_request(amount.satsInt, message, expiration, lightning_only=lightning_only, reuse_address=reuse_address) if not key: - self.requestCreateError.emit('ln' if self.wallet.has_lightning() else 'reuse_addr', ' '.join(msg)) return self.addressModel.setDirty() except InvoiceError as e: From d7c5c40c1da4c455d60cac571f10a60130614e3b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 18 Mar 2023 16:46:20 +0100 Subject: [PATCH 0451/1143] Save user-entered amount in invoice. fixes #8252. Note that this allows users to save invoices that have an empty amount, which is not allowed by the Qt GUI. Qt will complain at pay time about empty amount if a lightning invoice without amount is saved. With onchain invoices, Qt will create an onchain tx with a zero output. --- electrum/gui/qml/components/InvoiceDialog.qml | 15 ++++++++++++++- electrum/gui/qml/qeinvoice.py | 8 +++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index c559801cc..a240a632a 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -435,6 +435,9 @@ ElDialog { enabled: invoice.canSave onClicked: { app.stack.push(Qt.resolvedUrl('Invoices.qml')) + if (invoice.amount.isEmpty) { + invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) + } invoice.save_invoice() dialog.close() } @@ -446,8 +449,18 @@ ElDialog { icon.source: '../../icons/confirmed.png' enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay onClicked: { - if (invoice_key == '') // save invoice if not retrieved from key + if (invoice.amount.isEmpty) { + invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) + if (invoice_key != '') { + // delete the existing invoice because this affects get_id() + invoice.wallet.delete_invoice(invoice_key) + invoice_key = '' + } + } + if (invoice_key == '') { + // save invoice if new or modified invoice.save_invoice() + } dialog.close() doPay() // only signal here } diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 7188eda36..b60da544e 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -313,8 +313,6 @@ def set_effective_invoice(self, invoice: Invoice): self.set_lnprops() - self.canSave = True - self.determine_can_pay() self.invoiceChanged.emit() @@ -339,6 +337,7 @@ def updateStatusString(self): def determine_can_pay(self): self.canPay = False + self.canSave = False self.userinfo = '' if not self.amountOverride.isEmpty: @@ -346,6 +345,8 @@ def determine_can_pay(self): else: amount = self.amount + self.canSave = True + if amount.isEmpty: # unspecified amount return @@ -652,6 +653,8 @@ def validate(self): self.validationError.emit('recipient', _('Invalid Bitcoin address')) return + self.canSave = True + if self._amount.isEmpty: self.validationError.emit('amount', _('Invalid amount')) return @@ -659,7 +662,6 @@ def validate(self): if self._amount.isMax: self.canPay = True else: - self.canSave = True if self.get_max_spendable_onchain() >= self._amount.satsInt: self.canPay = True From 3b78466123d23e6a720e2b20705bd8e8240f29aa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 18 Mar 2023 17:44:48 +0100 Subject: [PATCH 0452/1143] simplify code (follow-up 2836dccfbbf5640ffbd7805c6fc6f7edf994cbfa) --- electrum/gui/qml/qewallet.py | 54 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index fcd9fb08d..c182625dc 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -577,49 +577,43 @@ def pay_thread(): threading.Thread(target=pay_thread, daemon=True).start() - def create_bitcoin_request(self, amount: int, message: str, expiration: int, *, lightning_only: bool = False, reuse_address: bool = False) -> Optional[Tuple]: - addr = self.wallet.get_unused_address() - if addr is None: - if reuse_address: - addr = self.wallet.get_receiving_address() - elif lightning_only: - addr = None - else: - has_lightning = self.wallet.has_lightning() - msg = [ - _('No more unused addresses in your wallet.'), - _('All your addresses are used by unpaid requests.'), - ] - msg.append(_('Do you wish to create a lightning-only request?') if has_lightning else _('Do you want to reuse an address?')) - self.requestCreateError.emit('ln' if has_lightning else 'reuse_addr', ' '.join(msg)) - return - - req_key = self.wallet.create_request(amount, message, expiration, addr) - self._logger.debug(f'created request with key {req_key} addr {addr}') - - return req_key - def _delete_expired_requests(self): - keys = self.wallet.delete_expired_requests() - for key in keys: - self.requestModel.delete_invoice(key) @pyqtSlot(QEAmount, str, int) @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning_only: bool = False, reuse_address: bool = False): - self._delete_expired_requests() + # delete expired_requests + keys = self.wallet.delete_expired_requests() + for key in keys: + self.requestModel.delete_invoice(key) try: - key = self.create_bitcoin_request(amount.satsInt, message, expiration, lightning_only=lightning_only, reuse_address=reuse_address) - if not key: - return - self.addressModel.setDirty() + amount = amount.satsInt + addr = self.wallet.get_unused_address() + if addr is None: + if reuse_address: + addr = self.wallet.get_receiving_address() + elif lightning_only: + addr = None + else: + has_lightning = self.wallet.has_lightning() + msg = [ + _('No more unused addresses in your wallet.'), + _('All your addresses are used by unpaid requests.'), + ] + msg.append(_('Do you wish to create a lightning-only request?') if has_lightning else _('Do you want to reuse an address?')) + self.requestCreateError.emit('ln' if has_lightning else 'reuse_addr', ' '.join(msg)) + return + + key = self.wallet.create_request(amount, message, expiration, addr) except InvoiceError as e: self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e)) return assert key is not None + self._logger.debug(f'created request with key {key} addr {addr}') + self.addressModel.setDirty() self.requestModel.add_invoice(self.wallet.get_request(key)) self.requestCreateSuccess.emit(key) From 5c60b9ad29ea46da0264cb19a47a39a4e72fe6d3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 9 Aug 2022 17:50:41 +0200 Subject: [PATCH 0453/1143] ln invoice dialog: show fallback address --- electrum/gui/qt/main_window.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 848fa2624..2971ebc38 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1456,16 +1456,21 @@ def show_lightning_invoice(self, invoice: Invoice): grid.addWidget(QLabel(_("Expiration time") + ':'), 4, 0) grid.addWidget(QLabel(format_time(invoice.time + invoice.exp)), 4, 1) grid.addWidget(QLabel(_('Features') + ':'), 5, 0) - grid.addWidget(QLabel('\n'.join(lnaddr.get_features().get_names())), 5, 1) + grid.addWidget(QLabel(', '.join(lnaddr.get_features().get_names())), 5, 1) payhash_e = ShowQRLineEdit(lnaddr.paymenthash.hex(), self.config, title=_("Payment Hash")) grid.addWidget(QLabel(_("Payment Hash") + ':'), 6, 0) grid.addWidget(payhash_e, 6, 1) + fallback = lnaddr.get_fallback_address() + if fallback: + fallback_e = ShowQRLineEdit(fallback, self.config, title=_("Fallback address")) + grid.addWidget(QLabel(_("Fallback address") + ':'), 7, 0) + grid.addWidget(fallback_e, 7, 1) invoice_e = ShowQRTextEdit(config=self.config) invoice_e.setFont(QFont(MONOSPACE_FONT)) invoice_e.addCopyButton() invoice_e.setText(invoice.lightning_invoice) - grid.addWidget(QLabel(_('Text') + ':'), 7, 0) - grid.addWidget(invoice_e, 7, 1) + grid.addWidget(QLabel(_('Text') + ':'), 8, 0) + grid.addWidget(invoice_e, 8, 1) vbox.addLayout(grid) vbox.addLayout(Buttons(CloseButton(d),)) d.exec_() From aa3697de745e2840bb9bc02646ce6734f969a41d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 06:36:36 +0100 Subject: [PATCH 0454/1143] Qr request_list: maybe fix elusive segfault --- electrum/gui/qt/request_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 8d6c49d73..dcce82ae3 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -209,8 +209,7 @@ def create_menu(self, position): def delete_requests(self, keys): self.wallet.delete_requests(keys) - for key in keys: - self.delete_item(key) + self.update() self.receive_tab.do_clear() def delete_expired_requests(self): From 5ab3a250c5e3f515d566d6d982dd5b5d3339f683 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 09:00:51 +0100 Subject: [PATCH 0455/1143] qml: remove '1 month' expiry option. The list of supported values is in electrum/invoices.py If the config is set to an unsuported value, the qt app will crash. --- electrum/gui/qml/components/controls/RequestExpiryComboBox.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml b/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml index b7d84d2a0..db13486f1 100644 --- a/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml +++ b/electrum/gui/qml/components/controls/RequestExpiryComboBox.qml @@ -17,7 +17,6 @@ ElComboBox { expiresmodel.append({'text': qsTr('1 hour'), 'value': 60*60}) expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60}) expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60}) - expiresmodel.append({'text': qsTr('1 month'), 'value': 31*24*60*60}) expires.currentIndex = 0 for (let i=0; i < expiresmodel.count; i++) { if (expiresmodel.get(i).value == Config.requestExpiry) { From c3e52bfafc2196f0d088928e7550c7065cc1a453 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 09:39:04 +0100 Subject: [PATCH 0456/1143] Qt: allow to save invoices that have no amount (such invoices are already saved by the QML GUI.) --- electrum/gui/qt/invoice_list.py | 5 ++++- electrum/gui/qt/paytoedit.py | 4 +--- electrum/gui/qt/send_tab.py | 12 +++++++++--- electrum/invoices.py | 3 ++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index cd5d59bbf..785a73f9d 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -179,7 +179,10 @@ def create_menu(self, position): copy_menu.addAction(_("Address"), lambda: self.main_window.do_copy(invoice.get_address(), title='Bitcoin Address')) status = wallet.get_invoice_status(invoice) if status == PR_UNPAID: - menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) + if bool(invoice.get_amount_sat()): + menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) + else: + menu.addAction(_("Edit amount") + "...", lambda: self.send_tab.do_edit_invoice(invoice)) if status == PR_FAILED: menu.addAction(_("Retry"), lambda: self.send_tab.do_pay_invoice(invoice)) if self.wallet.lnworker: diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index c0796f4f3..4b60598ee 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -364,9 +364,7 @@ def get_outputs(self, is_max: bool) -> List[PartialTxOutput]: if is_max: amount = '!' else: - amount = self.amount_edit.get_amount() - if amount is None: - return [] + amount = self.amount_edit.get_amount() or 0 self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)] return self.outputs[:] diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 9146e0aed..6b42168ae 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -500,9 +500,6 @@ def read_invoice(self) -> Optional[Invoice]: amount_sat = self.amount_e.get_amount() if amount_sat: invoice.amount_msat = int(amount_sat * 1000) - else: - self.show_error(_('No amount')) - return if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): self.show_error(_('Lightning is disabled')) return @@ -582,7 +579,16 @@ def pay_multiple_invoices(self, invoices): outputs += invoice.outputs self.pay_onchain_dialog(outputs) + def do_edit_invoice(self, invoice: 'Invoice'): + assert not bool(invoice.get_amount_sat()) + text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address() + self.payto_e._on_input_btn(text) + self.amount_e.setFocus() + def do_pay_invoice(self, invoice: 'Invoice'): + if not bool(invoice.get_amount_sat()): + self.show_error(_('No amount')) + return if invoice.is_lightning(): self.pay_lightning_invoice(invoice) else: diff --git a/electrum/invoices.py b/electrum/invoices.py index 7aa2e0488..aa1d06f23 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -161,7 +161,8 @@ def get_amount_sat(self) -> Union[int, str, None]: Returns an integer satoshi amount, or '!' or None. Callers who need msat precision should call get_amount_msat() """ - amount_msat = self.amount_msat + # return strictly positive value or None + amount_msat = self.amount_msat or None if amount_msat in [None, "!"]: return amount_msat return int(amount_msat // 1000) From b07fe970bfb44f60b77c377847a0c995a76ee556 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 17 Mar 2023 18:55:37 +0100 Subject: [PATCH 0457/1143] receive tab: do not use ButtonsTextEdit, add toggle to toolbar. --- electrum/gui/qt/receive_tab.py | 48 +++++++++++++++++++++----------- electrum/plugins/hw_wallet/qt.py | 2 +- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 438c6ea19..82b19cef2 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -6,7 +6,7 @@ from PyQt5.QtGui import QFont, QCursor from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import (QComboBox, QLabel, QVBoxLayout, QGridLayout, QLineEdit, +from PyQt5.QtWidgets import (QComboBox, QLabel, QVBoxLayout, QGridLayout, QLineEdit, QTextEdit, QHBoxLayout, QPushButton, QWidget, QSizePolicy, QFrame) from electrum.bitcoin import is_address @@ -73,7 +73,7 @@ def __init__(self, window: 'ElectrumWindow'): buttons.addWidget(self.create_invoice_button) grid.addLayout(buttons, 4, 0, 1, -1) - self.receive_address_e = ButtonsTextEdit() + self.receive_address_e = QTextEdit() self.receive_address_help_text = WWLabel('') vbox = QVBoxLayout() vbox.addWidget(self.receive_address_help_text) @@ -81,9 +81,9 @@ def __init__(self, window: 'ElectrumWindow'): self.receive_address_help.setVisible(False) self.receive_address_help.setLayout(vbox) - self.receive_URI_e = ButtonsTextEdit() + self.receive_URI_e = QTextEdit() self.receive_URI_help = WWLabel('') - self.receive_lightning_e = ButtonsTextEdit() + self.receive_lightning_e = QTextEdit() self.receive_lightning_help_text = WWLabel('') self.receive_rebalance_button = QPushButton('Rebalance') self.receive_rebalance_button.suggestion = None @@ -114,8 +114,9 @@ def on_receive_swap(): for e in [self.receive_address_e, self.receive_URI_e, self.receive_lightning_e]: e.setFont(QFont(MONOSPACE_FONT)) - e.addCopyButton() e.setReadOnly(True) + e.setContextMenuPolicy(Qt.NoContextMenu) + e.setTextInteractionFlags(Qt.NoTextInteraction) self.receive_lightning_e.textChanged.connect(self.update_receive_widgets) @@ -147,7 +148,15 @@ def on_receive_swap(): self.receive_requests_label.setMaximumWidth(400) from .request_list import RequestList self.request_list = RequestList(self) + # toolbar self.toolbar, menu = self.request_list.create_toolbar_with_menu('') + self.toggle_qr_button = QPushButton('') + self.toggle_qr_button.setIcon(read_QIcon('qrcode.png')) + self.toggle_qr_button.setToolTip(_('Switch between text and QR code view')) + self.toggle_qr_button.clicked.connect(self.toggle_receive_qr) + self.toggle_qr_button.setEnabled(False) + self.toolbar.insertWidget(2, self.toggle_qr_button) + # menu menu.addConfig( _('Add on-chain fallback to lightning requests'), 'bolt11_fallback', True, callback=self.on_toggle_bolt11_fallback) @@ -159,6 +168,7 @@ def on_receive_swap(): menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) menu.addAction(_("Delete expired requests"), self.request_list.delete_expired_requests) + self.toolbar_menu = menu # layout vbox_g = QVBoxLayout() @@ -216,9 +226,14 @@ def on_tab_changed(self, i): self.window.do_copy(data, title=title) self.update_receive_qr_window() - def toggle_receive_qr(self, e): + def do_copy(self, e): if e.button() != Qt.LeftButton: return + i = self.receive_tabs.currentIndex() + title, data = self.get_tab_data(i) + self.window.do_copy(data, title=title) + + def toggle_receive_qr(self): b = not self.config.get('receive_qr_visible', False) self.config.set_key('receive_qr_visible', b) self.update_receive_widgets() @@ -275,15 +290,16 @@ def update_current_request(self): self.receive_address_e.repaint() # always show self.receive_tabs.setVisible(True) + self.toggle_qr_button.setEnabled(True) self.update_receive_qr_window() def get_tab_data(self, i): if i == 0: - return _('Bitcoin URI'), self.receive_URI_e.text() + return _('Bitcoin URI'), self.receive_URI_e.toPlainText() elif i == 1: - return _('Address'), self.receive_address_e.text() + return _('Address'), self.receive_address_e.toPlainText() else: - return _('Lightning Request'), self.receive_lightning_e.text() + return _('Lightning Request'), self.receive_lightning_e.toPlainText() def update_receive_qr_window(self): if self.window.qr_window and self.window.qr_window.isVisible(): @@ -353,17 +369,18 @@ def do_clear(self): self.receive_URI_e.setText('') self.receive_lightning_e.setText('') self.receive_tabs.setVisible(False) + self.toggle_qr_button.setEnabled(False) self.receive_message_e.setText('') self.receive_amount_e.setAmount(None) self.request_list.clearSelection() def update_textedit_warning(self, *, text_e: ButtonsTextEdit, warning_text: Optional[str]): - if bool(text_e.text()) and warning_text: + if bool(text_e.toPlainText()) and warning_text: text_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) text_e.setToolTip(warning_text) else: text_e.setStyleSheet("") - text_e.setToolTip(text_e._default_tooltip) + text_e.setToolTip('') class ReceiveTabWidget(QWidget): @@ -376,12 +393,11 @@ def __init__(self, receive_tab: 'ReceiveTab', textedit: QWidget, qr: QWidget, he QWidget.__init__(self) for w in [textedit, qr, help_widget]: w.setMinimumSize(self.min_size) + for w in [textedit, qr]: - w.mousePressEvent = receive_tab.toggle_receive_qr - tooltip = _('Click to switch between text and QR code view') - w._default_tooltip = tooltip - w.setToolTip(tooltip) + w.mousePressEvent = receive_tab.do_copy w.setCursor(QCursor(Qt.PointingHandCursor)) + textedit.setFocusPolicy(Qt.NoFocus) if isinstance(help_widget, QLabel): help_widget.setFrameStyle(QFrame.StyledPanel) @@ -394,7 +410,7 @@ def __init__(self, receive_tab: 'ReceiveTab', textedit: QWidget, qr: QWidget, he self.setLayout(hbox) def update_visibility(self, is_qr): - if str(self.textedit.text()): + if str(self.textedit.toPlainText()): self.help_widget.setVisible(False) self.textedit.setVisible(not is_qr) self.qr.setVisible(is_qr) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 9448e88e9..17f1ebb0d 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -290,7 +290,7 @@ def show_address(): addr = str(receive_address_e.text()) keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore)) dev_name = f"{plugin.device} ({keystore.label})" - receive_address_e.addButton("eye1.png", show_address, _("Show on {}").format(dev_name)) + main_window.receive_tab.toolbar_menu.addAction(read_QIcon("eye1.png"), _("Show address on {}").format(dev_name), show_address) def create_handler(self, window: Union[ElectrumWindow, InstallWizard]) -> 'QtHandlerBase': raise NotImplementedError() From 4243b250b19ea2da4869864e0ae1d1ad4194c196 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 10:44:33 +0100 Subject: [PATCH 0458/1143] qt send_tab: simplify method names. when a method belongs to a class, there is no need to repeat the class name in the method name. --- electrum/gui/qt/send_tab.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 6b42168ae..a9b418f68 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -488,7 +488,7 @@ def handle_payment_identifier(self, text: str, *, can_use_network: bool = True): self.window.show_send_tab() def read_invoice(self) -> Optional[Invoice]: - if self.check_send_tab_payto_line_and_show_errors(): + if self.check_payto_line_and_show_errors(): return try: if not self._is_onchain: @@ -506,7 +506,7 @@ def read_invoice(self) -> Optional[Invoice]: return invoice else: outputs = self.read_outputs() - if self.check_send_tab_onchain_outputs_and_show_errors(outputs): + if self.check_onchain_outputs_and_show_errors(outputs): return message = self.message_e.text() return self.wallet.create_invoice( @@ -601,7 +601,7 @@ def read_outputs(self) -> List[PartialTxOutput]: outputs = self.payto_e.get_outputs(self.max_button.isChecked()) return outputs - def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool: + def check_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool: """Returns whether there are errors with outputs. Also shows error dialog to user if so. """ @@ -619,7 +619,7 @@ def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTx return False # no errors - def check_send_tab_payto_line_and_show_errors(self) -> bool: + def check_payto_line_and_show_errors(self) -> bool: """Returns whether there are errors. Also shows error dialog to user if so. """ From 8b0a6940bc411c6c139c8a72442f1ba5c6a41c0c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 11:13:45 +0100 Subject: [PATCH 0459/1143] receive tab: disable widgets if request has expired, instead of applying red stylesheet --- electrum/gui/qt/qrcodewidget.py | 10 ++++++---- electrum/gui/qt/receive_tab.py | 17 +++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qt/qrcodewidget.py b/electrum/gui/qt/qrcodewidget.py index 01d865934..80c1f4364 100644 --- a/electrum/gui/qt/qrcodewidget.py +++ b/electrum/gui/qt/qrcodewidget.py @@ -60,8 +60,9 @@ def paintEvent(self, e): return black = QColor(0, 0, 0, 255) + grey = QColor(196, 196, 196, 255) white = QColor(255, 255, 255, 255) - black_pen = QPen(black) + black_pen = QPen(black) if self.isEnabled() else QPen(grey) black_pen.setJoinStyle(Qt.MiterJoin) if not self.qr: @@ -95,13 +96,14 @@ def paintEvent(self, e): qp.setPen(white) qp.drawRect(0, 0, framesize, framesize) # Draw qr code - qp.setBrush(black) + qp.setBrush(black if self.isEnabled() else grey) qp.setPen(black_pen) for r in range(k): for c in range(k): if matrix[r][c]: - qp.drawRect(int(left+c*boxsize), int(top+r*boxsize), - boxsize - 1, boxsize - 1) + qp.drawRect( + int(left+c*boxsize), int(top+r*boxsize), + boxsize - 1, boxsize - 1) qp.end() def grab(self) -> QtGui.QPixmap: diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 82b19cef2..fc1b0b512 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -281,9 +281,13 @@ def update_current_request(self): self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? self.receive_lightning_help_text.setText(ln_help) self.receive_lightning_qr.setData(lnaddr.upper()) - self.update_textedit_warning(text_e=self.receive_address_e, warning_text=address_help) - self.update_textedit_warning(text_e=self.receive_URI_e, warning_text=URI_help) - self.update_textedit_warning(text_e=self.receive_lightning_e, warning_text=ln_help) + def update_warnings(text_e, qr_e, warning_text): + for w in [text_e, qr_e]: + w.setEnabled(bool(text_e.toPlainText()) and not warning_text) + w.setToolTip(warning_text) + update_warnings(self.receive_address_e, self.receive_address_qr, address_help) + update_warnings(self.receive_URI_e, self.receive_URI_qr, URI_help) + update_warnings(self.receive_lightning_e, self.receive_lightning_qr, ln_help) # macOS hack (similar to #4777) self.receive_lightning_e.repaint() self.receive_URI_e.repaint() @@ -374,13 +378,6 @@ def do_clear(self): self.receive_amount_e.setAmount(None) self.request_list.clearSelection() - def update_textedit_warning(self, *, text_e: ButtonsTextEdit, warning_text: Optional[str]): - if bool(text_e.toPlainText()) and warning_text: - text_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) - text_e.setToolTip(warning_text) - else: - text_e.setStyleSheet("") - text_e.setToolTip('') class ReceiveTabWidget(QWidget): From 5cf4b346a9d59b16c5e788aea13f0200ccfbecfb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 11:40:52 +0100 Subject: [PATCH 0460/1143] change message: detached QR code window --- electrum/gui/qt/receive_tab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index fc1b0b512..a302d7d45 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -164,7 +164,7 @@ def on_receive_swap(): _('Add lightning requests to bitcoin URIs'), 'bip21_lightning', False, tooltip=_('This may result in large QR codes'), callback=self.update_current_request) - self.qr_menu_action = menu.addToggle(_("Show QR code window"), self.window.toggle_qr_window) + self.qr_menu_action = menu.addToggle(_("Show detached QR code window"), self.window.toggle_qr_window) menu.addAction(_("Import requests"), self.window.import_requests) menu.addAction(_("Export requests"), self.window.export_requests) menu.addAction(_("Delete expired requests"), self.request_list.delete_expired_requests) From 68bba4705247bb8e77ecbdc14b35306a510eb3bb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 18:34:43 +0000 Subject: [PATCH 0461/1143] commands: small fix and clean-up for "serialize" cmd Docstring was outdated, and `txout.get('value', txout['value_sats'])` was a logic bug. fixes https://github.com/spesmilo/electrum/issues/8265 --- electrum/commands.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 91c42a2c8..d7e1af79e 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -371,9 +371,17 @@ async def getaddressunspent(self, address): @command('') async def serialize(self, jsontx): - """Create a transaction from json inputs. - Inputs must have a redeemPubkey. - Outputs must be a list of {'address':address, 'value':satoshi_amount}. + """Create a signed raw transaction from a json tx template. + + Example value for "jsontx" arg: { + "inputs": [ + {"prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539", "prevout_n": 1, + "value_sats": 1000000, "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"} + ], + "outputs": [ + {"address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd", "value_sats": 990000} + ] + } """ keypairs = {} inputs = [] # type: List[PartialTxInput] @@ -386,7 +394,10 @@ async def serialize(self, jsontx): else: raise Exception("missing prevout for txin") txin = PartialTxInput(prevout=prevout) - txin._trusted_value_sats = int(txin_dict.get('value', txin_dict['value_sats'])) + try: + txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats']) + except KeyError: + raise Exception("missing 'value_sats' field for txin") nsequence = txin_dict.get('nsequence', None) if nsequence is not None: txin.nsequence = nsequence @@ -399,8 +410,19 @@ async def serialize(self, jsontx): txin.script_descriptor = desc inputs.append(txin) - outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout.get('value', txout['value_sats']))) - for txout in jsontx.get('outputs')] + outputs = [] # type: List[PartialTxOutput] + for txout_dict in jsontx.get('outputs'): + try: + txout_addr = txout_dict['address'] + except KeyError: + raise Exception("missing 'address' field for txout") + try: + txout_val = int(txout_dict.get('value') or txout_dict['value_sats']) + except KeyError: + raise Exception("missing 'value_sats' field for txout") + txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val) + outputs.append(txout) + tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx.sign(keypairs) return tx.serialize() From a30cda4ebd2b867c7cc14beb1ea618036e95f91a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 19:15:44 +0000 Subject: [PATCH 0462/1143] lnutil: test ImportedChannelBackupStorage.from_bytes regression test - we should not inadvertently break deserialising existing backups --- electrum/lnutil.py | 11 +++++++++-- electrum/lnworker.py | 5 +---- electrum/tests/test_lnutil.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index b325fb7ab..b5a657be6 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -16,7 +16,7 @@ from .util import ShortID as ShortChannelID from .util import format_short_id as format_short_channel_id -from .crypto import sha256 +from .crypto import sha256, pw_decode_with_version_and_mac from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, TxOutput) from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number @@ -281,7 +281,7 @@ def to_bytes(self) -> bytes: return bytes(vds.input) @staticmethod - def from_bytes(s): + def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": vds = BCDataStream() vds.write(s) version = vds.read_int16() @@ -302,6 +302,13 @@ def from_bytes(s): host = vds.read_string(), port = vds.read_int16()) + @staticmethod + def from_encrypted_str(data: str, *, password: str) -> "ImportedChannelBackupStorage": + if not data.startswith('channel_backup:'): + raise ValueError("missing or invalid magic bytes") + encrypted = data[15:] + decrypted = pw_decode_with_version_and_mac(encrypted, password) + return ImportedChannelBackupStorage.from_bytes(decrypted) class ScriptHtlc(NamedTuple): diff --git a/electrum/lnworker.py b/electrum/lnworker.py index fe86970e8..57652bdc5 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2452,11 +2452,8 @@ async def request_force_close(self, channel_id: bytes, *, connect_str=None) -> N raise Exception(f'Unknown channel {channel_id.hex()}') def import_channel_backup(self, data): - assert data.startswith('channel_backup:') - encrypted = data[15:] xpub = self.wallet.get_fingerprint() - decrypted = pw_decode_with_version_and_mac(encrypted, xpub) - cb_storage = ImportedChannelBackupStorage.from_bytes(decrypted) + cb_storage = ImportedChannelBackupStorage.from_encrypted_str(data, password=xpub) channel_id = cb_storage.channel_id() if channel_id.hex() in self.db.get_dict("channels"): raise Exception('Channel already in wallet') diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index e3dff0d60..5133c1ad3 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -9,12 +9,15 @@ derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, - ln_compare_features, IncompatibleLightningFeatures, ChannelType) + ln_compare_features, IncompatibleLightningFeatures, ChannelType, + ImportedChannelBackupStorage) from electrum.util import bfh, MyEncoder from electrum.transaction import Transaction, PartialTransaction, Sighash from electrum.lnworker import LNWallet +from electrum.wallet import restore_wallet_from_text, Standard_Wallet +from electrum.simple_config import SimpleConfig -from . import ElectrumTestCase +from . import ElectrumTestCase, as_testnet funding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be' @@ -903,3 +906,29 @@ def test_channel_type(self): # ignore unknown channel types channel_type = ChannelType(0b10000000001000000000010).discard_unknown_and_check() self.assertEqual(ChannelType(0b10000000001000000000000), channel_type) + + @as_testnet + async def test_decode_imported_channel_backup(self): + encrypted_cb = "channel_backup:Adn87xcGIs9H2kfp4VpsOaNKWCHX08wBoqq37l1cLYKGlJamTeoaLEwpJA81l1BXF3GP/mRxqkY+whZG9l51G8izIY/kmMSvnh0DOiZEdwaaT/1/MwEHfsEomruFqs+iW24SFJPHbMM7f80bDtIxcLfZkKmgcKBAOlcqtq+dL3U3yH74S8BDDe2L4snaxxpCjF0JjDMBx1UR/28D+QlIi+lbvv1JMaCGXf+AF1+3jLQf8+lVI+rvFdyArws6Ocsvjf+ANQeSGUwW6Nb2xICQcMRgr1DO7bO4pgGu408eYRr2v3ayJBVtnKwSwd49gF5SDSjTDAO4CCM0uj9H5RxyzH7fqotkd9J80MBr84RiBXAeXKz+Ap8608/FVqgQ9BOcn6LhuAQdE5zXpmbQyw5jUGkPvHuseR+rzthzncy01odUceqTNg==" + config = SimpleConfig({'electrum_path': self.electrum_path}) + d = restore_wallet_from_text("9dk", path=None, gap_limit=2, config=config) + wallet1 = d['wallet'] # type: Standard_Wallet + decoded_cb = ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet1.get_fingerprint()) + self.assertEqual( + ImportedChannelBackupStorage( + funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe', + funding_index=1, + funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp', + is_initiator=True, + node_id=bfh('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f'), + privkey=bfh('7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772'), + host='lightning.electrum.org', + port=9739, + channel_seed=bfh('ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a'), + local_delay=1008, + remote_delay=720, + remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), + remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), + ), + decoded_cb, + ) From 4fb35c000200eee89d9d175dc91779fbc1522075 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 19:22:41 +0000 Subject: [PATCH 0463/1143] lnutil: clean-up ImportedChannelBackupStorage.from_bytes --- electrum/lnutil.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index b5a657be6..8cea30275 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -288,19 +288,20 @@ def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": if version != CHANNEL_BACKUP_VERSION: raise Exception(f"unknown version for channel backup: {version}") return ImportedChannelBackupStorage( - is_initiator = vds.read_boolean(), - privkey = vds.read_bytes(32).hex(), - channel_seed = vds.read_bytes(32).hex(), - node_id = vds.read_bytes(33).hex(), - funding_txid = vds.read_bytes(32).hex(), - funding_index = vds.read_int16(), - funding_address = vds.read_string(), - remote_payment_pubkey = vds.read_bytes(33).hex(), - remote_revocation_pubkey = vds.read_bytes(33).hex(), - local_delay = vds.read_int16(), - remote_delay = vds.read_int16(), - host = vds.read_string(), - port = vds.read_int16()) + is_initiator=vds.read_boolean(), + privkey=vds.read_bytes(32), + channel_seed=vds.read_bytes(32), + node_id=vds.read_bytes(33), + funding_txid=vds.read_bytes(32).hex(), + funding_index=vds.read_int16(), + funding_address=vds.read_string(), + remote_payment_pubkey=vds.read_bytes(33), + remote_revocation_pubkey=vds.read_bytes(33), + local_delay=vds.read_int16(), + remote_delay=vds.read_int16(), + host=vds.read_string(), + port=vds.read_int16(), + ) @staticmethod def from_encrypted_str(data: str, *, password: str) -> "ImportedChannelBackupStorage": From 5a4c39cb94375221fad0025f044d1a3eacc1dfe6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 19:32:09 +0000 Subject: [PATCH 0464/1143] lnutil.ImportedChannelBackupStorage: change ser format: int16->uint16 In the binary serialised format, replace all instances of int16 with uint16. In particular, this allows port>32767. Fixes https://github.com/spesmilo/electrum/issues/8264 I think this is backwards compatible, as in, any existing channel backup already out there, should be properly parsed with the new code. (new code however can serialise cbs that old code deserialises incorrectly) ``` >>> struct.pack('>> struct.pack(' bytes: vds = BCDataStream() - vds.write_int16(CHANNEL_BACKUP_VERSION) + vds.write_uint16(CHANNEL_BACKUP_VERSION) vds.write_boolean(self.is_initiator) vds.write_bytes(self.privkey, 32) vds.write_bytes(self.channel_seed, 32) vds.write_bytes(self.node_id, 33) vds.write_bytes(bfh(self.funding_txid), 32) - vds.write_int16(self.funding_index) + vds.write_uint16(self.funding_index) vds.write_string(self.funding_address) vds.write_bytes(self.remote_payment_pubkey, 33) vds.write_bytes(self.remote_revocation_pubkey, 33) - vds.write_int16(self.local_delay) - vds.write_int16(self.remote_delay) + vds.write_uint16(self.local_delay) + vds.write_uint16(self.remote_delay) vds.write_string(self.host) - vds.write_int16(self.port) + vds.write_uint16(self.port) return bytes(vds.input) @staticmethod def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": vds = BCDataStream() vds.write(s) - version = vds.read_int16() + version = vds.read_uint16() if version != CHANNEL_BACKUP_VERSION: raise Exception(f"unknown version for channel backup: {version}") return ImportedChannelBackupStorage( @@ -293,14 +293,14 @@ def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": channel_seed=vds.read_bytes(32), node_id=vds.read_bytes(33), funding_txid=vds.read_bytes(32).hex(), - funding_index=vds.read_int16(), + funding_index=vds.read_uint16(), funding_address=vds.read_string(), remote_payment_pubkey=vds.read_bytes(33), remote_revocation_pubkey=vds.read_bytes(33), - local_delay=vds.read_int16(), - remote_delay=vds.read_int16(), + local_delay=vds.read_uint16(), + remote_delay=vds.read_uint16(), host=vds.read_string(), - port=vds.read_int16(), + port=vds.read_uint16(), ) @staticmethod From 08ae0a73b2d3fb5163a115372d92d44704f7ee35 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 01:33:04 +0000 Subject: [PATCH 0465/1143] build: add separate .dockerignore files The .dockerignore symlink in the project root dir is only picked up by the android build. The android build has the project root as its build context for "docker build" -- the other builds have their own subdirectories as build context, e.g. contrib/build-linux/appimage. --- contrib/build-linux/appimage/.dockerignore | 3 +++ contrib/build-linux/sdist/.dockerignore | 1 + contrib/build-wine/.dockerignore | 6 ++++++ 3 files changed, 10 insertions(+) create mode 100644 contrib/build-linux/appimage/.dockerignore create mode 100644 contrib/build-linux/sdist/.dockerignore create mode 100644 contrib/build-wine/.dockerignore diff --git a/contrib/build-linux/appimage/.dockerignore b/contrib/build-linux/appimage/.dockerignore new file mode 100644 index 000000000..d75fb8304 --- /dev/null +++ b/contrib/build-linux/appimage/.dockerignore @@ -0,0 +1,3 @@ +build/ +.cache/ +fresh_clone/ diff --git a/contrib/build-linux/sdist/.dockerignore b/contrib/build-linux/sdist/.dockerignore new file mode 100644 index 000000000..d364c6400 --- /dev/null +++ b/contrib/build-linux/sdist/.dockerignore @@ -0,0 +1 @@ +fresh_clone/ diff --git a/contrib/build-wine/.dockerignore b/contrib/build-wine/.dockerignore new file mode 100644 index 000000000..f1aa3647c --- /dev/null +++ b/contrib/build-wine/.dockerignore @@ -0,0 +1,6 @@ +tmp/ +build/ +.cache/ +dist/ +signed/ +fresh_clone/ From ab073827cf46e568527f3fe309495c3d6336ffb0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sun, 19 Mar 2023 14:29:29 +0100 Subject: [PATCH 0466/1143] build: use uid of user building the build containers --- contrib/android/Dockerfile | 4 +++- contrib/android/build.sh | 2 ++ contrib/build-linux/appimage/Dockerfile | 4 +++- contrib/build-linux/appimage/build.sh | 2 ++ contrib/build-linux/sdist/Dockerfile | 4 +++- contrib/build-linux/sdist/build.sh | 2 ++ contrib/build-wine/Dockerfile | 4 +++- contrib/build-wine/build.sh | 2 ++ 8 files changed, 20 insertions(+), 4 deletions(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 19ad50903..9188dba08 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -2,6 +2,8 @@ FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 +ARG UID=1000 + ENV DEBIAN_FRONTEND=noninteractive ENV ANDROID_HOME="/opt/android" @@ -145,7 +147,7 @@ ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" -RUN useradd --create-home --shell /bin/bash ${USER} +RUN useradd --uid $UID --create-home --shell /bin/bash ${USER} RUN usermod -append --groups sudo ${USER} RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers WORKDIR ${WORK_DIR} diff --git a/contrib/android/build.sh b/contrib/android/build.sh index eed613c20..a57abb81f 100755 --- a/contrib/android/build.sh +++ b/contrib/android/build.sh @@ -11,6 +11,7 @@ PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_ANDROID="$CONTRIB/android" DISTDIR="$PROJECT_ROOT/dist" +BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh @@ -39,6 +40,7 @@ fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ + --build-arg UID=$BUILD_UID \ -t electrum-android-builder-img \ --file "$CONTRIB_ANDROID/Dockerfile" \ "$PROJECT_ROOT" diff --git a/contrib/build-linux/appimage/Dockerfile b/contrib/build-linux/appimage/Dockerfile index e92b6eb0d..f86285204 100644 --- a/contrib/build-linux/appimage/Dockerfile +++ b/contrib/build-linux/appimage/Dockerfile @@ -4,6 +4,8 @@ FROM debian:buster@sha256:233c3bbc892229c82da7231980d50adceba4db56a08c0b7053a4852782703459 +ARG UID=1000 + ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive @@ -75,7 +77,7 @@ ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" -RUN useradd --create-home --shell /bin/bash ${USER} +RUN useradd --uid $UID --create-home --shell /bin/bash ${USER} RUN usermod -append --groups sudo ${USER} RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers WORKDIR ${WORK_DIR} diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 6f48142a2..45a914823 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -11,6 +11,7 @@ PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage" DISTDIR="$PROJECT_ROOT/dist" +BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh @@ -24,6 +25,7 @@ fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ + --build-arg UID=$BUILD_UID \ -t electrum-appimage-builder-img \ "$CONTRIB_APPIMAGE" diff --git a/contrib/build-linux/sdist/Dockerfile b/contrib/build-linux/sdist/Dockerfile index 2caf62cf7..3fe81e0b9 100644 --- a/contrib/build-linux/sdist/Dockerfile +++ b/contrib/build-linux/sdist/Dockerfile @@ -1,5 +1,7 @@ FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 +ARG UID=1000 + ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive @@ -22,7 +24,7 @@ ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" -RUN useradd --create-home --shell /bin/bash ${USER} +RUN useradd --uid $UID --create-home --shell /bin/bash ${USER} RUN usermod -append --groups sudo ${USER} RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers WORKDIR ${WORK_DIR} diff --git a/contrib/build-linux/sdist/build.sh b/contrib/build-linux/sdist/build.sh index 361dadcd6..fe97ba0a7 100755 --- a/contrib/build-linux/sdist/build.sh +++ b/contrib/build-linux/sdist/build.sh @@ -11,6 +11,7 @@ PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_SDIST="$CONTRIB/build-linux/sdist" DISTDIR="$PROJECT_ROOT/dist" +BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh @@ -24,6 +25,7 @@ fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ + --build-arg UID=$BUILD_UID \ -t electrum-sdist-builder-img \ "$CONTRIB_SDIST" diff --git a/contrib/build-wine/Dockerfile b/contrib/build-wine/Dockerfile index 9b4dfbbdb..5703e81e6 100644 --- a/contrib/build-wine/Dockerfile +++ b/contrib/build-wine/Dockerfile @@ -1,5 +1,7 @@ FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 +ARG UID=1000 + # need ca-certificates before using snapshot packages RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ ca-certificates @@ -63,7 +65,7 @@ ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" -RUN useradd --create-home --shell /bin/bash ${USER} +RUN useradd --uid $UID --create-home --shell /bin/bash ${USER} RUN usermod -append --groups sudo ${USER} RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers WORKDIR ${WORK_DIR} diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index e73dec054..09ff475d1 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -10,6 +10,7 @@ PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_WINE="$CONTRIB/build-wine" +BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh @@ -26,6 +27,7 @@ fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ + --build-arg UID=$BUILD_UID \ -t electrum-wine-builder-img \ "$CONTRIB_WINE" From 6e472efd5f58d988c8db1366550d7b0b3a7b339a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 01:47:41 +0000 Subject: [PATCH 0467/1143] build: follow-up prev: only use host userid for local dev builds reproducibility probably needs a hardcoded userid Also, move the UID arg later in the dockerfiles, for better caching. (if local dev build and repro build set different UIDs, the build caches will diverge at that step) --- contrib/android/Dockerfile | 3 +-- contrib/android/build.sh | 5 ++++- contrib/build-linux/appimage/Dockerfile | 3 +-- contrib/build-linux/appimage/build.sh | 5 ++++- contrib/build-linux/sdist/Dockerfile | 3 +-- contrib/build-linux/sdist/build.sh | 5 ++++- contrib/build-wine/Dockerfile | 3 +-- contrib/build-wine/build.sh | 5 ++++- 8 files changed, 20 insertions(+), 12 deletions(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 9188dba08..a408cc5e2 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -2,8 +2,6 @@ FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 -ARG UID=1000 - ENV DEBIAN_FRONTEND=noninteractive ENV ANDROID_HOME="/opt/android" @@ -143,6 +141,7 @@ RUN apt -y update -qq \ # create new user to avoid using root; but with sudo access and no password for convenience. +ARG UID=1000 ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ diff --git a/contrib/android/build.sh b/contrib/android/build.sh index a57abb81f..7e4120314 100755 --- a/contrib/android/build.sh +++ b/contrib/android/build.sh @@ -37,10 +37,13 @@ if [ ! -z "$ELECBUILD_NOCACHE" ] ; then DOCKER_BUILD_FLAGS="--pull --no-cache" fi +if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build + DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" +fi + info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ - --build-arg UID=$BUILD_UID \ -t electrum-android-builder-img \ --file "$CONTRIB_ANDROID/Dockerfile" \ "$PROJECT_ROOT" diff --git a/contrib/build-linux/appimage/Dockerfile b/contrib/build-linux/appimage/Dockerfile index f86285204..c93f73513 100644 --- a/contrib/build-linux/appimage/Dockerfile +++ b/contrib/build-linux/appimage/Dockerfile @@ -4,8 +4,6 @@ FROM debian:buster@sha256:233c3bbc892229c82da7231980d50adceba4db56a08c0b7053a4852782703459 -ARG UID=1000 - ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive @@ -73,6 +71,7 @@ RUN apt-get update -q && \ apt-get clean # create new user to avoid using root; but with sudo access and no password for convenience. +ARG UID=1000 ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index 45a914823..be2184d77 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -22,10 +22,13 @@ if [ ! -z "$ELECBUILD_NOCACHE" ] ; then DOCKER_BUILD_FLAGS="--pull --no-cache" fi +if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build + DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" +fi + info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ - --build-arg UID=$BUILD_UID \ -t electrum-appimage-builder-img \ "$CONTRIB_APPIMAGE" diff --git a/contrib/build-linux/sdist/Dockerfile b/contrib/build-linux/sdist/Dockerfile index 3fe81e0b9..1a7a468a9 100644 --- a/contrib/build-linux/sdist/Dockerfile +++ b/contrib/build-linux/sdist/Dockerfile @@ -1,7 +1,5 @@ FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 -ARG UID=1000 - ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive @@ -20,6 +18,7 @@ RUN apt-get update -q && \ apt-get clean # create new user to avoid using root; but with sudo access and no password for convenience. +ARG UID=1000 ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ diff --git a/contrib/build-linux/sdist/build.sh b/contrib/build-linux/sdist/build.sh index fe97ba0a7..af895b01b 100755 --- a/contrib/build-linux/sdist/build.sh +++ b/contrib/build-linux/sdist/build.sh @@ -22,10 +22,13 @@ if [ ! -z "$ELECBUILD_NOCACHE" ] ; then DOCKER_BUILD_FLAGS="--pull --no-cache" fi +if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build + DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" +fi + info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ - --build-arg UID=$BUILD_UID \ -t electrum-sdist-builder-img \ "$CONTRIB_SDIST" diff --git a/contrib/build-wine/Dockerfile b/contrib/build-wine/Dockerfile index 5703e81e6..8fb6acf5c 100644 --- a/contrib/build-wine/Dockerfile +++ b/contrib/build-wine/Dockerfile @@ -1,7 +1,5 @@ FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 -ARG UID=1000 - # need ca-certificates before using snapshot packages RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ ca-certificates @@ -61,6 +59,7 @@ RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \ apt-get clean # create new user to avoid using root; but with sudo access and no password for convenience. +ARG UID=1000 ENV USER="user" ENV HOME_DIR="/home/${USER}" ENV WORK_DIR="${HOME_DIR}/wspace" \ diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index 09ff475d1..06b503e46 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -24,10 +24,13 @@ if [ ! -z "$ELECBUILD_NOCACHE" ] ; then DOCKER_BUILD_FLAGS="--pull --no-cache" fi +if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build + DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" +fi + info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ - --build-arg UID=$BUILD_UID \ -t electrum-wine-builder-img \ "$CONTRIB_WINE" From 4fa192d9e704d556b8d194aa26578d364085d993 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 20 Mar 2023 11:09:18 +0100 Subject: [PATCH 0468/1143] follow-up c3e52bfafc2196f0d088928e7550c7065cc1a453 --- electrum/gui/qt/paytoedit.py | 2 +- electrum/gui/qt/send_tab.py | 10 +++++++--- electrum/invoices.py | 3 +-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 4b60598ee..af2ebfd61 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -364,7 +364,7 @@ def get_outputs(self, is_max: bool) -> List[PartialTxOutput]: if is_max: amount = '!' else: - amount = self.amount_edit.get_amount() or 0 + amount = self.send_tab.get_amount() self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)] return self.outputs[:] diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index a9b418f68..4e5281f0b 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -497,7 +497,7 @@ def read_invoice(self) -> Optional[Invoice]: return invoice = Invoice.from_bech32(invoice_str) if invoice.amount_msat is None: - amount_sat = self.amount_e.get_amount() + amount_sat = self.get_amount() if amount_sat: invoice.amount_msat = int(amount_sat * 1000) if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): @@ -531,9 +531,13 @@ def save_pending_invoice(self): self.invoice_list.update() self.pending_invoice = None + def get_amount(self) -> int: + # must not be None + return self.amount_e.get_amount() or 0 + def _lnurl_get_invoice(self) -> None: assert self._lnurl_data - amount = self.amount_e.get_amount() + amount = self.get_amount() if not (self._lnurl_data.min_sendable_sat <= amount <= self._lnurl_data.max_sendable_sat): self.show_error(f'Amount must be between {self._lnurl_data.min_sendable_sat} and {self._lnurl_data.max_sendable_sat} sat.') return @@ -542,7 +546,7 @@ async def f(): try: invoice_data = await callback_lnurl( self._lnurl_data.callback_url, - params={'amount': self.amount_e.get_amount() * 1000}, + params={'amount': self.get_amount() * 1000}, ) except LNURLError as e: self.show_error_signal.emit(f"LNURL request encountered error: {e}") diff --git a/electrum/invoices.py b/electrum/invoices.py index aa1d06f23..7aa2e0488 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -161,8 +161,7 @@ def get_amount_sat(self) -> Union[int, str, None]: Returns an integer satoshi amount, or '!' or None. Callers who need msat precision should call get_amount_msat() """ - # return strictly positive value or None - amount_msat = self.amount_msat or None + amount_msat = self.amount_msat if amount_msat in [None, "!"]: return amount_msat return int(amount_msat // 1000) From c9df290301871eb86b1a549159a6066b70dedf76 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 20 Mar 2023 13:15:03 +0100 Subject: [PATCH 0469/1143] android: update P4A to 8589243afb48fdb116d791dc5b3973382e83273f include Qt Virtual Keyboard libraries and associated QtQuick components --- contrib/android/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 19ad50903..3ca448808 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,7 +179,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "8ac9841dc0d2c825a247583b9ed5a33b7b533e85^{commit}" \ + && git checkout "8589243afb48fdb116d791dc5b3973382e83273f^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars From 4ed69cc54f3b848fb41039929447f6070333e980 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 20 Mar 2023 14:10:13 +0100 Subject: [PATCH 0470/1143] qml: BtcField/FiatField ImhDigitsOnly input method hint --- electrum/gui/qml/components/controls/BtcField.qml | 2 +- electrum/gui/qml/components/controls/FiatField.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/BtcField.qml b/electrum/gui/qml/components/controls/BtcField.qml index 9436542c1..3afeaa029 100644 --- a/electrum/gui/qml/components/controls/BtcField.qml +++ b/electrum/gui/qml/components/controls/BtcField.qml @@ -10,7 +10,7 @@ TextField { font.family: FixedFont placeholderText: qsTr('Amount') - inputMethodHints: Qt.ImhPreferNumbers + inputMethodHints: Qt.ImhDigitsOnly property Amount textAsSats onTextChanged: { textAsSats = Config.unitsToSats(amount.text) diff --git a/electrum/gui/qml/components/controls/FiatField.qml b/electrum/gui/qml/components/controls/FiatField.qml index 3c2ed376c..fff8150e0 100644 --- a/electrum/gui/qml/components/controls/FiatField.qml +++ b/electrum/gui/qml/components/controls/FiatField.qml @@ -10,7 +10,7 @@ TextField { font.family: FixedFont placeholderText: qsTr('Amount') - inputMethodHints: Qt.ImhPreferNumbers + inputMethodHints: Qt.ImhDigitsOnly onTextChanged: { if (amountFiat.activeFocus) btcfield.text = text == '' From b8d4ccd432fd2a6bad36ce0349e1a7ee5981be70 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 15:50:38 +0000 Subject: [PATCH 0471/1143] wallet: fix get_locktime_for_new_transaction for lagging server Merchant reported that 0.5% of txs they make are rejected by the connected server due to the locktime being in the future. fixes https://github.com/spesmilo/electrum/issues/8245 --- electrum/wallet.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 33c12ceb5..9e8bd985b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -207,12 +207,21 @@ def get_locktime_for_new_transaction(network: 'Network') -> int: chain = network.blockchain() if chain.is_tip_stale(): return 0 + # figure out current block height + chain_height = chain.height() # learnt from all connected servers, SPV-checked + server_height = network.get_server_height() # height claimed by main server, unverified + # note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork) + # - if it's lagging too much, it is the network's job to switch away + if server_height < chain_height - 10: + # the diff is suspiciously large... give up and use something non-fingerprintable + return 0 # discourage "fee sniping" - locktime = chain.height() + locktime = min(chain_height, server_height) # sometimes pick locktime a bit further back, to help privacy # of setups that need more time (offline/multisig/coinjoin/...) if random.randint(0, 9) == 0: locktime = max(0, locktime - random.randint(0, 99)) + locktime = max(0, locktime) return locktime From 677e1259df9b6b31ed3b99e630ea0e728a4c32c8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 20 Mar 2023 16:52:21 +0100 Subject: [PATCH 0472/1143] qml: ElDialog now defaults to parent on Overlay.overlay This was replicated in basically all ElDialog derived dialogs --- .../gui/qml/components/ChannelOpenProgressDialog.qml | 6 ------ electrum/gui/qml/components/CloseChannelDialog.qml | 5 ----- electrum/gui/qml/components/ConfirmTxDialog.qml | 6 ------ electrum/gui/qml/components/CpfpBumpFeeDialog.qml | 6 ------ electrum/gui/qml/components/ExceptionDialog.qml | 11 ----------- electrum/gui/qml/components/ExportTxDialog.qml | 7 ------- electrum/gui/qml/components/GenericShareDialog.qml | 7 ------- .../gui/qml/components/ImportAddressesKeysDialog.qml | 6 ------ .../gui/qml/components/ImportChannelBackupDialog.qml | 5 ----- electrum/gui/qml/components/InvoiceDialog.qml | 6 ------ .../qml/components/LightningPaymentProgressDialog.qml | 6 ------ electrum/gui/qml/components/LnurlPayRequestDialog.qml | 6 ------ electrum/gui/qml/components/LoadingWalletDialog.qml | 4 ---- electrum/gui/qml/components/MessageDialog.qml | 8 +------- electrum/gui/qml/components/OpenChannelDialog.qml | 6 ------ electrum/gui/qml/components/OpenWalletDialog.qml | 6 ------ electrum/gui/qml/components/OtpDialog.qml | 6 ------ electrum/gui/qml/components/PasswordDialog.qml | 7 ------- electrum/gui/qml/components/Pin.qml | 2 -- electrum/gui/qml/components/ProxyConfigDialog.qml | 7 ------- electrum/gui/qml/components/RbfBumpFeeDialog.qml | 6 ------ electrum/gui/qml/components/RbfCancelDialog.qml | 6 ------ electrum/gui/qml/components/ReceiveDetailsDialog.qml | 7 ------- electrum/gui/qml/components/ReceiveDialog.qml | 6 ------ electrum/gui/qml/components/SendDialog.qml | 7 ------- electrum/gui/qml/components/ServerConfigDialog.qml | 7 ------- electrum/gui/qml/components/SwapDialog.qml | 6 ------ electrum/gui/qml/components/SwapProgressDialog.qml | 6 ------ electrum/gui/qml/components/controls/ElDialog.qml | 6 ++++++ electrum/gui/qml/components/wizard/Wizard.qml | 2 +- 30 files changed, 8 insertions(+), 174 deletions(-) diff --git a/electrum/gui/qml/components/ChannelOpenProgressDialog.qml b/electrum/gui/qml/components/ChannelOpenProgressDialog.qml index 65d7d0086..1094974c8 100644 --- a/electrum/gui/qml/components/ChannelOpenProgressDialog.qml +++ b/electrum/gui/qml/components/ChannelOpenProgressDialog.qml @@ -16,12 +16,6 @@ ElDialog { allowClose: false - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - property alias state: s.state property alias error: errorText.text property alias info: infoText.text diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index 129bb44b3..a5ac0d707 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -18,11 +18,6 @@ ElDialog { title: qsTr('Close Channel') iconSource: Qt.resolvedUrl('../../icons/lightning_disconnected.png') - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } property bool _closing: false closePolicy: Popup.NoAutoClose diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 73097766f..1dc5b22c9 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -30,12 +30,6 @@ ElDialog { height: parent.height padding: 0 - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - function updateAmountText() { btcValue.text = Config.formatSats(finalizer.effectiveAmount, false) fiatValue.text = Daemon.fx.enabled diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index 675c817ff..c60084f87 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -22,12 +22,6 @@ ElDialog { height: parent.height padding: 0 - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/electrum/gui/qml/components/ExceptionDialog.qml b/electrum/gui/qml/components/ExceptionDialog.qml index 843590156..5c325f2f6 100644 --- a/electrum/gui/qml/components/ExceptionDialog.qml +++ b/electrum/gui/qml/components/ExceptionDialog.qml @@ -15,12 +15,6 @@ ElDialog property bool _sending: false - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - width: parent.width height: parent.height @@ -113,11 +107,6 @@ ElDialog property string reportText z: 3000 - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } width: parent.width height: parent.height diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 7bfaec28f..b69461803 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -17,16 +17,9 @@ ElDialog { title: qsTr('Share Transaction') - parent: Overlay.overlay - modal: true - width: parent.width height: parent.height - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index edab35c96..8a9b8e49c 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -15,16 +15,9 @@ ElDialog { title: '' - parent: Overlay.overlay - modal: true - width: parent.width height: parent.height - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml index 61940b67a..10a069b19 100644 --- a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml +++ b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml @@ -15,12 +15,6 @@ ElDialog { property bool valid: false - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - width: parent.width height: parent.height diff --git a/electrum/gui/qml/components/ImportChannelBackupDialog.qml b/electrum/gui/qml/components/ImportChannelBackupDialog.qml index 036159e26..42dce5d96 100644 --- a/electrum/gui/qml/components/ImportChannelBackupDialog.qml +++ b/electrum/gui/qml/components/ImportChannelBackupDialog.qml @@ -11,11 +11,6 @@ ElDialog { property bool valid: false - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } width: parent.width height: parent.height diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index a240a632a..4e31ed965 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -21,12 +21,6 @@ ElDialog { padding: 0 - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - property bool _canMax: invoice.invoiceType == Invoice.OnchainInvoice ColumnLayout { diff --git a/electrum/gui/qml/components/LightningPaymentProgressDialog.qml b/electrum/gui/qml/components/LightningPaymentProgressDialog.qml index 326bfc41d..db771afdf 100644 --- a/electrum/gui/qml/components/LightningPaymentProgressDialog.qml +++ b/electrum/gui/qml/components/LightningPaymentProgressDialog.qml @@ -17,12 +17,6 @@ ElDialog { title: qsTr('Paying Lightning Invoice...') - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - Item { id: s state: '' diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index 7d72965f5..4bbbc5cd3 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -15,12 +15,6 @@ ElDialog { property InvoiceParser invoiceParser - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 property bool valid: comment.text.length <= invoiceParser.lnurlData['comment_allowed'] diff --git a/electrum/gui/qml/components/LoadingWalletDialog.qml b/electrum/gui/qml/components/LoadingWalletDialog.qml index a9f938350..b7c4ed5a1 100644 --- a/electrum/gui/qml/components/LoadingWalletDialog.qml +++ b/electrum/gui/qml/components/LoadingWalletDialog.qml @@ -13,11 +13,7 @@ ElDialog { title: qsTr('Loading Wallet') iconSource: Qt.resolvedUrl('../../icons/wallet.png') - modal: true parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } x: Math.floor((parent.width - implicitWidth) / 2) y: Math.floor((parent.height - implicitHeight) / 2) diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 51fd9da56..b8c10335c 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -18,16 +18,10 @@ ElDialog { signal yesClicked - parent: Overlay.overlay - modal: true z: 1 // raise z so it also covers dialogs using overlay as parent anchors.centerIn: parent - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { @@ -36,7 +30,7 @@ ElDialog { Layout.alignment: Qt.AlignHCenter TextArea { id: message - Layout.preferredWidth: Overlay.overlay.width *2/3 + Layout.preferredWidth: dialog.parent.width * 2/3 readOnly: true wrapMode: TextInput.WordWrap textFormat: richText ? TextEdit.RichText : TextEdit.PlainText diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 05ae0049c..31fe0f4ee 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -13,17 +13,11 @@ ElDialog { title: qsTr("Open Lightning Channel") iconSource: Qt.resolvedUrl('../../icons/lightning.png') - parent: Overlay.overlay - modal: true padding: 0 width: parent.width height: parent.height - Overlay.modal: Rectangle { - color: "#aa000000" - } - ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index a299c24e9..eb125e998 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -18,12 +18,6 @@ ElDialog { title: qsTr('Open Wallet') iconSource: Qt.resolvedUrl('../../icons/wallet.png') - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - focus: true width: parent.width * 4/5 diff --git a/electrum/gui/qml/components/OtpDialog.qml b/electrum/gui/qml/components/OtpDialog.qml index 41ec51f85..feb961d4e 100644 --- a/electrum/gui/qml/components/OtpDialog.qml +++ b/electrum/gui/qml/components/OtpDialog.qml @@ -18,12 +18,6 @@ ElDialog { property bool _waiting: false property string _otpError - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - focus: true ColumnLayout { diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index a15ff532f..85d2dc853 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -17,17 +17,10 @@ ElDialog { property string password property string infotext - parent: Overlay.overlay - modal: true - anchors.centerIn: parent width: parent.width * 4/5 padding: 0 - Overlay.modal: Rectangle { - color: "#aa000000" - } - ColumnLayout { id: rootLayout width: parent.width diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index b27c4a671..180c4f0c9 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -17,8 +17,6 @@ ElDialog { anchors.centerIn: parent - modal: true - parent: Overlay.overlay Overlay.modal: Rectangle { color: canCancel ? "#aa000000" : "#ff000000" } diff --git a/electrum/gui/qml/components/ProxyConfigDialog.qml b/electrum/gui/qml/components/ProxyConfigDialog.qml index 218fe3ad4..503bf57ac 100644 --- a/electrum/gui/qml/components/ProxyConfigDialog.qml +++ b/electrum/gui/qml/components/ProxyConfigDialog.qml @@ -12,16 +12,9 @@ ElDialog { title: qsTr('Proxy settings') - parent: Overlay.overlay - modal: true - width: parent.width height: parent.height - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 7f7164cbd..e5bea81bb 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -22,12 +22,6 @@ ElDialog { height: parent.height padding: 0 - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 06dd9b7d9..1f561aa88 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -21,12 +21,6 @@ ElDialog { height: parent.height padding: 0 - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index f74998937..4d954a2fe 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -17,13 +17,6 @@ ElDialog { property alias description: message.text property alias expiry: expires.currentValue - parent: Overlay.overlay - modal: true - - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 1372b9a72..d7c6553bb 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -23,14 +23,8 @@ ElDialog { property bool _ispaid: false - parent: Overlay.overlay - modal: true iconSource: Qt.resolvedUrl('../../icons/tab_receive.png') - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index d6e3471a8..49ee44afa 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -15,13 +15,6 @@ ElDialog { signal txFound(data: string) signal channelBackupFound(data: string) - parent: Overlay.overlay - modal: true - - Overlay.modal: Rectangle { - color: "#aa000000" - } - header: Item {} padding: 0 topPadding: 0 diff --git a/electrum/gui/qml/components/ServerConfigDialog.qml b/electrum/gui/qml/components/ServerConfigDialog.qml index edd16ea49..b9664220f 100644 --- a/electrum/gui/qml/components/ServerConfigDialog.qml +++ b/electrum/gui/qml/components/ServerConfigDialog.qml @@ -12,16 +12,9 @@ ElDialog { title: qsTr('Server settings') - parent: Overlay.overlay - modal: true - width: parent.width height: parent.height - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 51057709b..0a1b25ca5 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -18,12 +18,6 @@ ElDialog { title: qsTr('Lightning Swap') iconSource: Qt.resolvedUrl('../../icons/update.png') - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/SwapProgressDialog.qml b/electrum/gui/qml/components/SwapProgressDialog.qml index ec3434808..308aecdb1 100644 --- a/electrum/gui/qml/components/SwapProgressDialog.qml +++ b/electrum/gui/qml/components/SwapProgressDialog.qml @@ -20,12 +20,6 @@ ElDialog { ? qsTr('Reverse swap...') : qsTr('Swap...') - modal: true - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - Item { id: s state: '' diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index ed7865bc1..1caa8e71d 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -12,6 +12,12 @@ Dialog { close() } + parent: Overlay.overlay + modal: true + Overlay.modal: Rectangle { + color: "#aa000000" + } + closePolicy: allowClose ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index 8db489eca..c2c38329e 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -6,11 +6,11 @@ import "../controls" ElDialog { id: wizard - modal: true focus: true width: parent.width height: parent.height + padding: 0 title: wizardTitle + (pages.currentItem.title ? ' - ' + pages.currentItem.title : '') From a5c58f8aaedf9b2bb1aecccdbd863c597c66d514 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 20 Mar 2023 19:52:01 +0100 Subject: [PATCH 0473/1143] qml: layout fixes for small form-factor devices --- .../gui/qml/components/CpfpBumpFeeDialog.qml | 301 +++++++++-------- .../gui/qml/components/RbfBumpFeeDialog.qml | 317 +++++++++--------- .../gui/qml/components/RbfCancelDialog.qml | 264 ++++++++------- 3 files changed, 458 insertions(+), 424 deletions(-) diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index c60084f87..a81c235a6 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -26,185 +26,198 @@ ElDialog { anchors.fill: parent spacing: 0 - GridLayout { - Layout.preferredWidth: parent.width - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge - columns: 2 - - Label { - Layout.columnSpan: 2 - Layout.fillWidth: true - text: qsTr('A CPFP is a transaction that sends an unconfirmed output back to yourself, with a high fee. The goal is to have miners confirm the parent transaction in order to get the fee attached to the child transaction.') - wrapMode: Text.Wrap - } + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true - Label { - Layout.columnSpan: 2 - Layout.fillWidth: true - Layout.bottomMargin: constants.paddingLarge - text: qsTr('The proposed fee is computed using your fee/kB settings, applied to the total size of both child and parent transactions. After you broadcast a CPFP transaction, it is normal to see a new unconfirmed transaction in your history.') - wrapMode: Text.Wrap - } + leftMargin: constants.paddingLarge + rightMargin: constants.paddingLarge - Label { - Layout.preferredWidth: 1 - Layout.fillWidth: true - text: qsTr('Total size') - color: Material.accentColor - } + contentHeight: rootLayout.height + clip: true + interactive: height < contentHeight - Label { - Layout.preferredWidth: 1 - Layout.fillWidth: true - text: qsTr('%1 bytes').arg(cpfpfeebumper.totalSize) - } + GridLayout { + id: rootLayout + width: parent.width - Label { - text: qsTr('Input amount') - color: Material.accentColor - } + columns: 2 - FormattedAmount { - amount: cpfpfeebumper.inputAmount - } + Label { + Layout.columnSpan: 2 + Layout.fillWidth: true + text: qsTr('A CPFP is a transaction that sends an unconfirmed output back to yourself, with a high fee. The goal is to have miners confirm the parent transaction in order to get the fee attached to the child transaction.') + wrapMode: Text.Wrap + } - Label { - text: qsTr('Output amount') - color: Material.accentColor - } + Label { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + text: qsTr('The proposed fee is computed using your fee/kB settings, applied to the total size of both child and parent transactions. After you broadcast a CPFP transaction, it is normal to see a new unconfirmed transaction in your history.') + wrapMode: Text.Wrap + } - FormattedAmount { - amount: cpfpfeebumper.outputAmount - valid: cpfpfeebumper.valid - } + Label { + Layout.preferredWidth: 1 + Layout.fillWidth: true + text: qsTr('Total size') + color: Material.accentColor + } - Slider { - id: feeslider - leftPadding: constants.paddingMedium - snapMode: Slider.SnapOnRelease - stepSize: 1 - from: 0 - to: cpfpfeebumper.sliderSteps - onValueChanged: { - if (activeFocus) - cpfpfeebumper.sliderPos = value - } - Component.onCompleted: { - value = cpfpfeebumper.sliderPos - } - Connections { - target: cpfpfeebumper - function onSliderPosChanged() { - feeslider.value = cpfpfeebumper.sliderPos - } + Label { + Layout.preferredWidth: 1 + Layout.fillWidth: true + text: qsTr('%1 bytes').arg(cpfpfeebumper.totalSize) } - } - FeeMethodComboBox { - id: feemethod - feeslider: cpfpfeebumper - } + Label { + text: qsTr('Input amount') + color: Material.accentColor + } - Label { - visible: feemethod.currentValue - text: qsTr('Target') - color: Material.accentColor - } + FormattedAmount { + amount: cpfpfeebumper.inputAmount + } - Label { - visible: feemethod.currentValue - text: cpfpfeebumper.target - } + Label { + text: qsTr('Output amount') + color: Material.accentColor + } - Label { - text: qsTr('Fee for child') - color: Material.accentColor - } + FormattedAmount { + amount: cpfpfeebumper.outputAmount + valid: cpfpfeebumper.valid + } - FormattedAmount { - amount: cpfpfeebumper.feeForChild - valid: cpfpfeebumper.valid - } + RowLayout { + Layout.columnSpan: 2 + Slider { + id: feeslider + leftPadding: constants.paddingMedium + snapMode: Slider.SnapOnRelease + stepSize: 1 + from: 0 + to: cpfpfeebumper.sliderSteps + onValueChanged: { + if (activeFocus) + cpfpfeebumper.sliderPos = value + } + Component.onCompleted: { + value = cpfpfeebumper.sliderPos + } + Connections { + target: cpfpfeebumper + function onSliderPosChanged() { + feeslider.value = cpfpfeebumper.sliderPos + } + } + } - Label { - text: qsTr('Total fee') - color: Material.accentColor - } + FeeMethodComboBox { + id: feemethod + feeslider: cpfpfeebumper + } + } - FormattedAmount { - amount: cpfpfeebumper.totalFee - valid: cpfpfeebumper.valid - } + Label { + visible: feemethod.currentValue + text: qsTr('Target') + color: Material.accentColor + } - Label { - text: qsTr('Total fee rate') - color: Material.accentColor - } + Label { + visible: feemethod.currentValue + text: cpfpfeebumper.target + } - RowLayout { Label { - text: cpfpfeebumper.valid ? cpfpfeebumper.totalFeeRate : '' - font.family: FixedFont + text: qsTr('Fee for child') + color: Material.accentColor + } + + FormattedAmount { + amount: cpfpfeebumper.feeForChild + valid: cpfpfeebumper.valid } Label { - visible: cpfpfeebumper.valid - text: 'sat/vB' + text: qsTr('Total fee') color: Material.accentColor } - } - InfoTextArea { - Layout.columnSpan: 2 - Layout.preferredWidth: parent.width * 3/4 - Layout.alignment: Qt.AlignHCenter - Layout.topMargin: constants.paddingLarge - visible: cpfpfeebumper.warning != '' - text: cpfpfeebumper.warning - iconStyle: InfoTextArea.IconStyle.Warn - } + FormattedAmount { + amount: cpfpfeebumper.totalFee + valid: cpfpfeebumper.valid + } - Label { - visible: cpfpfeebumper.valid - text: qsTr('Outputs') - Layout.columnSpan: 2 - color: Material.accentColor - } + Label { + text: qsTr('Total fee rate') + color: Material.accentColor + } + + RowLayout { + Label { + text: cpfpfeebumper.valid ? cpfpfeebumper.totalFeeRate : '' + font.family: FixedFont + } - Repeater { - model: cpfpfeebumper.valid ? cpfpfeebumper.outputs : [] - delegate: TextHighlightPane { + Label { + visible: cpfpfeebumper.valid + text: 'sat/vB' + color: Material.accentColor + } + } + + InfoTextArea { Layout.columnSpan: 2 - Layout.fillWidth: true + Layout.preferredWidth: parent.width * 3/4 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingLarge + visible: cpfpfeebumper.warning != '' + text: cpfpfeebumper.warning + iconStyle: InfoTextArea.IconStyle.Warn + } - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor + Label { + visible: cpfpfeebumper.valid + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: cpfpfeebumper.valid ? cpfpfeebumper.outputs : [] + delegate: TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + RowLayout { + width: parent.width + Label { + text: modelData.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: modelData.is_mine ? constants.colorMine : Material.foreground + } + Label { + text: Config.formatSats(modelData.value_sats) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } } } } } } - Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } - FlatButton { id: sendButton Layout.fillWidth: true diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index e5bea81bb..b1d406c3d 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -26,201 +26,212 @@ ElDialog { anchors.fill: parent spacing: 0 - GridLayout { - Layout.preferredWidth: parent.width - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge - columns: 2 - - Label { - Layout.columnSpan: 2 - Layout.fillWidth: true - text: qsTr('Increase your transaction\'s fee to improve its position in the mempool') - wrapMode: Text.Wrap - } + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true - Label { - Layout.preferredWidth: 1 - Layout.fillWidth: true - text: qsTr('Method') - color: Material.accentColor - } + leftMargin: constants.paddingLarge + rightMargin: constants.paddingLarge - RowLayout { - Layout.preferredWidth: 1 - Layout.fillWidth: true - ElComboBox { - enabled: rbffeebumper.canChangeBumpMethod - - textRole: 'text' - valueRole: 'value' - - model: [ - { text: qsTr('Preserve payment'), value: 'preserve_payment' }, - { text: qsTr('Decrease payment'), value: 'decrease_payment' } - ] - onCurrentValueChanged: { - if (activeFocus) - rbffeebumper.bumpMethod = currentValue - } - Component.onCompleted: { - currentIndex = indexOfValue(rbffeebumper.bumpMethod) - } - } - Item { Layout.fillWidth: true; Layout.preferredHeight: 1 } - } + contentHeight: rootLayout.height + clip: true + interactive: height < contentHeight - Label { - Layout.preferredWidth: 1 - Layout.fillWidth: true - text: qsTr('Old fee') - color: Material.accentColor - } + GridLayout { + id: rootLayout - FormattedAmount { - Layout.preferredWidth: 1 - Layout.fillWidth: true - amount: rbffeebumper.oldfee - } + width: parent.width - Label { - text: qsTr('Old fee rate') - color: Material.accentColor - } + columns: 2 - RowLayout { Label { - id: oldfeeRate - text: rbffeebumper.oldfeeRate - font.family: FixedFont + Layout.columnSpan: 2 + Layout.fillWidth: true + text: qsTr('Increase your transaction\'s fee to improve its position in the mempool') + wrapMode: Text.Wrap } Label { - text: 'sat/vB' + Layout.preferredWidth: 1 + Layout.fillWidth: true + text: qsTr('Method') color: Material.accentColor } - } - Label { - text: qsTr('Mining fee') - color: Material.accentColor - } + RowLayout { + Layout.preferredWidth: 1 + Layout.fillWidth: true + Layout.minimumWidth: bumpMethodComboBox.implicitWidth + + ElComboBox { + id: bumpMethodComboBox + enabled: rbffeebumper.canChangeBumpMethod + + textRole: 'text' + valueRole: 'value' + + model: [ + { text: qsTr('Preserve payment'), value: 'preserve_payment' }, + { text: qsTr('Decrease payment'), value: 'decrease_payment' } + ] + onCurrentValueChanged: { + if (activeFocus) + rbffeebumper.bumpMethod = currentValue + } + Component.onCompleted: { + currentIndex = indexOfValue(rbffeebumper.bumpMethod) + } + } + Item { Layout.fillWidth: true; Layout.preferredHeight: 1 } + } - FormattedAmount { - amount: rbffeebumper.fee - valid: rbffeebumper.valid - } + Label { + Layout.preferredWidth: 1 + Layout.fillWidth: true + text: qsTr('Old fee') + color: Material.accentColor + } - Label { - text: qsTr('Fee rate') - color: Material.accentColor - } + FormattedAmount { + Layout.preferredWidth: 1 + Layout.fillWidth: true + amount: rbffeebumper.oldfee + } - RowLayout { Label { - id: feeRate - text: rbffeebumper.valid ? rbffeebumper.feeRate : '' - font.family: FixedFont + text: qsTr('Old fee rate') + color: Material.accentColor + } + + RowLayout { + Label { + id: oldfeeRate + text: rbffeebumper.oldfeeRate + font.family: FixedFont + } + + Label { + text: 'sat/vB' + color: Material.accentColor + } } Label { - visible: rbffeebumper.valid - text: 'sat/vB' + text: qsTr('Mining fee') color: Material.accentColor } - } - Label { - text: qsTr('Target') - color: Material.accentColor - } + FormattedAmount { + amount: rbffeebumper.fee + valid: rbffeebumper.valid + } - Label { - id: targetdesc - text: rbffeebumper.target - } + Label { + text: qsTr('Fee rate') + color: Material.accentColor + } - RowLayout { - Layout.columnSpan: 2 - Layout.fillWidth: true - Slider { - id: feeslider - Layout.fillWidth: true - leftPadding: constants.paddingMedium - snapMode: Slider.SnapOnRelease - stepSize: 1 - from: 0 - to: rbffeebumper.sliderSteps - onValueChanged: { - if (activeFocus) - rbffeebumper.sliderPos = value + RowLayout { + Label { + id: feeRate + text: rbffeebumper.valid ? rbffeebumper.feeRate : '' + font.family: FixedFont } - Component.onCompleted: { - value = rbffeebumper.sliderPos - } - Connections { - target: rbffeebumper - function onSliderPosChanged() { - feeslider.value = rbffeebumper.sliderPos - } + + Label { + visible: rbffeebumper.valid + text: 'sat/vB' + color: Material.accentColor } } - FeeMethodComboBox { - id: target - feeslider: rbffeebumper + Label { + text: qsTr('Target') + color: Material.accentColor } - } - InfoTextArea { - Layout.columnSpan: 2 - Layout.preferredWidth: parent.width * 3/4 - Layout.alignment: Qt.AlignHCenter - visible: rbffeebumper.warning != '' - text: rbffeebumper.warning - iconStyle: InfoTextArea.IconStyle.Warn - } + Label { + id: targetdesc + text: rbffeebumper.target + } - Label { - visible: rbffeebumper.valid - text: qsTr('Outputs') - Layout.columnSpan: 2 - color: Material.accentColor - } + RowLayout { + Layout.columnSpan: 2 + Slider { + id: feeslider + leftPadding: constants.paddingMedium + snapMode: Slider.SnapOnRelease + stepSize: 1 + from: 0 + to: rbffeebumper.sliderSteps + onValueChanged: { + if (activeFocus) + rbffeebumper.sliderPos = value + } + Component.onCompleted: { + value = rbffeebumper.sliderPos + } + Connections { + target: rbffeebumper + function onSliderPosChanged() { + feeslider.value = rbffeebumper.sliderPos + } + } + } - Repeater { - model: rbffeebumper.valid ? rbffeebumper.outputs : [] - delegate: TextHighlightPane { + FeeMethodComboBox { + id: target + feeslider: rbffeebumper + } + } + + InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true + visible: rbffeebumper.warning != '' + text: rbffeebumper.warning + iconStyle: InfoTextArea.IconStyle.Warn + } - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor + Label { + visible: rbffeebumper.valid + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: rbffeebumper.valid ? rbffeebumper.outputs : [] + delegate: TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + RowLayout { + width: parent.width + Label { + text: modelData.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: modelData.is_mine ? constants.colorMine : Material.foreground + } + Label { + text: Config.formatSats(modelData.value_sats) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } } } } } } - Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } - FlatButton { id: sendButton Layout.fillWidth: true diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 1f561aa88..e083a04e5 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -25,173 +25,183 @@ ElDialog { anchors.fill: parent spacing: 0 - GridLayout { + Flickable { Layout.fillWidth: true - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge + Layout.fillHeight: true - columns: 2 + leftMargin: constants.paddingLarge + rightMargin: constants.paddingLarge - Label { - Layout.columnSpan: 2 - Layout.fillWidth: true - text: qsTr('Cancel an unconfirmed RBF transaction by double-spending its inputs back to your wallet with a higher fee.') - wrapMode: Text.Wrap - } - - Label { - text: qsTr('Old fee') - color: Material.accentColor - } - - FormattedAmount { - amount: txcanceller.oldfee - } + contentHeight: rootLayout.height + clip: true + interactive: height < contentHeight - Label { - text: qsTr('Old fee rate') - color: Material.accentColor - } + GridLayout { + id: rootLayout + width: parent.width + columns: 2 - RowLayout { Label { - id: oldfeeRate - text: txcanceller.oldfeeRate - font.family: FixedFont + Layout.columnSpan: 2 + Layout.fillWidth: true + text: qsTr('Cancel an unconfirmed RBF transaction by double-spending its inputs back to your wallet with a higher fee.') + wrapMode: Text.Wrap } Label { - text: 'sat/vB' + text: qsTr('Old fee') color: Material.accentColor } - } - Label { - text: qsTr('Mining fee') - color: Material.accentColor - } + FormattedAmount { + amount: txcanceller.oldfee + } - FormattedAmount { - amount: txcanceller.fee - valid: txcanceller.valid - } + Label { + text: qsTr('Old fee rate') + color: Material.accentColor + } - Label { - text: qsTr('Fee rate') - color: Material.accentColor - } + RowLayout { + Label { + id: oldfeeRate + text: txcanceller.oldfeeRate + font.family: FixedFont + } - RowLayout { - Label { - id: feeRate - text: txcanceller.valid ? txcanceller.feeRate : '' - font.family: FixedFont + Label { + text: 'sat/vB' + color: Material.accentColor + } } Label { - visible: txcanceller.valid - text: 'sat/vB' + text: qsTr('Mining fee') color: Material.accentColor } - } - Label { - text: qsTr('Target') - color: Material.accentColor - } + FormattedAmount { + amount: txcanceller.fee + valid: txcanceller.valid + } - Label { - id: targetdesc - text: txcanceller.target - } + Label { + text: qsTr('Fee rate') + color: Material.accentColor + } - Slider { - id: feeslider - leftPadding: constants.paddingMedium - snapMode: Slider.SnapOnRelease - stepSize: 1 - from: 0 - to: txcanceller.sliderSteps - onValueChanged: { - if (activeFocus) - txcanceller.sliderPos = value - } - Component.onCompleted: { - value = txcanceller.sliderPos - } - Connections { - target: txcanceller - function onSliderPosChanged() { - feeslider.value = txcanceller.sliderPos + RowLayout { + Label { + id: feeRate + text: txcanceller.valid ? txcanceller.feeRate : '' + font.family: FixedFont + } + + Label { + visible: txcanceller.valid + text: 'sat/vB' + color: Material.accentColor } } - } - FeeMethodComboBox { - id: target - feeslider: txcanceller - } + Label { + text: qsTr('Target') + color: Material.accentColor + } - CheckBox { - id: final_cb - text: qsTr('Replace-by-Fee') - Layout.columnSpan: 2 - checked: txcanceller.rbf - onCheckedChanged: { - if (activeFocus) - txcanceller.rbf = checked + Label { + id: targetdesc + text: txcanceller.target } - } - InfoTextArea { - Layout.columnSpan: 2 - Layout.preferredWidth: parent.width * 3/4 - Layout.alignment: Qt.AlignHCenter - visible: txcanceller.warning != '' - text: txcanceller.warning - iconStyle: InfoTextArea.IconStyle.Warn - } + RowLayout { + Layout.columnSpan: 2 + Slider { + id: feeslider + leftPadding: constants.paddingMedium + snapMode: Slider.SnapOnRelease + stepSize: 1 + from: 0 + to: txcanceller.sliderSteps + onValueChanged: { + if (activeFocus) + txcanceller.sliderPos = value + } + Component.onCompleted: { + value = txcanceller.sliderPos + } + Connections { + target: txcanceller + function onSliderPosChanged() { + feeslider.value = txcanceller.sliderPos + } + } + } - Label { - visible: txcanceller.valid - text: qsTr('Outputs') - Layout.columnSpan: 2 - color: Material.accentColor - } + FeeMethodComboBox { + id: target + feeslider: txcanceller + } + } - Repeater { - model: txcanceller.valid ? txcanceller.outputs : [] - delegate: TextHighlightPane { + CheckBox { + id: final_cb + text: qsTr('Replace-by-Fee') + Layout.columnSpan: 2 + checked: txcanceller.rbf + onCheckedChanged: { + if (activeFocus) + txcanceller.rbf = checked + } + } + + InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true + visible: txcanceller.warning != '' + text: txcanceller.warning + iconStyle: InfoTextArea.IconStyle.Warn + } - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor + Label { + visible: txcanceller.valid + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: txcanceller.valid ? txcanceller.outputs : [] + delegate: TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + + RowLayout { + width: parent.width + Label { + text: modelData.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: modelData.is_mine ? constants.colorMine : Material.foreground + } + Label { + text: Config.formatSats(modelData.value_sats) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } } } } } } - Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } - FlatButton { id: confirmButton Layout.fillWidth: true From aac9afa7c15dee062e2676217430e12dcf1e8e37 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 17:49:50 +0000 Subject: [PATCH 0474/1143] tests: add a failing testcase for daemon.update_password_for_directory related https://github.com/spesmilo/electrum/issues/8259 --- electrum/tests/__init__.py | 1 + electrum/tests/test_daemon.py | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 electrum/tests/test_daemon.py diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py index 218a0db67..6ef060e7d 100644 --- a/electrum/tests/__init__.py +++ b/electrum/tests/__init__.py @@ -50,6 +50,7 @@ def setUp(self): self.electrum_path = tempfile.mkdtemp() async def asyncSetUp(self): + await super().asyncSetUp() loop = util.get_asyncio_loop() # IsolatedAsyncioTestCase creates event loops with debug=True, which makes the tests take ~4x time if not (os.environ.get("PYTHONASYNCIODEBUG") or os.environ.get("PYTHONDEVMODE")): diff --git a/electrum/tests/test_daemon.py b/electrum/tests/test_daemon.py new file mode 100644 index 000000000..28a5e6198 --- /dev/null +++ b/electrum/tests/test_daemon.py @@ -0,0 +1,41 @@ +import os + +from electrum.daemon import Daemon +from electrum.simple_config import SimpleConfig +from electrum.wallet import restore_wallet_from_text, Abstract_Wallet, Standard_Wallet +from electrum import util + +from . import ElectrumTestCase + + +class TestUnifiedPassword(ElectrumTestCase): + config: 'SimpleConfig' + + def setUp(self): + super().setUp() + self.config = SimpleConfig({'electrum_path': self.electrum_path}) + self.config.set_key("single_password", True) + + self.wallet_dir = os.path.dirname(self.config.get_wallet_path()) + assert "wallets" == os.path.basename(self.wallet_dir) + + async def asyncSetUp(self): + await super().asyncSetUp() + self.daemon = Daemon(config=self.config, listen_jsonrpc=False) + + async def asyncTearDown(self): + await self.daemon.stop() + await super().asyncTearDown() + + async def test_update_password_for_directory(self): + wallet1: Standard_Wallet = restore_wallet_from_text( + "9dk", path=f"{self.wallet_dir}/w1", password=None, gap_limit=2, config=self.config)['wallet'] + wallet2: Standard_Wallet = restore_wallet_from_text( + "x8", path=f"{self.wallet_dir}/w2", password="123456", gap_limit=2, config=self.config)['wallet'] + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) + self.assertEqual((True, False), (can_be_unified, is_unified)) + is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") + self.assertTrue(is_unified) + + + From 11f06d860e3f0afca6a842a7fe2a6503fee1652a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 19:54:53 +0000 Subject: [PATCH 0475/1143] tests: add more tests for daemon.update_password_for_directory --- electrum/daemon.py | 5 ++ electrum/tests/test_daemon.py | 155 ++++++++++++++++++++++++++++++++-- electrum/wallet.py | 15 +++- 3 files changed, 164 insertions(+), 11 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index ccedd9248..96ac2cf7d 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -604,6 +604,11 @@ def _check_password_for_directory(self, *, old_password, new_password=None, wall if not os.path.isfile(path): continue wallet = self.get_wallet(path) + # note: we only create a new wallet object if one was not loaded into the wallet already. + # This is to avoid having two wallet objects contending for the same file. + # Take care: this only works if the daemon knows about all wallet objects. + # if other code already has created a Wallet() for a file but did not tell the daemon, + # hard-to-understand bugs will follow... if wallet is None: try: wallet = self._load_wallet(path, old_password, manual_upgrades=False, config=self.config) diff --git a/electrum/tests/test_daemon.py b/electrum/tests/test_daemon.py index 28a5e6198..688890880 100644 --- a/electrum/tests/test_daemon.py +++ b/electrum/tests/test_daemon.py @@ -1,11 +1,12 @@ import os +from typing import Optional, Iterable from electrum.daemon import Daemon from electrum.simple_config import SimpleConfig -from electrum.wallet import restore_wallet_from_text, Abstract_Wallet, Standard_Wallet +from electrum.wallet import restore_wallet_from_text from electrum import util -from . import ElectrumTestCase +from . import ElectrumTestCase, as_testnet class TestUnifiedPassword(ElectrumTestCase): @@ -15,6 +16,7 @@ def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) self.config.set_key("single_password", True) + self.config.set_key("offline", True) self.wallet_dir = os.path.dirname(self.config.get_wallet_path()) assert "wallets" == os.path.basename(self.wallet_dir) @@ -22,20 +24,157 @@ def setUp(self): async def asyncSetUp(self): await super().asyncSetUp() self.daemon = Daemon(config=self.config, listen_jsonrpc=False) + assert self.daemon.network is None async def asyncTearDown(self): await self.daemon.stop() await super().asyncTearDown() - async def test_update_password_for_directory(self): - wallet1: Standard_Wallet = restore_wallet_from_text( - "9dk", path=f"{self.wallet_dir}/w1", password=None, gap_limit=2, config=self.config)['wallet'] - wallet2: Standard_Wallet = restore_wallet_from_text( - "x8", path=f"{self.wallet_dir}/w2", password="123456", gap_limit=2, config=self.config)['wallet'] + def _restore_wallet_from_text(self, text, *, password: Optional[str], encrypt_file: bool = None) -> str: + """Returns path for created wallet.""" + basename = util.get_new_wallet_name(self.wallet_dir) + path = os.path.join(self.wallet_dir, basename) + wallet_dict = restore_wallet_from_text( + text, + path=path, + password=password, + encrypt_file=encrypt_file, + gap_limit=2, + config=self.config, + ) + # We return the path instead of the wallet object, as extreme + # care would be needed to use the wallet object directly: + # Unless the daemon knows about it, daemon._load_wallet might create a conflicting wallet object + # for the same fs path, and there would be two wallet objects contending for the same file. + return path + + def _run_post_unif_sanity_checks(self, paths: Iterable[str], *, password: str): + for path in paths: + w = self.daemon.load_wallet(path, password) + self.assertIsNotNone(w) + w.check_password(password) + self.assertTrue(w.has_storage_encryption()) + if w.can_have_keystore_encryption(): + self.assertTrue(w.has_keystore_encryption()) + if w.has_seed(): + self.assertIsInstance(w.get_seed(password), str) + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password=password, wallet_dir=self.wallet_dir) + self.assertEqual((True, True), (can_be_unified, is_unified)) + + # "cannot unify pw" tests ---> + + async def test_cannot_unify_two_std_wallets_both_have_ks_and_sto_enc(self): + path1 = self._restore_wallet_from_text("9dk", password="123456", encrypt_file=True) + path2 = self._restore_wallet_from_text("x8", password="asdasd", encrypt_file=True) + with open(path1, "rb") as f: + raw1_before = f.read() + with open(path2, "rb") as f: + raw2_before = f.read() + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) - self.assertEqual((True, False), (can_be_unified, is_unified)) + self.assertEqual((False, False), (can_be_unified, is_unified)) + is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") + self.assertFalse(is_unified) + # verify that files on disk haven't changed: + with open(path1, "rb") as f: + raw1_after = f.read() + with open(path2, "rb") as f: + raw2_after = f.read() + self.assertEqual(raw1_before, raw1_after) + self.assertEqual(raw2_before, raw2_after) + + async def test_cannot_unify_mixed_wallets(self): + path1 = self._restore_wallet_from_text("9dk", password="123456", encrypt_file=True) + path2 = self._restore_wallet_from_text("9dk", password="asdasd", encrypt_file=False) + path3 = self._restore_wallet_from_text("9dk", password=None) + with open(path1, "rb") as f: + raw1_before = f.read() + with open(path2, "rb") as f: + raw2_before = f.read() + with open(path3, "rb") as f: + raw3_before = f.read() + + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) + self.assertEqual((False, False), (can_be_unified, is_unified)) + is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") + self.assertFalse(is_unified) + # verify that files on disk haven't changed: + with open(path1, "rb") as f: + raw1_after = f.read() + with open(path2, "rb") as f: + raw2_after = f.read() + with open(path3, "rb") as f: + raw3_after = f.read() + self.assertEqual(raw1_before, raw1_after) + self.assertEqual(raw2_before, raw2_after) + self.assertEqual(raw3_before, raw3_after) + + # "can unify pw" tests ---> + + async def test_can_unify_two_std_wallets_both_have_ks_and_sto_enc(self): + path1 = self._restore_wallet_from_text("9dk", password="123456", encrypt_file=True) + path2 = self._restore_wallet_from_text("x8", password="123456", encrypt_file=True) + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) + self.assertEqual((True, True), (can_be_unified, is_unified)) is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") self.assertTrue(is_unified) + self._run_post_unif_sanity_checks([path1, path2], password="123456") + async def test_can_unify_two_std_wallets_one_has_ks_enc_other_has_both_enc(self): + path1 = self._restore_wallet_from_text("9dk", password="123456", encrypt_file=True) + path2 = self._restore_wallet_from_text("x8", password="123456", encrypt_file=False) + with open(path2, "rb") as f: + raw2_before = f.read() + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) + self.assertEqual((True, False), (can_be_unified, is_unified)) + is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") + self.assertTrue(is_unified) + self._run_post_unif_sanity_checks([path1, path2], password="123456") + # verify that file at path2 changed: + with open(path2, "rb") as f: + raw2_after = f.read() + self.assertNotEqual(raw2_before, raw2_after) + async def test_can_unify_two_std_wallets_one_without_password(self): + path1 = self._restore_wallet_from_text("9dk", password=None) + path2 = self._restore_wallet_from_text("x8", password="123456", encrypt_file=True) + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) + self.assertEqual((True, False), (can_be_unified, is_unified)) + is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") + self.assertTrue(is_unified) + self._run_post_unif_sanity_checks([path1, path2], password="123456") + + @as_testnet + async def test_can_unify_large_folder_yet_to_be_unified(self): + paths = [] + # seed + paths.append(self._restore_wallet_from_text("9dk", password="123456", encrypt_file=True)) + paths.append(self._restore_wallet_from_text("9dk", password="123456", encrypt_file=False)) + paths.append(self._restore_wallet_from_text("9dk", password=None)) + # xpub + xpub = "vpub5UqWay427dCjkpE3gPKLnkBUqDRoBed1328uNrLDoTyKo6HFSs9agfDMy1VXbVtcuBVRiAZQsPPsPdu1Ge8m8qvNZPyzJ4ecPsf6U1ieW4x" + paths.append(self._restore_wallet_from_text(xpub, password="123456", encrypt_file=True)) + paths.append(self._restore_wallet_from_text(xpub, password="123456", encrypt_file=False)) + paths.append(self._restore_wallet_from_text(xpub, password=None)) + # xprv + xprv = "vprv9FrABTX8HFeSYL9aaMnLRcEkHBbJnBu9foDJaTvcF8SLvHx6uKqL8rtt7kTd66V4QPLfWPaCJMVZa3h9zuzLr7YFZd1uoEevqqyxp66oSbN" + paths.append(self._restore_wallet_from_text(xprv, password="123456", encrypt_file=True)) + paths.append(self._restore_wallet_from_text(xprv, password="123456", encrypt_file=False)) + paths.append(self._restore_wallet_from_text(xprv, password=None)) + # WIFs + wifs= "p2wpkh:cRyfp9nJ8soK1bBUJAcWbMrsJZxKJpe7HBSxz5uXVbwydvUxz9zT p2wpkh:cV6J6T2AG4oXAXdYHAV6dbzR41QnGumDSVvWrmj2yYpos81RtyBK" + paths.append(self._restore_wallet_from_text(wifs, password="123456", encrypt_file=True)) + paths.append(self._restore_wallet_from_text(wifs, password="123456", encrypt_file=False)) + paths.append(self._restore_wallet_from_text(wifs, password=None)) + # addrs + addrs = "tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd" + paths.append(self._restore_wallet_from_text(addrs, password="123456", encrypt_file=True)) + paths.append(self._restore_wallet_from_text(addrs, password="123456", encrypt_file=False)) + paths.append(self._restore_wallet_from_text(addrs, password=None)) + # do unification + can_be_unified, is_unified = self.daemon._check_password_for_directory(old_password="123456", wallet_dir=self.wallet_dir) + self.assertEqual((True, False), (can_be_unified, is_unified)) + is_unified = self.daemon.update_password_for_directory(old_password="123456", new_password="123456") + self.assertTrue(is_unified) + self._run_post_unif_sanity_checks(paths, password="123456") diff --git a/electrum/wallet.py b/electrum/wallet.py index 9e8bd985b..b0c9f6813 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -3591,9 +3591,16 @@ def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=N return {'seed': seed, 'wallet': wallet, 'msg': msg} -def restore_wallet_from_text(text, *, path: Optional[str], config: SimpleConfig, - passphrase=None, password=None, encrypt_file=True, - gap_limit=None) -> dict: +def restore_wallet_from_text( + text: str, + *, + path: Optional[str], + config: SimpleConfig, + passphrase: Optional[str] = None, + password: Optional[str] = None, + encrypt_file: Optional[bool] = None, + gap_limit: Optional[int] = None, +) -> dict: """Restore a wallet from text. Text can be a seed phrase, a master public key, a master private key, a list of bitcoin addresses or bitcoin private keys.""" @@ -3603,6 +3610,8 @@ def restore_wallet_from_text(text, *, path: Optional[str], config: SimpleConfig, storage = WalletStorage(path) if storage.file_exists(): raise Exception("Remove the existing wallet first!") + if encrypt_file is None: + encrypt_file = True db = WalletDB('', manual_upgrades=False) text = text.strip() if keystore.is_address_list(text): From 9df5f55a1f89f40241a0ce81a896a79323427877 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 20:00:28 +0000 Subject: [PATCH 0476/1143] password unification: bugfix, now passes test cases fixes https://github.com/spesmilo/electrum/issues/8259 note that technically this is an API change for - wallet.check_password - wallet.update_password - storage.check_password --- electrum/daemon.py | 9 +++++++-- electrum/storage.py | 5 ++++- electrum/wallet.py | 7 ++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 96ac2cf7d..f6efa490e 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -623,13 +623,18 @@ def _check_password_for_directory(self, *, old_password, new_password=None, wall if not wallet.storage.is_encrypted(): is_unified = False try: - wallet.check_password(old_password) + try: + wallet.check_password(old_password) + old_password_real = old_password + except util.InvalidPassword: + wallet.check_password(None) + old_password_real = None except Exception: failed.append(path) continue if new_password: self.logger.info(f'updating password for wallet: {path!r}') - wallet.update_password(old_password, new_password, encrypt_storage=True) + wallet.update_password(old_password_real, new_password, encrypt_storage=True) can_be_unified = failed == [] is_unified = can_be_unified and is_unified return can_be_unified, is_unified diff --git a/electrum/storage.py b/electrum/storage.py index fbf7cbe70..2eecc7eb0 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -29,6 +29,7 @@ import base64 import zlib from enum import IntEnum +from typing import Optional from . import ecc from .util import (profiler, InvalidPassword, WalletFileException, bfh, standardize_path, @@ -186,9 +187,11 @@ def encrypt_before_writing(self, plaintext: str) -> str: s = s.decode('utf8') return s - def check_password(self, password) -> None: + def check_password(self, password: Optional[str]) -> None: """Raises an InvalidPassword exception on invalid password""" if not self.is_encrypted(): + if password is not None: + raise InvalidPassword("password given but wallet has no password") return if not self.is_past_initial_decryption(): self.decrypt(password) # this sets self.pubkey diff --git a/electrum/wallet.py b/electrum/wallet.py index b0c9f6813..9d7d68925 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2682,7 +2682,12 @@ def has_storage_encryption(self) -> bool: def may_have_password(cls): return True - def check_password(self, password): + def check_password(self, password: Optional[str]) -> None: + """Raises an InvalidPassword exception on invalid password""" + if not self.has_password(): + if password is not None: + raise InvalidPassword("password given but wallet has no password") + return if self.has_keystore_encryption(): self.keystore.check_password(password) if self.has_storage_encryption(): From c9b6a6c01e20f66c9d00d517d42947e991b50950 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 02:02:14 +0000 Subject: [PATCH 0477/1143] build: fix repro builds where host userid != 1000 - repro builds to use fixed uid=1000 inside the container - in case the file permissions leak into the binaries, they are still reproducible - chown 1000:1000 fresh_clone - repro builds to create fresh_clone dir outside git clone - otherwise the local dev build would still interact with the fresh_clone dir - due to e.g. recursive "find -exec touch", - and even the "docker build" cmd itself would try to stat/read it - see https://github.com/docker/for-linux/issues/380 - and "rm -rf fresh_clone" needs sudo if the host uid is not 1000 - this way the local dev build does not need sudo to recap: - local dev builds use the host userid inside the container, directly operate on the project dir - does not need sudo - repro builds create a fresh git clone, chown it to 1000, and use userid=1000 inside the container - if the host userid is 1000, does not need sudo - otherwise, needs sudo closes https://github.com/spesmilo/electrum/issues/8261 --- .gitignore | 4 ---- contrib/android/build.sh | 17 ++++++++++++----- contrib/build-linux/appimage/.dockerignore | 1 - contrib/build-linux/appimage/build.sh | 17 ++++++++++++----- contrib/build-linux/sdist/.dockerignore | 1 - contrib/build-linux/sdist/build.sh | 17 ++++++++++++----- contrib/build-wine/.dockerignore | 1 - contrib/build-wine/build.sh | 17 ++++++++++++----- 8 files changed, 48 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 739ef9783..8ac25d80a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,14 +34,10 @@ contrib/build-wine/build/ contrib/build-wine/.cache/ contrib/build-wine/dist/ contrib/build-wine/signed/ -contrib/build-wine/fresh_clone/ -contrib/build-linux/sdist/fresh_clone/ contrib/build-linux/appimage/build/ contrib/build-linux/appimage/.cache/ -contrib/build-linux/appimage/fresh_clone/ contrib/osx/.cache/ contrib/osx/build-venv/ -contrib/android/fresh_clone contrib/android/android_debug.keystore contrib/secp256k1/ contrib/zbar/ diff --git a/contrib/android/build.sh b/contrib/android/build.sh index 7e4120314..0c58faaf8 100755 --- a/contrib/android/build.sh +++ b/contrib/android/build.sh @@ -52,11 +52,11 @@ docker build \ # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." - FRESH_CLONE="$CONTRIB_ANDROID/fresh_clone/electrum" && \ - rm -rf "$FRESH_CLONE" && \ - umask 0022 && \ - git clone "$PROJECT_ROOT" "$FRESH_CLONE" && \ - cd "$FRESH_CLONE" + FRESH_CLONE="/tmp/electrum_build/android/fresh_clone/electrum" + rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) + umask 0022 + git clone "$PROJECT_ROOT" "$FRESH_CLONE" + cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else @@ -72,6 +72,13 @@ fi info "building binary..." mkdir --parents "$PROJECT_ROOT_OR_FRESHCLONE_ROOT"/.buildozer/.gradle +# check uid and maybe chown. see #8261 +if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) + if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then + info "need to chown -R FRESH_CLONE dir. prompting for sudo." + sudo chown -R 1000:1000 "$FRESH_CLONE" + fi +fi docker run -it --rm \ --name electrum-android-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/home/user/wspace/electrum \ diff --git a/contrib/build-linux/appimage/.dockerignore b/contrib/build-linux/appimage/.dockerignore index d75fb8304..a4fb4fb12 100644 --- a/contrib/build-linux/appimage/.dockerignore +++ b/contrib/build-linux/appimage/.dockerignore @@ -1,3 +1,2 @@ build/ .cache/ -fresh_clone/ diff --git a/contrib/build-linux/appimage/build.sh b/contrib/build-linux/appimage/build.sh index be2184d77..a6f5cabeb 100755 --- a/contrib/build-linux/appimage/build.sh +++ b/contrib/build-linux/appimage/build.sh @@ -35,11 +35,11 @@ docker build \ # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." - FRESH_CLONE="$CONTRIB_APPIMAGE/fresh_clone/electrum" && \ - rm -rf "$FRESH_CLONE" && \ - umask 0022 && \ - git clone "$PROJECT_ROOT" "$FRESH_CLONE" && \ - cd "$FRESH_CLONE" + FRESH_CLONE="/tmp/electrum_build/appimage/fresh_clone/electrum" + rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) + umask 0022 + git clone "$PROJECT_ROOT" "$FRESH_CLONE" + cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else @@ -47,6 +47,13 @@ else fi info "building binary..." +# check uid and maybe chown. see #8261 +if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) + if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then + info "need to chown -R FRESH_CLONE dir. prompting for sudo." + sudo chown -R 1000:1000 "$FRESH_CLONE" + fi +fi docker run -it \ --name electrum-appimage-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/opt/electrum \ diff --git a/contrib/build-linux/sdist/.dockerignore b/contrib/build-linux/sdist/.dockerignore index d364c6400..e69de29bb 100644 --- a/contrib/build-linux/sdist/.dockerignore +++ b/contrib/build-linux/sdist/.dockerignore @@ -1 +0,0 @@ -fresh_clone/ diff --git a/contrib/build-linux/sdist/build.sh b/contrib/build-linux/sdist/build.sh index af895b01b..11a746273 100755 --- a/contrib/build-linux/sdist/build.sh +++ b/contrib/build-linux/sdist/build.sh @@ -35,11 +35,11 @@ docker build \ # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." - FRESH_CLONE="$CONTRIB_SDIST/fresh_clone/electrum" && \ - rm -rf "$FRESH_CLONE" && \ - umask 0022 && \ - git clone "$PROJECT_ROOT" "$FRESH_CLONE" && \ - cd "$FRESH_CLONE" + FRESH_CLONE="/tmp/electrum_build/sdist/fresh_clone/electrum" + rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) + umask 0022 + git clone "$PROJECT_ROOT" "$FRESH_CLONE" + cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else @@ -47,6 +47,13 @@ else fi info "building binary..." +# check uid and maybe chown. see #8261 +if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) + if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then + info "need to chown -R FRESH_CLONE dir. prompting for sudo." + sudo chown -R 1000:1000 "$FRESH_CLONE" + fi +fi docker run -it \ --name electrum-sdist-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/opt/electrum \ diff --git a/contrib/build-wine/.dockerignore b/contrib/build-wine/.dockerignore index f1aa3647c..a3e70a019 100644 --- a/contrib/build-wine/.dockerignore +++ b/contrib/build-wine/.dockerignore @@ -3,4 +3,3 @@ build/ .cache/ dist/ signed/ -fresh_clone/ diff --git a/contrib/build-wine/build.sh b/contrib/build-wine/build.sh index 06b503e46..f3bc4e6a9 100755 --- a/contrib/build-wine/build.sh +++ b/contrib/build-wine/build.sh @@ -37,11 +37,11 @@ docker build \ # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." - FRESH_CLONE="$CONTRIB_WINE/fresh_clone/electrum" && \ - rm -rf "$FRESH_CLONE" && \ - umask 0022 && \ - git clone "$PROJECT_ROOT" "$FRESH_CLONE" && \ - cd "$FRESH_CLONE" + FRESH_CLONE="/tmp/electrum_build/windows/fresh_clone/electrum" + rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) + umask 0022 + git clone "$PROJECT_ROOT" "$FRESH_CLONE" + cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else @@ -49,6 +49,13 @@ else fi info "building binary..." +# check uid and maybe chown. see #8261 +if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) + if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then + info "need to chown -R FRESH_CLONE dir. prompting for sudo." + sudo chown -R 1000:1000 "$FRESH_CLONE" + fi +fi docker run -it \ --name electrum-wine-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/opt/wine64/drive_c/electrum \ From ce6e4d99e79f0808fc4041c43d8e4266fe8b5e54 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 21 Mar 2023 08:04:03 +0100 Subject: [PATCH 0478/1143] Qt history_list: disable summary if fx history is not available --- electrum/gui/qt/history_list.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index c274b997f..2770091c8 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -552,7 +552,7 @@ def create_toolbar(self, config): menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar(self.config)) self.menu_fiat = menu.addConfig(_('Show Fiat Values'), 'history_rates', False, callback=self.main_window.app.update_fiat_signal.emit) self.menu_capgains = menu.addConfig(_('Show Capital Gains'), 'history_rates_capital_gains', False, callback=self.main_window.app.update_fiat_signal.emit) - menu.addAction(_("&Summary"), self.show_summary) + self.menu_summary = menu.addAction(_("&Summary"), self.show_summary) menu.addAction(_("&Plot"), self.plot_history_dialog) menu.addAction(_("&Export"), self.export_history_dialog) hbox = self.create_toolbar_buttons() @@ -566,6 +566,7 @@ def update_toolbar_menu(self): # setChecked because has_history can be modified through settings dialog self.menu_fiat.setChecked(fx and fx.has_history()) self.menu_capgains.setEnabled(fx and fx.has_history()) + self.menu_summary.setEnabled(fx and fx.has_history()) def get_toolbar_buttons(self): return self.period_combo, self.start_button, self.end_button From 077ea9a4a59fc3d39500688732401326f6b8d4c5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 21 Mar 2023 09:44:40 +0100 Subject: [PATCH 0479/1143] qml: AddressDetails heading --- electrum/gui/qml/components/AddressDetails.qml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index 6077225d2..cb58e668c 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -39,6 +39,11 @@ Pane { columns: 2 + Heading { + Layout.columnSpan: 2 + text: qsTr('Address details') + } + Label { text: qsTr('Address') Layout.columnSpan: 2 From 3cf732cb5116beae84f2b287659c342c8ad79305 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 21 Mar 2023 09:44:53 +0100 Subject: [PATCH 0480/1143] qml: introduction text bottom margins in RbF bump fee and cancel dialogs --- electrum/gui/qml/components/RbfBumpFeeDialog.qml | 1 + electrum/gui/qml/components/RbfCancelDialog.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index b1d406c3d..9d2c514c1 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -47,6 +47,7 @@ ElDialog { Label { Layout.columnSpan: 2 Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge text: qsTr('Increase your transaction\'s fee to improve its position in the mempool') wrapMode: Text.Wrap } diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index e083a04e5..899c10dc1 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -44,6 +44,7 @@ ElDialog { Label { Layout.columnSpan: 2 Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge text: qsTr('Cancel an unconfirmed RBF transaction by double-spending its inputs back to your wallet with a higher fee.') wrapMode: Text.Wrap } From a080e5130f392b95988e9a82739cd2cf6b0035d6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 21 Mar 2023 08:42:30 +0100 Subject: [PATCH 0481/1143] Qt receive_tab: toggle_view_button instead of tabs --- electrum/gui/qt/receive_tab.py | 197 +++++++++++++++------------------ electrum/gui/qt/util.py | 67 ----------- 2 files changed, 92 insertions(+), 172 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index a302d7d45..4093e9431 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -73,18 +73,16 @@ def __init__(self, window: 'ElectrumWindow'): buttons.addWidget(self.create_invoice_button) grid.addLayout(buttons, 4, 0, 1, -1) - self.receive_address_e = QTextEdit() - self.receive_address_help_text = WWLabel('') - vbox = QVBoxLayout() - vbox.addWidget(self.receive_address_help_text) - self.receive_address_help = FramedWidget() - self.receive_address_help.setVisible(False) - self.receive_address_help.setLayout(vbox) - - self.receive_URI_e = QTextEdit() - self.receive_URI_help = WWLabel('') - self.receive_lightning_e = QTextEdit() - self.receive_lightning_help_text = WWLabel('') + self.receive_e = QTextEdit() + self.receive_e.setFont(QFont(MONOSPACE_FONT)) + self.receive_e.setReadOnly(True) + self.receive_e.setContextMenuPolicy(Qt.NoContextMenu) + self.receive_e.setTextInteractionFlags(Qt.NoTextInteraction) + self.receive_e.textChanged.connect(self.update_receive_widgets) + + self.receive_qr = QRCodeWidget(manual_size=True) + + self.receive_help_text = WWLabel('') self.receive_rebalance_button = QPushButton('Rebalance') self.receive_rebalance_button.suggestion = None def on_receive_rebalance(): @@ -103,44 +101,19 @@ def on_receive_swap(): buttons.addWidget(self.receive_rebalance_button) buttons.addWidget(self.receive_swap_button) vbox = QVBoxLayout() - vbox.addWidget(self.receive_lightning_help_text) + vbox.addWidget(self.receive_help_text) vbox.addLayout(buttons) - self.receive_lightning_help = FramedWidget() - self.receive_lightning_help.setVisible(False) - self.receive_lightning_help.setLayout(vbox) - self.receive_address_qr = QRCodeWidget(manual_size=True) - self.receive_URI_qr = QRCodeWidget(manual_size=True) - self.receive_lightning_qr = QRCodeWidget(manual_size=True) - - for e in [self.receive_address_e, self.receive_URI_e, self.receive_lightning_e]: - e.setFont(QFont(MONOSPACE_FONT)) - e.setReadOnly(True) - e.setContextMenuPolicy(Qt.NoContextMenu) - e.setTextInteractionFlags(Qt.NoTextInteraction) - - self.receive_lightning_e.textChanged.connect(self.update_receive_widgets) - - self.receive_address_widget = ReceiveTabWidget(self, - self.receive_address_e, self.receive_address_qr, self.receive_address_help) - self.receive_URI_widget = ReceiveTabWidget(self, - self.receive_URI_e, self.receive_URI_qr, self.receive_URI_help) - self.receive_lightning_widget = ReceiveTabWidget(self, - self.receive_lightning_e, self.receive_lightning_qr, self.receive_lightning_help) - - from .util import VTabWidget - self.receive_tabs = VTabWidget() - self.receive_tabs.setMinimumHeight(ReceiveTabWidget.min_size.height()) - - #self.receive_tabs.setMinimumHeight(ReceiveTabWidget.min_size.height() + 4) # for margins - self.receive_tabs.addTab(self.receive_URI_widget, read_QIcon("link.png"), _('URI')) - self.receive_tabs.addTab(self.receive_address_widget, read_QIcon("bitcoin.png"), _('Address')) - self.receive_tabs.addTab(self.receive_lightning_widget, read_QIcon("lightning.png"), _('Lightning')) - self.receive_tabs.setCurrentIndex(self.config.get('receive_tabs_index', 0)) - self.receive_tabs.currentChanged.connect(self.on_tab_changed) - receive_tabs_sp = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) - receive_tabs_sp.setRetainSizeWhenHidden(True) - self.receive_tabs.setSizePolicy(receive_tabs_sp) - self.receive_tabs.setVisible(False) + self.receive_help_widget = FramedWidget() + self.receive_help_widget.setVisible(False) + self.receive_help_widget.setLayout(vbox) + + self.receive_widget = ReceiveWidget( + self, self.receive_e, self.receive_qr, self.receive_help_widget) + + receive_widget_sp = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) + receive_widget_sp.setRetainSizeWhenHidden(True) + self.receive_widget.setSizePolicy(receive_widget_sp) + self.receive_widget.setVisible(False) self.receive_requests_label = QLabel(_('Requests')) # with QDarkStyle, this label may partially cover the qrcode widget. @@ -150,12 +123,20 @@ def on_receive_swap(): self.request_list = RequestList(self) # toolbar self.toolbar, menu = self.request_list.create_toolbar_with_menu('') + self.toggle_qr_button = QPushButton('') self.toggle_qr_button.setIcon(read_QIcon('qrcode.png')) self.toggle_qr_button.setToolTip(_('Switch between text and QR code view')) self.toggle_qr_button.clicked.connect(self.toggle_receive_qr) self.toggle_qr_button.setEnabled(False) self.toolbar.insertWidget(2, self.toggle_qr_button) + + self.toggle_view_button = QPushButton('') + self.toggle_view_button.setToolTip(_('switch between view')) + self.toggle_view_button.clicked.connect(self.toggle_view) + self.toggle_view_button.setEnabled(False) + self.update_view_button() + self.toolbar.insertWidget(2, self.toggle_view_button) # menu menu.addConfig( _('Add on-chain fallback to lightning requests'), 'bolt11_fallback', True, @@ -177,7 +158,7 @@ def on_receive_swap(): hbox = QHBoxLayout() hbox.addLayout(vbox_g) hbox.addStretch() - hbox.addWidget(self.receive_tabs) + hbox.addWidget(self.receive_widget) self.searchable_list = self.request_list vbox = QVBoxLayout(self) @@ -220,17 +201,33 @@ def on_toggle_bolt11_fallback(self): self.wallet.lnworker.clear_invoices_cache() self.update_current_request() - def on_tab_changed(self, i): + def update_view_button(self): + i = self.config.get('receive_tabs_index', 0) + if i == 0: + icon, text = read_QIcon("link.png"), _('Bitcoin URI') + elif i == 1: + icon, text = read_QIcon("bitcoin.png"), _('Address') + elif i == 2: + icon, text = read_QIcon("lightning.png"), _('Lightning') + self.toggle_view_button.setText(text) + self.toggle_view_button.setIcon(icon) + + def toggle_view(self): + i = self.config.get('receive_tabs_index', 0) + i = (i + 1) % (3 if self.wallet.has_lightning() else 2) self.config.set_key('receive_tabs_index', i) - title, data = self.get_tab_data(i) + self.update_current_request() + self.update_view_button() + + def on_tab_changed(self): + text, data, help_text, title = self.get_tab_data() self.window.do_copy(data, title=title) self.update_receive_qr_window() def do_copy(self, e): if e.button() != Qt.LeftButton: return - i = self.receive_tabs.currentIndex() - title, data = self.get_tab_data(i) + text, data, help_text, title = self.get_tab_data() self.window.do_copy(data, title=title) def toggle_receive_qr(self): @@ -240,75 +237,59 @@ def toggle_receive_qr(self): def update_receive_widgets(self): b = self.config.get('receive_qr_visible', False) - self.receive_URI_widget.update_visibility(b) - self.receive_address_widget.update_visibility(b) - self.receive_lightning_widget.update_visibility(b) + self.receive_widget.update_visibility(b) def update_current_request(self): key = self.request_list.get_current_key() req = self.wallet.get_request(key) if key else None if req is None: - self.receive_URI_e.setText('') - self.receive_lightning_e.setText('') - self.receive_address_e.setText('') + self.receive_e.setText('') return help_texts = self.wallet.get_help_texts_for_receive_request(req) - addr = (req.get_address() or '') if not help_texts.address_is_error else '' - URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else '' - lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else '' - address_help = help_texts.address_help - URI_help = help_texts.URI_help - ln_help = help_texts.ln_help + self.addr = (req.get_address() or '') if not help_texts.address_is_error else '' + self.URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else '' + self.lnaddr = self.wallet.get_bolt11_invoice(req) if not help_texts.ln_is_error else '' + self.address_help = help_texts.address_help + self.URI_help = help_texts.URI_help + self.ln_help = help_texts.ln_help can_rebalance = help_texts.can_rebalance() can_swap = help_texts.can_swap() self.receive_rebalance_button.suggestion = help_texts.ln_rebalance_suggestion self.receive_swap_button.suggestion = help_texts.ln_swap_suggestion - self.receive_rebalance_button.setVisible(can_rebalance) self.receive_swap_button.setVisible(can_swap) self.receive_rebalance_button.setEnabled(can_rebalance and self.window.num_tasks() == 0) self.receive_swap_button.setEnabled(can_swap and self.window.num_tasks() == 0) - icon_name = "lightning.png" if lnaddr else "lightning_disconnected.png" - self.receive_tabs.setTabIcon(2, read_QIcon(icon_name)) - # encode lightning invoices as uppercase so QR encoding can use - # alphanumeric mode; resulting in smaller QR codes - self.receive_address_e.setText(addr) - self.receive_address_qr.setData(addr) - self.receive_address_help_text.setText(address_help) - self.receive_URI_e.setText(URI) - self.receive_URI_qr.setData(URI) - self.receive_URI_help.setText(URI_help) - self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? - self.receive_lightning_help_text.setText(ln_help) - self.receive_lightning_qr.setData(lnaddr.upper()) - def update_warnings(text_e, qr_e, warning_text): - for w in [text_e, qr_e]: - w.setEnabled(bool(text_e.toPlainText()) and not warning_text) - w.setToolTip(warning_text) - update_warnings(self.receive_address_e, self.receive_address_qr, address_help) - update_warnings(self.receive_URI_e, self.receive_URI_qr, URI_help) - update_warnings(self.receive_lightning_e, self.receive_lightning_qr, ln_help) + text, data, help_text, title = self.get_tab_data() + self.receive_e.setText(text) + self.receive_qr.setData(data) + self.receive_help_text.setText(help_text) + for w in [self.receive_e, self.receive_qr]: + w.setEnabled(bool(text) and not help_text) + w.setToolTip(help_text) # macOS hack (similar to #4777) - self.receive_lightning_e.repaint() - self.receive_URI_e.repaint() - self.receive_address_e.repaint() + self.receive_e.repaint() # always show - self.receive_tabs.setVisible(True) + self.receive_widget.setVisible(True) self.toggle_qr_button.setEnabled(True) + self.toggle_view_button.setEnabled(True) self.update_receive_qr_window() - def get_tab_data(self, i): + def get_tab_data(self): + i = self.config.get('receive_tabs_index', 0) if i == 0: - return _('Bitcoin URI'), self.receive_URI_e.toPlainText() + out = self.URI, self.URI, self.URI_help, _('Bitcoin URI') elif i == 1: - return _('Address'), self.receive_address_e.toPlainText() - else: - return _('Lightning Request'), self.receive_lightning_e.toPlainText() + out = self.addr, self.addr, self.address_help, _('Address') + elif i == 2: + # encode lightning invoices as uppercase so QR encoding can use + # alphanumeric mode; resulting in smaller QR codes + out = self.lnaddr, self.lnaddr.upper(), self.ln_help, _('Lightning Request') + return out def update_receive_qr_window(self): if self.window.qr_window and self.window.qr_window.isVisible(): - i = self.receive_tabs.currentIndex() - title, data = self.get_tab_data(i) + text, data, help_text, title = self.get_tab_data() if i == 2: data = data.upper() self.window.qr_window.qrw.setData(data) @@ -347,7 +328,7 @@ def create_invoice(self): self.receive_amount_e.setText('') self.receive_message_e.setText('') # copy current tab to clipboard - self.on_tab_changed(self.receive_tabs.currentIndex()) + self.on_tab_changed() def get_bitcoin_address_for_request(self, amount) -> Optional[str]: addr = self.wallet.get_unused_address() @@ -369,25 +350,25 @@ def get_bitcoin_address_for_request(self, amount) -> Optional[str]: return addr def do_clear(self): - self.receive_address_e.setText('') - self.receive_URI_e.setText('') - self.receive_lightning_e.setText('') - self.receive_tabs.setVisible(False) + self.receive_e.setText('') + self.receive_widget.setVisible(False) self.toggle_qr_button.setEnabled(False) + self.toggle_view_button.setEnabled(False) self.receive_message_e.setText('') self.receive_amount_e.setAmount(None) self.request_list.clearSelection() -class ReceiveTabWidget(QWidget): +class ReceiveWidget(QWidget): min_size = QSize(200, 200) def __init__(self, receive_tab: 'ReceiveTab', textedit: QWidget, qr: QWidget, help_widget: QWidget): + QWidget.__init__(self) self.textedit = textedit self.qr = qr self.help_widget = help_widget - QWidget.__init__(self) + self.setMinimumSize(self.min_size) for w in [textedit, qr, help_widget]: w.setMinimumSize(self.min_size) @@ -416,6 +397,12 @@ def update_visibility(self, is_qr): self.textedit.setVisible(False) self.qr.setVisible(False) + def resizeEvent(self, e): + # keep square aspect ratio when resized + size = e.size() + w = size.height() + self.setFixedWidth(w) + return super().resizeEvent(e) class FramedWidget(QFrame): def __init__(self): diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index a03a78cb5..63db8c344 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1263,73 +1263,6 @@ def apply(self, image: QImage): return result -class SquareTabWidget(QtWidgets.QStackedWidget): - def resizeEvent(self, e): - # keep square aspect ratio when resized - size = e.size() - w = size.height() - self.setFixedWidth(w) - return super().resizeEvent(e) - - -class VTabWidget(QWidget): - """QtWidgets.QTabWidget alternative with "West" tab positions and horizontal tab-text.""" - def __init__(self): - QWidget.__init__(self) - - hbox = QHBoxLayout() - self.setLayout(hbox) - hbox.setContentsMargins(0, 0, 0, 0) - hbox.setSpacing(0) - - self._tabs_vbox = tabs_vbox = QVBoxLayout() - self._tab_btns = [] # type: List[QPushButton] - tabs_vbox.setContentsMargins(0, 0, 0, 0) - tabs_vbox.setSpacing(0) - - _tabs_vbox_outer_w = QWidget() - _tabs_vbox_outer = QVBoxLayout() - _tabs_vbox_outer_w.setLayout(_tabs_vbox_outer) - _tabs_vbox_outer_w.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, _tabs_vbox_outer_w.sizePolicy().verticalPolicy())) - _tabs_vbox_outer.setContentsMargins(0, 0, 0, 0) - _tabs_vbox_outer.setSpacing(0) - _tabs_vbox_outer.addLayout(tabs_vbox) - _tabs_vbox_outer.addStretch(1) - - self.content_w = content_w = SquareTabWidget() - content_w.setStyleSheet("SquareTabWidget {padding:0px; }") - hbox.addStretch(1) - hbox.addWidget(_tabs_vbox_outer_w) - hbox.addWidget(content_w) - - self.currentChanged = content_w.currentChanged - self.currentIndex = content_w.currentIndex - - def addTab(self, widget: QWidget, icon: QIcon, text: str): - btn = QPushButton(icon, text) - btn.setStyleSheet("QPushButton { text-align: left; }") - btn.setFocusPolicy(QtCore.Qt.NoFocus) - btn.setCheckable(True) - btn.setSizePolicy(QSizePolicy.Preferred, btn.sizePolicy().verticalPolicy()) - - def on_btn_click(): - btn.setChecked(True) - for btn2 in self._tab_btns: - if btn2 != btn: - btn2.setChecked(False) - self.content_w.setCurrentIndex(idx) - btn.clicked.connect(on_btn_click) - idx = len(self._tab_btns) - self._tab_btns.append(btn) - - self._tabs_vbox.addWidget(btn) - self.content_w.addWidget(widget) - - def setTabIcon(self, idx: int, icon: QIcon): - self._tab_btns[idx].setIcon(icon) - - def setCurrentIndex(self, idx: int): - self._tab_btns[idx].click() class QtEventListener(EventListener): From 1503ef1e2ac7470e33b13f2cf3928f2208a1ccf8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 21 Mar 2023 10:41:54 +0100 Subject: [PATCH 0482/1143] follow-up previous commit --- electrum/gui/qt/receive_tab.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 4093e9431..405d19c2d 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -290,8 +290,6 @@ def get_tab_data(self): def update_receive_qr_window(self): if self.window.qr_window and self.window.qr_window.isVisible(): text, data, help_text, title = self.get_tab_data() - if i == 2: - data = data.upper() self.window.qr_window.qrw.setData(data) def create_invoice(self): From 11765521325be71b1d665896724438b5b758a9a2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 21 Mar 2023 12:41:20 +0100 Subject: [PATCH 0483/1143] android: upgrade to Qt 5.15.7, PyQt5 5.15.9 --- contrib/android/Dockerfile | 2 +- contrib/android/p4a_recipes/ply/__init__.py | 18 ++++++++++++++++++ contrib/android/p4a_recipes/pyqt5/__init__.py | 4 ++-- .../android/p4a_recipes/pyqt5sip/__init__.py | 4 ++-- .../p4a_recipes/pyqt_builder/__init__.py | 4 ++-- contrib/android/p4a_recipes/qt5/__init__.py | 2 +- contrib/android/p4a_recipes/sip/__init__.py | 6 +++--- 7 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 contrib/android/p4a_recipes/ply/__init__.py diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 3ca448808..3d002f6a6 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,7 +179,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "8589243afb48fdb116d791dc5b3973382e83273f^{commit}" \ + && git checkout "a3add4f5f2a15a0b56f0c09d729418e4dbef475f^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars diff --git a/contrib/android/p4a_recipes/ply/__init__.py b/contrib/android/p4a_recipes/ply/__init__.py new file mode 100644 index 000000000..4978d5280 --- /dev/null +++ b/contrib/android/p4a_recipes/ply/__init__.py @@ -0,0 +1,18 @@ +import os + +from pythonforandroid.recipes.ply import PlyRecipe +from pythonforandroid.util import load_source + +util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) + + +assert PlyRecipe._version == "3.11" +assert PlyRecipe.depends == ['packaging'] +assert PlyRecipe.python_depends == [] + + +class PlyRecipePinned(util.InheritedRecipeMixin, PlyRecipe): + sha512sum = "37e39a4f930874933223be58a3da7f259e155b75135f1edd47069b3b40e5e96af883ebf1c8a1bbd32f914a9e92cfc12e29fec05cf61b518f46c1d37421b20008" + + +recipe = PlyRecipePinned() diff --git a/contrib/android/p4a_recipes/pyqt5/__init__.py b/contrib/android/p4a_recipes/pyqt5/__init__.py index a3e16e5a3..c21caaa69 100644 --- a/contrib/android/p4a_recipes/pyqt5/__init__.py +++ b/contrib/android/p4a_recipes/pyqt5/__init__.py @@ -6,13 +6,13 @@ util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) -assert PyQt5Recipe._version == "5.15.6" +assert PyQt5Recipe._version == "5.15.9" assert PyQt5Recipe.depends == ['qt5', 'pyjnius', 'setuptools', 'pyqt5sip', 'hostpython3', 'pyqt_builder'] assert PyQt5Recipe.python_depends == [] class PyQt5RecipePinned(util.InheritedRecipeMixin, PyQt5Recipe): - sha512sum = "65fd663cb70e8701e49bd4b39dc9384546cf2edd1b3bab259ca64b50908f48bdc02ca143f36cd6b429075f5616dcc7b291607dcb63afa176e828cded3b82f5c7" + sha512sum = "1c07d93aefe1c24e80851eb4631b80a99e7ba06e823181325456edb90285d3d22417a9f7d4c3ff9c6195bd801e7dc2bbabf0587af844a5e4b0a410c4611d119e" recipe = PyQt5RecipePinned() diff --git a/contrib/android/p4a_recipes/pyqt5sip/__init__.py b/contrib/android/p4a_recipes/pyqt5sip/__init__.py index fd760faac..8150bed4e 100644 --- a/contrib/android/p4a_recipes/pyqt5sip/__init__.py +++ b/contrib/android/p4a_recipes/pyqt5sip/__init__.py @@ -6,13 +6,13 @@ util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) -assert PyQt5SipRecipe._version == "12.9.0" +assert PyQt5SipRecipe._version == "12.11.1" assert PyQt5SipRecipe.depends == ['setuptools', 'python3'] assert PyQt5SipRecipe.python_depends == [] class PyQt5SipRecipePinned(util.InheritedRecipeMixin, PyQt5SipRecipe): - sha512sum = "ca6f3b18b64391fded88732a8109a04d85727bbddecdf126679b187c7f0487c3c1f69ada3e8c54051281a43c6f2de70390ac5ff18a1bed79994070ddde730c5f" + sha512sum = "9a24b6e8356fdb1070672ee37e5f4259d72a75bb60376ad0946274331ae29a6cceb98a6c5a278bf5e8015a3d493c925bacab8593ef02c310ff3773bd3ee46a5d" recipe = PyQt5SipRecipePinned() diff --git a/contrib/android/p4a_recipes/pyqt_builder/__init__.py b/contrib/android/p4a_recipes/pyqt_builder/__init__.py index d29acfa8d..b43a1208d 100644 --- a/contrib/android/p4a_recipes/pyqt_builder/__init__.py +++ b/contrib/android/p4a_recipes/pyqt_builder/__init__.py @@ -1,13 +1,13 @@ from pythonforandroid.recipes.pyqt_builder import PyQtBuilderRecipe -assert PyQtBuilderRecipe._version == "1.12.2" +assert PyQtBuilderRecipe._version == "1.14.1" assert PyQtBuilderRecipe.depends == ["sip", "packaging", "python3"] assert PyQtBuilderRecipe.python_depends == [] class PyQtBuilderRecipePinned(PyQtBuilderRecipe): - sha512sum = "022f2cd40c100543c4b442fc5b27bbf2ec853d94b531f8f6dc1d7f92b07bcc20e8f0a4eb64feb96d094ba0d5f01fddcc8aed23ddf67a61417e07983a73918230" + sha512sum = "4de9be2c42f38fbc22d46a31dd6da37c02620bb112a674ef846a4eb7f862715852e1d7328da1e0d0e33f78475166fe3c690e710e18bfeb48f840f137831a2182" recipe = PyQtBuilderRecipePinned() diff --git a/contrib/android/p4a_recipes/qt5/__init__.py b/contrib/android/p4a_recipes/qt5/__init__.py index 2235f3659..562b21832 100644 --- a/contrib/android/p4a_recipes/qt5/__init__.py +++ b/contrib/android/p4a_recipes/qt5/__init__.py @@ -6,7 +6,7 @@ util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) -assert Qt5Recipe._version == "9b43a43ee96198674060c6b9591e515e2d27c28f" +assert Qt5Recipe._version == "95254e52c658729e80f741324045034c15ce9cb0" assert Qt5Recipe.depends == ['python3'] assert Qt5Recipe.python_depends == [] diff --git a/contrib/android/p4a_recipes/sip/__init__.py b/contrib/android/p4a_recipes/sip/__init__.py index 9f2d16c0c..ee4f44956 100644 --- a/contrib/android/p4a_recipes/sip/__init__.py +++ b/contrib/android/p4a_recipes/sip/__init__.py @@ -1,13 +1,13 @@ from pythonforandroid.recipes.sip import SipRecipe -assert SipRecipe._version == "6.5.1" -assert SipRecipe.depends == ["setuptools", "packaging", "toml", "python3"] +assert SipRecipe._version == "6.7.7" +assert SipRecipe.depends == ["setuptools", "packaging", "toml", "ply", "python3"], SipRecipe.depends assert SipRecipe.python_depends == [] class SipRecipePinned(SipRecipe): - sha512sum = "2d6f225e653873462d97dfdc85bd308a26b66996e1bb98e2c3aa60a3b260db745021f1d3182db8e943fd216ee27a2f65731b96d287e94f8f2e7972c5df971c69" + sha512sum = "b41a1e53e8bad1fca08eda2c89b8a7cabe6cb9e54d0ddeba0c718499b0288633fb6b90128d54f3df2420e20bb217d3df224750d30e865487d2b0a640fba82444" recipe = SipRecipePinned() From 6b9d294a86ca61e24834b152cbd319e61e0eda0e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 21 Mar 2023 14:13:00 +0100 Subject: [PATCH 0484/1143] android: log_level 2 when running in CI --- contrib/android/make_apk.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/make_apk.sh b/contrib/android/make_apk.sh index 0da3dc144..d9b1bd9ef 100755 --- a/contrib/android/make_apk.sh +++ b/contrib/android/make_apk.sh @@ -45,7 +45,7 @@ info "apk building phase starts." # So, in particular, to build a testnet apk, simply uncomment: #export APP_PACKAGE_DOMAIN=org.electrum.testnet -if [ ! $CI ]; then +if [ $CI ]; then # override log level specified in buildozer.spec to "debug": export BUILDOZER_LOG_LEVEL=2 fi From ed0f1ea27f5dfd58858b611fc3f2ce65f8066b37 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 21 Mar 2023 15:31:07 +0000 Subject: [PATCH 0485/1143] qt receive_tab: use correct qrcode icon (based on dark/light theme) --- electrum/gui/qt/receive_tab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 405d19c2d..00842f2e6 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -19,7 +19,7 @@ from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .qrcodewidget import QRCodeWidget from .util import read_QIcon, ColorScheme, HelpLabel, WWLabel, MessageBoxMixin, MONOSPACE_FONT -from .util import ButtonsTextEdit +from .util import ButtonsTextEdit, get_iconname_qrcode if TYPE_CHECKING: from . import ElectrumGui @@ -125,7 +125,7 @@ def on_receive_swap(): self.toolbar, menu = self.request_list.create_toolbar_with_menu('') self.toggle_qr_button = QPushButton('') - self.toggle_qr_button.setIcon(read_QIcon('qrcode.png')) + self.toggle_qr_button.setIcon(read_QIcon(get_iconname_qrcode())) self.toggle_qr_button.setToolTip(_('Switch between text and QR code view')) self.toggle_qr_button.clicked.connect(self.toggle_receive_qr) self.toggle_qr_button.setEnabled(False) From 3fb3e3b8090010e7154a944b3559e18fe9f5a5e2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 21 Mar 2023 17:08:36 +0100 Subject: [PATCH 0486/1143] lnurl6: pay invoice directly --- electrum/gui/qt/send_tab.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 4e5281f0b..5ff597f6a 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -295,7 +295,6 @@ def get_frozen_balance_str(self) -> Optional[str]: def do_clear(self): self._lnurl_data = None - self.send_button.restore_original_text() self.max_button.setChecked(False) self.payment_request = None self.payto_URI = None @@ -396,7 +395,6 @@ def on_lnurl6_round1(self, lnurl_data: LNURL6Data, url: str): self.message_e.setText(f"lnurl: {domain}: {lnurl_data.metadata_plaintext}") self.amount_e.setAmount(lnurl_data.min_sendable_sat) self.amount_e.setFrozen(False) - self.send_button.setText(_('Get Invoice')) for btn in [self.send_button, self.clear_button]: btn.setEnabled(True) self.set_onchain(False) @@ -559,14 +557,13 @@ async def f(): self.prepare_for_send_tab_network_lookup() def on_lnurl6_round2(self, bolt11_invoice: str): - self.set_bolt11(bolt11_invoice) - self.payto_e.setFrozen(True) - self.amount_e.setEnabled(False) - self.fiat_send_e.setEnabled(False) - for btn in [self.send_button, self.clear_button, self.save_button]: - btn.setEnabled(True) - self.send_button.restore_original_text() self._lnurl_data = None + invoice = Invoice.from_bech32(bolt11_invoice) + assert invoice.get_amount_sat() == self.get_amount(), (invoice.get_amount_sat(), self.get_amount()) + self.do_clear() + self.payto_e.setText(bolt11_invoice) + self.pending_invoice = invoice + self.do_pay_invoice(invoice) def do_pay_or_get_invoice(self): if self._lnurl_data: From bac889c59383ce776fece1e10839c732c91605b5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 21 Mar 2023 14:32:36 +0100 Subject: [PATCH 0487/1143] android: fix ply depends assert --- contrib/android/p4a_recipes/ply/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/p4a_recipes/ply/__init__.py b/contrib/android/p4a_recipes/ply/__init__.py index 4978d5280..210b70ad1 100644 --- a/contrib/android/p4a_recipes/ply/__init__.py +++ b/contrib/android/p4a_recipes/ply/__init__.py @@ -7,7 +7,7 @@ assert PlyRecipe._version == "3.11" -assert PlyRecipe.depends == ['packaging'] +assert PlyRecipe.depends == ['packaging', 'python3'] assert PlyRecipe.python_depends == [] From 98304662ca833b5d554cf2c1ee212e24065db18a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 21 Mar 2023 16:34:39 +0000 Subject: [PATCH 0488/1143] android build: default to log_level=2 It is nice to see debug logs for local builds, and also extremely useful to have them on the CI. follow-up https://github.com/spesmilo/electrum/commit/6b9d294a86ca61e24834b152cbd319e61e0eda0e --- contrib/android/buildozer_kivy.spec | 2 +- contrib/android/buildozer_qml.spec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/android/buildozer_kivy.spec b/contrib/android/buildozer_kivy.spec index 9c78d172d..b40009658 100644 --- a/contrib/android/buildozer_kivy.spec +++ b/contrib/android/buildozer_kivy.spec @@ -212,7 +212,7 @@ p4a.local_recipes = %(source.dir)s/contrib/android/p4a_recipes/ [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) -log_level = 1 +log_level = 2 # (str) Path to build output (i.e. .apk, .ipa) storage bin_dir = ./dist diff --git a/contrib/android/buildozer_qml.spec b/contrib/android/buildozer_qml.spec index 26555b334..701fe306e 100644 --- a/contrib/android/buildozer_qml.spec +++ b/contrib/android/buildozer_qml.spec @@ -224,7 +224,7 @@ p4a.bootstrap = qt5 [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) -log_level = 1 +log_level = 2 # (str) Path to build output (i.e. .apk, .ipa) storage bin_dir = ./dist From 7f7ee8d82f58a8b694085ed38565a611b270ae77 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 21 Mar 2023 16:51:50 +0000 Subject: [PATCH 0489/1143] qml: pressing "Esc" on desktop to ~simulate "back" button --- electrum/gui/qml/components/Pin.qml | 2 +- electrum/gui/qml/components/controls/ElDialog.qml | 2 +- electrum/gui/qml/components/main.qml | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index 180c4f0c9..1c9f8b724 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -23,7 +23,7 @@ ElDialog { focus: true - closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose + closePolicy: canCancel ? Popup.CloseOnPressOutside : Popup.NoAutoClose property bool canCancel: true diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 1caa8e71d..71079d7aa 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -19,7 +19,7 @@ Dialog { } closePolicy: allowClose - ? Popup.CloseOnEscape | Popup.CloseOnPressOutside + ? Popup.CloseOnPressOutside : Popup.NoAutoClose onOpenedChanged: { diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index dbf6ec4b9..ab5a268a1 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -430,6 +430,14 @@ ApplicationWindow } } + Shortcut { + context: Qt.ApplicationShortcut + sequence: "Esc" + onActivated: { + close() + } + } + Connections { target: Daemon function onWalletRequiresPassword(name, path) { From 558eb1a372cf7cb93e2d5a4aeaa3273cb2afa166 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 21 Mar 2023 17:51:29 +0000 Subject: [PATCH 0490/1143] qml: reorganise toolbarTopLayout, so that top-left click opens menu previously clicking too far left would not open the wallet-menu: - click on label would open it, but - click on wallet-icon or padding to its left would not --- electrum/gui/qml/components/main.qml | 51 ++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index dbf6ec4b9..a44cdbb13 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -112,34 +112,43 @@ ApplicationWindow Layout.alignment: Qt.AlignVCenter Item { - Layout.preferredWidth: constants.paddingXLarge - Layout.preferredHeight: 1 - } - - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - visible: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) - source: '../../icons/wallet.png' - } - - Label { Layout.fillWidth: true Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height) - text: stack.currentItem.title - ? stack.currentItem.title - : Daemon.currentWallet.name - elide: Label.ElideRight - verticalAlignment: Qt.AlignVCenter - font.pixelSize: constants.fontSizeMedium - font.bold: true + MouseArea { anchors.fill: parent onClicked: { - stack.getRoot().menu.open() + stack.getRoot().menu.open() // open wallet-menu stack.getRoot().menu.y = toolbar.height } } + + RowLayout { + + Item { + Layout.preferredWidth: constants.paddingXLarge + Layout.preferredHeight: 1 + } + + Image { + Layout.preferredWidth: constants.iconSizeSmall + Layout.preferredHeight: constants.iconSizeSmall + visible: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) + source: '../../icons/wallet.png' + } + + Label { + Layout.fillWidth: true + Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height) + text: stack.currentItem.title + ? stack.currentItem.title + : Daemon.currentWallet.name + elide: Label.ElideRight + verticalAlignment: Qt.AlignVCenter + font.pixelSize: constants.fontSizeMedium + font.bold: true + } + } } Item { @@ -148,7 +157,7 @@ ApplicationWindow MouseArea { anchors.fill: parent - onClicked: openAppMenu() + onClicked: openAppMenu() // open global-app-menu } RowLayout { From 0d007b5739d775e7f198c3bb815b26e9a19eb2be Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 22 Mar 2023 10:21:09 +0100 Subject: [PATCH 0491/1143] follow-up aa3697d --- electrum/gui/qt/request_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index dcce82ae3..730010c0b 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -214,8 +214,7 @@ def delete_requests(self, keys): def delete_expired_requests(self): keys = self.wallet.delete_expired_requests() - for key in keys: - self.delete_item(key) + self.update() self.receive_tab.do_clear() def set_visibility_of_columns(self): From 7834f6c427594283095274d76ca3203b33df3618 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 22 Mar 2023 12:22:36 +0000 Subject: [PATCH 0492/1143] commands: fix satoshis decimal conversion in payto cmd and others When called via jsonrpc (but not via cli) with non-string amounts, there could be a rounding error resulting in sending 1 sat less. example: ``` $ ./run_electrum --testnet -w ~/.electrum/testnet/wallets/test_segwit_2 paytomany '[["tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg", 0.00033389]]' --fee 0 02000000000101b9e6018acb16952e3c9618b069df404dc85544eda8120e5f6e7cd7e94ce5ae8d0100000000fdffffff02fd8100000000000016001410c5b97085ec1637a9f702852f5a81f650fae1566d82000000000000160014d5a97ae05a1f4f315d0579b6577daceb5177a42b024730440220251d2ce83f6e69273de8e9be8602fbcf72b9157e1c0116161fa52f7e04db6e4302202d84045cc6b7056a215d1db3f59884e28dadd5257e1a3960068f90df90b452d1012102b0eff3bf364a2ab5effe952cba33521ebede81dac88c71951a5ed598cb48347b3a022500 $ curl --data-binary '{"id":"curltext","method":"paytomany","params":{"outputs":[["tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg", 0.00033389]], "fee": 0, "wallet": "/home/user/.electrum/testnet/wallets/test_segwit_2"}}' http://user:pass@127.0.0.1:7777 {"id": "curltext", "jsonrpc": "2.0", "result": "02000000000101b9e6018acb16952e3c9618b069df404dc85544eda8120e5f6e7cd7e94ce5ae8d0100000000fdffffff02fe8100000000000016001410c5b97085ec1637a9f702852f5a81f650fae1566c82000000000000160014d5a97ae05a1f4f315d0579b6577daceb5177a42b0247304402206ef66b845ca298c14dc6e8049cba9ed19db1671132194518ce5d521de6f5df8802205ca4b1aee703e3b98331fb9b88210917b385560020c8b2a8a88da38996b101c4012102b0eff3bf364a2ab5effe952cba33521ebede81dac88c71951a5ed598cb48347b39022500"} ``` ^ note that first tx has output for 0.00033389, second tx has output for 0.00033388 fixes https://github.com/spesmilo/electrum/issues/8274 --- electrum/commands.py | 30 +++++++++++++++--------------- electrum/exchange_rate.py | 14 +------------- electrum/util.py | 11 +++++++++++ 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index d7e1af79e..fbae8858e 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -42,7 +42,7 @@ from .import util, ecc from .util import (bfh, format_satoshis, json_decode, json_normalize, - is_hash256_str, is_hex_str, to_bytes, parse_max_spend) + is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal) from . import bitcoin from .bitcoin import is_address, hash_160, COIN from .bip32 import BIP32Node @@ -85,10 +85,10 @@ def satoshis_or_max(amount): def satoshis(amount): # satoshi conversion must not be performed by the parser - return int(COIN*Decimal(amount)) if amount is not None else None + return int(COIN*to_decimal(amount)) if amount is not None else None def format_satoshis(x): - return str(Decimal(x)/COIN) if x is not None else None + return str(to_decimal(x)/COIN) if x is not None else None class Command: @@ -357,7 +357,7 @@ async def listunspent(self, wallet: Abstract_Wallet = None): for txin in wallet.get_utxos(): d = txin.to_json() v = d.pop("value_sats") - d["value"] = str(Decimal(v)/COIN) if v is not None else None + d["value"] = str(to_decimal(v)/COIN) if v is not None else None coins.append(d) return coins @@ -543,13 +543,13 @@ async def getbalance(self, wallet: Abstract_Wallet = None): """Return the balance of your wallet. """ c, u, x = wallet.get_balance() l = wallet.lnworker.get_balance() if wallet.lnworker else None - out = {"confirmed": str(Decimal(c)/COIN)} + out = {"confirmed": str(to_decimal(c)/COIN)} if u: - out["unconfirmed"] = str(Decimal(u)/COIN) + out["unconfirmed"] = str(to_decimal(u)/COIN) if x: - out["unmatured"] = str(Decimal(x)/COIN) + out["unmatured"] = str(to_decimal(x)/COIN) if l: - out["lightning"] = str(Decimal(l)/COIN) + out["lightning"] = str(to_decimal(l)/COIN) return out @command('n') @@ -559,8 +559,8 @@ async def getaddressbalance(self, address): """ sh = bitcoin.address_to_scripthash(address) out = await self.network.get_balance_for_scripthash(sh) - out["confirmed"] = str(Decimal(out["confirmed"])/COIN) - out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) + out["confirmed"] = str(to_decimal(out["confirmed"])/COIN) + out["unconfirmed"] = str(to_decimal(out["unconfirmed"])/COIN) return out @command('n') @@ -1056,7 +1056,7 @@ async def getfeerate(self, fee_method=None, fee_level=None): else: raise Exception('Invalid fee estimation method: {}'.format(fee_method)) if fee_level is not None: - fee_level = Decimal(fee_level) + fee_level = to_decimal(fee_level) return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level) @command('w') @@ -1358,7 +1358,7 @@ async def convert_currency(self, from_amount=1, from_ccy = '', to_ccy = ''): raise Exception(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable') # Conversion try: - from_amount = Decimal(from_amount) + from_amount = to_decimal(from_amount) to_amount = from_amount / rate_from * rate_to except InvalidOperation: raise Exception("from_amount is not a number") @@ -1456,7 +1456,7 @@ def eval_bool(x: str) -> bool: # don't use floats because of rounding errors from .transaction import convert_raw_tx_to_hex -json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x))) +json_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x))) arg_types = { 'num': int, 'nbits': int, @@ -1469,8 +1469,8 @@ def eval_bool(x: str) -> bool: 'jsontx': json_loads, 'inputs': json_loads, 'outputs': json_loads, - 'fee': lambda x: str(Decimal(x)) if x is not None else None, - 'amount': lambda x: str(Decimal(x)) if not parse_max_spend(x) else x, + 'fee': lambda x: str(to_decimal(x)) if x is not None else None, + 'amount': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x, 'locktime': int, 'addtransaction': eval_bool, 'fee_method': str, diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 55c9edee0..5bb8bc428 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -17,7 +17,7 @@ from .bitcoin import COIN from .i18n import _ from .util import (ThreadJob, make_dir, log_exceptions, OldTaskGroup, - make_aiohttp_session, resource_path, EventListener, event_listener) + make_aiohttp_session, resource_path, EventListener, event_listener, to_decimal) from .network import Network from .simple_config import SimpleConfig from .logging import Logger @@ -39,18 +39,6 @@ 'BTC': 8, 'LTC': 8, 'XRP': 6, 'ETH': 18, } - -def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal: - # helper function mainly for float->Decimal conversion, i.e.: - # >>> Decimal(41754.681) - # Decimal('41754.680999999996856786310672760009765625') - # >>> Decimal("41754.681") - # Decimal('41754.681') - if isinstance(x, Decimal): - return x - return Decimal(str(x)) - - POLL_PERIOD_SPOT_RATE = 150 # approx. every 2.5 minutes, try to refresh spot price EXPIRY_SPOT_RATE = 600 # spot price becomes stale after 10 minutes diff --git a/electrum/util.py b/electrum/util.py index f15e0df2c..edd0ff6c8 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -205,6 +205,17 @@ class UserCancelled(Exception): pass +def to_decimal(x: Union[str, float, int, Decimal]) -> Decimal: + # helper function mainly for float->Decimal conversion, i.e.: + # >>> Decimal(41754.681) + # Decimal('41754.680999999996856786310672760009765625') + # >>> Decimal("41754.681") + # Decimal('41754.681') + if isinstance(x, Decimal): + return x + return Decimal(str(x)) + + # note: this is not a NamedTuple as then its json encoding cannot be customized class Satoshis(object): __slots__ = ('value',) From 9eb25cd4427a8c0e124db9b5e62059032bd45704 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 23 Mar 2023 08:22:36 +0100 Subject: [PATCH 0493/1143] follow-up a080e5130f392b95988e9a82739cd2cf6b0035d6 --- electrum/gui/qt/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2971ebc38..aca538c62 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1141,7 +1141,7 @@ def on_event_request_status(self, wallet, key, status): msg += '\n' + req.get_message() self.notify(msg) self.receive_tab.request_list.delete_item(key) - self.receive_tab.receive_tabs.setVisible(False) + self.receive_tab.do_clear() self.need_update.set() else: self.receive_tab.request_list.refresh_item(key) From e7cc2c5a637a296bb0bf0633e8d4a5bf7c8c863d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 10:12:05 +0100 Subject: [PATCH 0494/1143] Revert "qml: pressing "Esc" on desktop to ~simulate "back" button" This reverts commit 7f7ee8d82f58a8b694085ed38565a611b270ae77. This commit caused a regression with the android back button not closing a dialog. reproduce: 1. from the main window, press receive. 2. in the request details window, press Create request. 3. in the receive payment dialog, press Copy 4. observe dialog cannot be closed by back button --- electrum/gui/qml/components/Pin.qml | 2 +- electrum/gui/qml/components/controls/ElDialog.qml | 2 +- electrum/gui/qml/components/main.qml | 8 -------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index 1c9f8b724..180c4f0c9 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -23,7 +23,7 @@ ElDialog { focus: true - closePolicy: canCancel ? Popup.CloseOnPressOutside : Popup.NoAutoClose + closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose property bool canCancel: true diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 71079d7aa..1caa8e71d 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -19,7 +19,7 @@ Dialog { } closePolicy: allowClose - ? Popup.CloseOnPressOutside + ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose onOpenedChanged: { diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index d97e3f1e2..a44cdbb13 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -439,14 +439,6 @@ ApplicationWindow } } - Shortcut { - context: Qt.ApplicationShortcut - sequence: "Esc" - onActivated: { - close() - } - } - Connections { target: Daemon function onWalletRequiresPassword(name, path) { From 17bb1ad5c5a447725f2aed659d32ce3e3ed158a4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 21 Mar 2023 10:40:28 +0100 Subject: [PATCH 0495/1143] qml: enable Qt virtual keyboard and add Electrum keyboard style, modified from Qt 'default' style --- contrib/android/buildozer_qml.spec | 2 +- .../Electrum/images/backspace-868482.svg | 23 + .../Styles/Electrum/images/check-868482.svg | 8 + .../Styles/Electrum/images/enter-868482.svg | 13 + .../Styles/Electrum/images/globe-868482.svg | 26 + .../Electrum/images/handwriting-868482.svg | 18 + .../Electrum/images/hidekeyboard-868482.svg | 55 + .../Styles/Electrum/images/search-868482.svg | 14 + .../images/selectionhandle-bottom.svg | 201 ++++ .../Styles/Electrum/images/shift-80c342.svg | 12 + .../Styles/Electrum/images/shift-868482.svg | 12 + .../Styles/Electrum/images/shift-c5d6b6.svg | 12 + .../Electrum/images/textmode-868482.svg | 33 + .../VirtualKeyboard/Styles/Electrum/style.qml | 1041 +++++++++++++++++ electrum/gui/qml/__init__.py | 10 +- .../gui/qml/components/controls/ElDialog.qml | 2 +- electrum/gui/qml/components/main.qml | 56 +- 17 files changed, 1520 insertions(+), 18 deletions(-) create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml diff --git a/contrib/android/buildozer_qml.spec b/contrib/android/buildozer_qml.spec index 701fe306e..f5ee73bd1 100644 --- a/contrib/android/buildozer_qml.spec +++ b/contrib/android/buildozer_qml.spec @@ -13,7 +13,7 @@ package.domain = org.electrum source.dir = . # (list) Source files to include (let empty to include all the files) -source.include_exts = py,png,jpg,qml,qmltypes,ttf,txt,gif,pem,mo,json,csv,so +source.include_exts = py,png,jpg,qml,qmltypes,ttf,txt,gif,pem,mo,json,csv,so,svg # (list) Source files to exclude (let empty to not exclude anything) source.exclude_exts = spec diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg new file mode 100644 index 000000000..764c3c68e --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg new file mode 100644 index 000000000..544fec504 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg new file mode 100644 index 000000000..88c148666 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg new file mode 100644 index 000000000..7cb9b7947 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg new file mode 100644 index 000000000..65d378747 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg new file mode 100644 index 000000000..31e680a11 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg new file mode 100644 index 000000000..4aff84996 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg new file mode 100644 index 000000000..312e3ab50 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg new file mode 100644 index 000000000..d39a2230d --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg new file mode 100644 index 000000000..95b6d5044 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg new file mode 100644 index 000000000..22f9d5de2 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg new file mode 100644 index 000000000..515f5c797 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml new file mode 100644 index 000000000..6091d8bc9 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml @@ -0,0 +1,1041 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.7 +import QtQuick.VirtualKeyboard 2.1 +import QtQuick.VirtualKeyboard.Styles 2.1 + +import QtQuick.Controls.Material 2.0 + +KeyboardStyle { + id: currentStyle + + readonly property bool compactSelectionList: [InputEngine.InputMode.Pinyin, InputEngine.InputMode.Cangjie, InputEngine.InputMode.Zhuyin].indexOf(InputContext.inputEngine.inputMode) !== -1 + readonly property string fontFamily: "Sans" + readonly property real keyBackgroundMargin: Math.round(13 * scaleHint) + readonly property real keyContentMargin: Math.round(45 * scaleHint) + readonly property real keyIconScale: scaleHint * 0.6 + readonly property string resourcePrefix: '' + + readonly property string inputLocale: InputContext.locale + property color inputLocaleIndicatorColor: "white" + property Timer inputLocaleIndicatorHighlightTimer: Timer { + interval: 1000 + onTriggered: inputLocaleIndicatorColor = "gray" + } + onInputLocaleChanged: { + inputLocaleIndicatorColor = 'red' //"white" + inputLocaleIndicatorHighlightTimer.restart() + } + + keyboardDesignWidth: 2560 + keyboardDesignHeight: 1200 + keyboardRelativeLeftMargin: 114 / keyboardDesignWidth + keyboardRelativeRightMargin: 114 / keyboardDesignWidth + keyboardRelativeTopMargin: 13 / keyboardDesignHeight + keyboardRelativeBottomMargin: 86 / keyboardDesignHeight + + keyboardBackground: Rectangle { + color: constants.colorAlpha(Material.accentColor, 0.5) //mutedForeground //'red' //"black" + } + + keyPanel: KeyPanel { + id: keyPanel + Rectangle { + id: keyBackground + radius: 5 + color: "#383533" + anchors.fill: keyPanel + anchors.margins: keyBackgroundMargin + Text { + id: keySmallText + text: control.smallText + visible: control.smallTextVisible + color: "gray" + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: keyContentMargin / 3 + font { + family: fontFamily + weight: Font.Normal + pixelSize: 38 * scaleHint * 2 + capitalization: control.uppercased ? Font.AllUppercase : Font.MixedCase + } + } + Text { + id: keyText + text: control.displayText + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + anchors.leftMargin: keyContentMargin + anchors.topMargin: keyContentMargin + anchors.rightMargin: keyContentMargin + anchors.bottomMargin: keyContentMargin + font { + family: fontFamily + weight: Font.Normal + pixelSize: 52 * scaleHint * 2 + capitalization: control.uppercased ? Font.AllUppercase : Font.MixedCase + } + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: keyBackground + opacity: 0.75 + } + PropertyChanges { + target: keyText + opacity: 0.5 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: keyBackground + opacity: 0.75 + } + PropertyChanges { + target: keyText + opacity: 0.05 + } + } + ] + } + + backspaceKeyPanel: KeyPanel { + id: backspaceKeyPanel + Rectangle { + id: backspaceKeyBackground + radius: 5 + color: "#23211E" + anchors.fill: backspaceKeyPanel + anchors.margins: keyBackgroundMargin + Image { + id: backspaceKeyIcon + anchors.centerIn: parent + sourceSize.width: 159 * keyIconScale + sourceSize.height: 88 * keyIconScale + smooth: false + source: resourcePrefix + "images/backspace-868482.svg" + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: backspaceKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: backspaceKeyIcon + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: backspaceKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: backspaceKeyIcon + opacity: 0.2 + } + } + ] + } + + languageKeyPanel: KeyPanel { + id: languageKeyPanel + Rectangle { + id: languageKeyBackground + radius: 5 + color: "#35322f" + anchors.fill: languageKeyPanel + anchors.margins: keyBackgroundMargin + Image { + id: languageKeyIcon + anchors.centerIn: parent + sourceSize.width: 144 * keyIconScale + sourceSize.height: 144 * keyIconScale + smooth: false + source: resourcePrefix + "images/globe-868482.svg" + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: languageKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: languageKeyIcon + opacity: 0.75 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: languageKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: languageKeyIcon + opacity: 0.2 + } + } + ] + } + + enterKeyPanel: KeyPanel { + id: enterKeyPanel + Rectangle { + id: enterKeyBackground + radius: 5 + color: "#1e1b18" + anchors.fill: enterKeyPanel + anchors.margins: keyBackgroundMargin + Image { + id: enterKeyIcon + visible: enterKeyText.text.length === 0 + anchors.centerIn: parent + readonly property size enterKeyIconSize: { + switch (control.actionId) { + case EnterKeyAction.Go: + case EnterKeyAction.Send: + case EnterKeyAction.Next: + case EnterKeyAction.Done: + return Qt.size(170, 119) + case EnterKeyAction.Search: + return Qt.size(148, 148) + default: + return Qt.size(211, 80) + } + } + sourceSize.width: enterKeyIconSize.width * keyIconScale + sourceSize.height: enterKeyIconSize.height * keyIconScale + smooth: false + source: { + switch (control.actionId) { + case EnterKeyAction.Go: + case EnterKeyAction.Send: + case EnterKeyAction.Next: + case EnterKeyAction.Done: + return resourcePrefix + "images/check-868482.svg" + case EnterKeyAction.Search: + return resourcePrefix + "images/search-868482.svg" + default: + return resourcePrefix + "images/enter-868482.svg" + } + } + } + Text { + id: enterKeyText + visible: text.length !== 0 + text: control.actionId !== EnterKeyAction.None ? control.displayText : "" + clip: true + fontSizeMode: Text.HorizontalFit + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: "#80c342" + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint + capitalization: Font.AllUppercase + } + anchors.fill: parent + anchors.margins: Math.round(42 * scaleHint) + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: enterKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: enterKeyIcon + opacity: 0.6 + } + PropertyChanges { + target: enterKeyText + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: enterKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: enterKeyIcon + opacity: 0.2 + } + PropertyChanges { + target: enterKeyText + opacity: 0.2 + } + } + ] + } + + hideKeyPanel: KeyPanel { + id: hideKeyPanel + Rectangle { + id: hideKeyBackground + radius: 5 + color: "#1e1b18" + anchors.fill: hideKeyPanel + anchors.margins: keyBackgroundMargin + Image { + id: hideKeyIcon + anchors.centerIn: parent + sourceSize.width: 144 * keyIconScale + sourceSize.height: 127 * keyIconScale + smooth: false + source: resourcePrefix + "images/hidekeyboard-868482.svg" + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: hideKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: hideKeyIcon + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: hideKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: hideKeyIcon + opacity: 0.2 + } + } + ] + } + + shiftKeyPanel: KeyPanel { + id: shiftKeyPanel + Rectangle { + id: shiftKeyBackground + radius: 5 + color: "#1e1b18" + anchors.fill: shiftKeyPanel + anchors.margins: keyBackgroundMargin + Image { + id: shiftKeyIcon + anchors.centerIn: parent + sourceSize.width: 144 * keyIconScale + sourceSize.height: 134 * keyIconScale + smooth: false + source: resourcePrefix + "images/shift-868482.svg" + } + states: [ + State { + name: "capsLockActive" + when: InputContext.capsLockActive + PropertyChanges { + target: shiftKeyBackground + color: "#5a892e" + } + PropertyChanges { + target: shiftKeyIcon + source: resourcePrefix + "images/shift-c5d6b6.svg" + } + }, + State { + name: "shiftActive" + when: InputContext.shiftActive + PropertyChanges { + target: shiftKeyIcon + source: resourcePrefix + "images/shift-80c342.svg" + } + } + ] + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: shiftKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: shiftKeyIcon + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: shiftKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: shiftKeyIcon + opacity: 0.2 + } + } + ] + } + + spaceKeyPanel: KeyPanel { + id: spaceKeyPanel + Rectangle { + id: spaceKeyBackground + radius: 5 + color: "#35322f" + anchors.fill: spaceKeyPanel + anchors.margins: keyBackgroundMargin + Text { + id: spaceKeyText + text: Qt.locale(InputContext.locale).nativeLanguageName + color: currentStyle.inputLocaleIndicatorColor + Behavior on color { PropertyAnimation { duration: 250 } } + anchors.centerIn: parent + font { + family: fontFamily + weight: Font.Normal + pixelSize: 48 * scaleHint * 1.5 + } + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: spaceKeyBackground + opacity: 0.80 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: spaceKeyBackground + opacity: 0.8 + } + } + ] + } + + symbolKeyPanel: KeyPanel { + id: symbolKeyPanel + Rectangle { + id: symbolKeyBackground + radius: 5 + color: "#1e1b18" + anchors.fill: symbolKeyPanel + anchors.margins: keyBackgroundMargin + Text { + id: symbolKeyText + text: control.displayText + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + anchors.margins: keyContentMargin + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint * 2 + capitalization: Font.AllUppercase + } + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: symbolKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: symbolKeyText + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: symbolKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: symbolKeyText + opacity: 0.2 + } + } + ] + } + + modeKeyPanel: KeyPanel { + id: modeKeyPanel + Rectangle { + id: modeKeyBackground + radius: 5 + color: "#1e1b18" + anchors.fill: modeKeyPanel + anchors.margins: keyBackgroundMargin + Text { + id: modeKeyText + text: control.displayText + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + anchors.margins: keyContentMargin + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint + capitalization: Font.AllUppercase + } + } + Rectangle { + id: modeKeyIndicator + implicitHeight: parent.height * 0.1 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: parent.width * 0.4 + anchors.rightMargin: parent.width * 0.4 + anchors.bottomMargin: parent.height * 0.12 + color: "#80c342" + radius: 3 + visible: control.mode + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: modeKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: modeKeyText + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: modeKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: modeKeyText + opacity: 0.2 + } + } + ] + } + + handwritingKeyPanel: KeyPanel { + id: handwritingKeyPanel + Rectangle { + id: hwrKeyBackground + radius: 5 + color: "#35322f" + anchors.fill: handwritingKeyPanel + anchors.margins: keyBackgroundMargin + Image { + id: hwrKeyIcon + anchors.centerIn: parent + readonly property size hwrKeyIconSize: keyboard.handwritingMode ? Qt.size(124, 96) : Qt.size(156, 104) + sourceSize.width: hwrKeyIconSize.width * keyIconScale + sourceSize.height: hwrKeyIconSize.height * keyIconScale + smooth: false + source: resourcePrefix + (keyboard.handwritingMode ? "images/textmode-868482.svg" : "images/handwriting-868482.svg") + } + } + states: [ + State { + name: "pressed" + when: control.pressed + PropertyChanges { + target: hwrKeyBackground + opacity: 0.80 + } + PropertyChanges { + target: hwrKeyIcon + opacity: 0.6 + } + }, + State { + name: "disabled" + when: !control.enabled + PropertyChanges { + target: hwrKeyBackground + opacity: 0.8 + } + PropertyChanges { + target: hwrKeyIcon + opacity: 0.2 + } + } + ] + } + + characterPreviewMargin: 0 + characterPreviewDelegate: Item { + property string text + id: characterPreview + Rectangle { + id: characterPreviewBackground + anchors.fill: parent + color: "#5d5b59" + radius: 5 + Text { + id: characterPreviewText + color: "white" + text: characterPreview.text + fontSizeMode: Text.HorizontalFit + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent + anchors.margins: Math.round(48 * scaleHint) + font { + family: fontFamily + weight: Font.Normal + pixelSize: 82 * scaleHint * 2 + } + } + } + } + + alternateKeysListItemWidth: 99 * scaleHint * 2 + alternateKeysListItemHeight: 150 * scaleHint * 2 + alternateKeysListDelegate: Item { + id: alternateKeysListItem + width: alternateKeysListItemWidth + height: alternateKeysListItemHeight + Text { + id: listItemText + text: model.text + color: "#868482" + font { + family: fontFamily + weight: Font.Normal + pixelSize: 52 * scaleHint * 2 + } + anchors.centerIn: parent + } + states: State { + name: "current" + when: alternateKeysListItem.ListView.isCurrentItem + PropertyChanges { + target: listItemText + color: "white" + } + } + } + alternateKeysListHighlight: Rectangle { + color: "#5d5b59" + radius: 5 + } + alternateKeysListBackground: Rectangle { + color: "#1e1b18" + radius: 5 + } + + selectionListHeight: 85 * scaleHint * 2 + selectionListDelegate: SelectionListItem { + id: selectionListItem + width: Math.round(selectionListLabel.width + selectionListLabel.anchors.leftMargin * 2) + Text { + id: selectionListLabel + anchors.left: parent.left + anchors.leftMargin: Math.round((compactSelectionList ? 50 : 140) * scaleHint) + anchors.verticalCenter: parent.verticalCenter + text: decorateText(display, wordCompletionLength) + color: "#80c342" + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint * 2 + } + function decorateText(text, wordCompletionLength) { + if (wordCompletionLength > 0) { + return text.slice(0, -wordCompletionLength) + '' + text.slice(-wordCompletionLength) + '' + } + return text + } + } + Rectangle { + id: selectionListSeparator + width: 4 * scaleHint + height: 36 * scaleHint + radius: 2 + color: "#35322f" + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.left + } + states: State { + name: "current" + when: selectionListItem.ListView.isCurrentItem + PropertyChanges { + target: selectionListLabel + color: "white" + } + } + } + selectionListBackground: Rectangle { + color: "#1e1b18" + } + selectionListAdd: Transition { + NumberAnimation { property: "y"; from: wordCandidateView.height; duration: 200 } + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 200 } + } + selectionListRemove: Transition { + NumberAnimation { property: "y"; to: -wordCandidateView.height; duration: 200 } + NumberAnimation { property: "opacity"; to: 0; duration: 200 } + } + + navigationHighlight: Rectangle { + color: "transparent" + border.color: "yellow" + border.width: 5 + } + + traceInputKeyPanelDelegate: TraceInputKeyPanel { + id: traceInputKeyPanel + traceMargins: keyBackgroundMargin + Rectangle { + id: traceInputKeyPanelBackground + radius: 5 + color: "#35322f" + anchors.fill: traceInputKeyPanel + anchors.margins: keyBackgroundMargin + Text { + id: hwrInputModeIndicator + visible: control.patternRecognitionMode === InputEngine.PatternRecognitionMode.Handwriting + text: { + switch (InputContext.inputEngine.inputMode) { + case InputEngine.InputMode.Numeric: + if (["ar", "fa"].indexOf(InputContext.locale.substring(0, 2)) !== -1) + return "\u0660\u0661\u0662" + // Fallthrough + case InputEngine.InputMode.Dialable: + return "123" + case InputEngine.InputMode.Greek: + return "ΑΒΓ" + case InputEngine.InputMode.Cyrillic: + return "АБВ" + case InputEngine.InputMode.Arabic: + if (InputContext.locale.substring(0, 2) === "fa") + return "\u0627\u200C\u0628\u200C\u067E" + return "\u0623\u200C\u0628\u200C\u062C" + case InputEngine.InputMode.Hebrew: + return "\u05D0\u05D1\u05D2" + case InputEngine.InputMode.ChineseHandwriting: + return "中文" + case InputEngine.InputMode.JapaneseHandwriting: + return "日本語" + case InputEngine.InputMode.KoreanHandwriting: + return "한국어" + case InputEngine.InputMode.Thai: + return "กขค" + default: + return "Abc" + } + } + color: "white" + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: keyContentMargin + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint + capitalization: { + if (InputContext.capsLockActive) + return Font.AllUppercase + if (InputContext.shiftActive) + return Font.MixedCase + return Font.AllLowercase + } + } + } + } + Canvas { + id: traceInputKeyGuideLines + anchors.fill: traceInputKeyPanelBackground + opacity: 0.1 + onPaint: { + var ctx = getContext("2d") + ctx.lineWidth = 1 + ctx.strokeStyle = Qt.rgba(0xFF, 0xFF, 0xFF) + ctx.clearRect(0, 0, width, height) + var i + var margin = Math.round(30 * scaleHint) + if (control.horizontalRulers) { + for (i = 0; i < control.horizontalRulers.length; i++) { + ctx.beginPath() + var y = Math.round(control.horizontalRulers[i]) + var rightMargin = Math.round(width - margin) + if (i + 1 === control.horizontalRulers.length) { + ctx.moveTo(margin, y) + ctx.lineTo(rightMargin, y) + } else { + var dashLen = Math.round(20 * scaleHint) + for (var dash = margin, dashCount = 0; + dash < rightMargin; dash += dashLen, dashCount++) { + if ((dashCount & 1) === 0) { + ctx.moveTo(dash, y) + ctx.lineTo(Math.min(dash + dashLen, rightMargin), y) + } + } + } + ctx.stroke() + } + } + if (control.verticalRulers) { + for (i = 0; i < control.verticalRulers.length; i++) { + ctx.beginPath() + ctx.moveTo(control.verticalRulers[i], margin) + ctx.lineTo(control.verticalRulers[i], Math.round(height - margin)) + ctx.stroke() + } + } + } + Connections { + target: control + onHorizontalRulersChanged: traceInputKeyGuideLines.requestPaint() + onVerticalRulersChanged: traceInputKeyGuideLines.requestPaint() + } + } + } + + traceCanvasDelegate: TraceCanvas { + id: traceCanvas + onAvailableChanged: { + if (!available) + return + var ctx = getContext("2d") + if (parent.canvasType === "fullscreen") { + ctx.lineWidth = 10 + ctx.strokeStyle = Qt.rgba(0, 0, 0) + } else { + ctx.lineWidth = 10 * scaleHint + ctx.strokeStyle = Qt.rgba(0xFF, 0xFF, 0xFF) + } + ctx.lineCap = "round" + ctx.fillStyle = ctx.strokeStyle + } + autoDestroyDelay: 800 + onTraceChanged: if (trace === null) opacity = 0 + Behavior on opacity { PropertyAnimation { easing.type: Easing.OutCubic; duration: 150 } } + } + + popupListDelegate: SelectionListItem { + property real cursorAnchor: popupListLabel.x + popupListLabel.width + id: popupListItem + width: popupListLabel.width + popupListLabel.anchors.leftMargin * 2 + height: popupListLabel.height + popupListLabel.anchors.topMargin * 2 + Text { + id: popupListLabel + anchors.left: parent.left + anchors.top: parent.top + anchors.leftMargin: popupListLabel.height / 2 + anchors.topMargin: popupListLabel.height / 3 + text: decorateText(display, wordCompletionLength) + color: "#5CAA15" + font { + family: fontFamily + weight: Font.Normal + pixelSize: Qt.inputMethod.cursorRectangle.height * 0.8 + } + function decorateText(text, wordCompletionLength) { + if (wordCompletionLength > 0) { + return text.slice(0, -wordCompletionLength) + '' + text.slice(-wordCompletionLength) + '' + } + return text + } + } + states: State { + name: "current" + when: popupListItem.ListView.isCurrentItem + PropertyChanges { + target: popupListLabel + color: "black" + } + } + } + + popupListBackground: Item { + Rectangle { + width: parent.width + height: parent.height + color: "white" + border { + width: 1 + color: "#929495" + } + } + } + + popupListAdd: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 200 } + } + + popupListRemove: Transition { + NumberAnimation { property: "opacity"; to: 0; duration: 200 } + } + + languagePopupListEnabled: true + + languageListDelegate: SelectionListItem { + id: languageListItem + width: languageNameTextMetrics.width * 17 + height: languageNameTextMetrics.height + languageListLabel.anchors.topMargin + languageListLabel.anchors.bottomMargin + Text { + id: languageListLabel + anchors.left: parent.left + anchors.top: parent.top + anchors.leftMargin: languageNameTextMetrics.height / 2 + anchors.rightMargin: anchors.leftMargin + anchors.topMargin: languageNameTextMetrics.height / 3 + anchors.bottomMargin: anchors.topMargin + text: languageNameFormatter.elidedText + // color: "#5CAA15" + color: constants.mutedForeground + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint * 2 + } + } + TextMetrics { + id: languageNameTextMetrics + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint * 2 + } + text: "X" + } + TextMetrics { + id: languageNameFormatter + font { + family: fontFamily + weight: Font.Normal + pixelSize: 44 * scaleHint * 2 + } + elide: Text.ElideRight + elideWidth: languageListItem.width - languageListLabel.anchors.leftMargin - languageListLabel.anchors.rightMargin + text: displayName + } + states: State { + name: "current" + when: languageListItem.ListView.isCurrentItem + PropertyChanges { + target: languageListLabel + color: 'white' + } + } + } + + languageListBackground: Rectangle { + color: constants.lighterBackground + + border { + width: 1 + color: "#929495" + } + } + + languageListAdd: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 200 } + } + + languageListRemove: Transition { + NumberAnimation { property: "opacity"; to: 0; duration: 200 } + } + + selectionHandle: Image { + sourceSize.width: 20 + source: resourcePrefix + "images/selectionhandle-bottom.svg" + } + + fullScreenInputContainerBackground: Rectangle { + color: "#FFF" + } + + fullScreenInputBackground: Rectangle { + color: "#FFF" + } + + fullScreenInputMargins: Math.round(15 * scaleHint) + + fullScreenInputPadding: Math.round(30 * scaleHint) + + fullScreenInputCursor: Rectangle { + width: 1 + color: "#000" + visible: parent.blinkStatus + } + + fullScreenInputFont.pixelSize: 58 * scaleHint +} diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index fdbca3980..171e50085 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -48,8 +48,14 @@ class ElectrumGui(Logger): def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): set_language(config.get('language', self.get_default_language())) Logger.__init__(self) - #os.environ['QML_IMPORT_TRACE'] = '1' - #os.environ['QT_DEBUG_PLUGINS'] = '1' + + # uncomment to debug plugin and import tracing + # os.environ['QML_IMPORT_TRACE'] = '1' + # os.environ['QT_DEBUG_PLUGINS'] = '1' + + os.environ['QT_IM_MODULE'] = 'qtvirtualkeyboard' + os.environ['QT_VIRTUALKEYBOARD_STYLE'] = 'Electrum' + os.environ['QML2_IMPORT_PATH'] = 'electrum/gui/qml' self.logger.info(f"Qml GUI starting up... Qt={QT_VERSION_STR}, PyQt={PYQT_VERSION_STR}") self.logger.info("CWD=%s" % os.getcwd()) diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 1caa8e71d..0bcce3bdb 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -12,7 +12,7 @@ Dialog { close() } - parent: Overlay.overlay + parent: Overlay.overlay.children[0] modal: true Overlay.modal: Rectangle { color: "#aa000000" diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index a44cdbb13..1f9b49615 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -6,6 +6,7 @@ import QtQuick.Controls.Material.impl 2.12 import QtQml 2.6 import QtMultimedia 5.6 +import QtQuick.VirtualKeyboard 2.15 import org.electrum 1.0 @@ -30,6 +31,7 @@ ApplicationWindow Constants { id: appconstants } property alias stack: mainStackView + property alias inputPanel: inputPanel property variant activeDialogs: [] @@ -216,8 +218,9 @@ ApplicationWindow StackView { id: mainStackView - anchors.fill: parent - + // anchors.fill: parent + width: parent.width + height: inputPanel.y - header.height initialItem: Qt.resolvedUrl('WalletMainView.qml') function getRoot() { @@ -258,26 +261,51 @@ ApplicationWindow } } + Item { + // Item as first child in Overlay that adjusts its size to the available + // screen space minus the virtual keyboard (e.g. to center dialogs in) + parent: Overlay.overlay + width: parent.width + height: inputPanel.y + } + + InputPanel { + id: inputPanel + width: parent.width + y: parent.height + + states: State { + name: "visible" + when: inputPanel.active + PropertyChanges { + target: inputPanel + y: parent.height - height + } + } + transitions: Transition { + from: '' + to: 'visible' + reversible: true + ParallelAnimation { + NumberAnimation { + properties: "y" + duration: 250 + easing.type: Easing.OutQuad + } + } + } + } + property alias newWalletWizard: _newWalletWizard Component { id: _newWalletWizard - NewWalletWizard { - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - } + NewWalletWizard { } } property alias serverConnectWizard: _serverConnectWizard Component { id: _serverConnectWizard - ServerConnectWizard { - parent: Overlay.overlay - Overlay.modal: Rectangle { - color: "#aa000000" - } - } + ServerConnectWizard { } } property alias messageDialog: _messageDialog From 4bdd521a4bbdd0d325eceba8ecf0539bfb023a2c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 22 Mar 2023 13:56:03 +0100 Subject: [PATCH 0496/1143] qml: abstract ElDialog resize behavior to property --- electrum/gui/qml/components/LoadingWalletDialog.qml | 2 +- electrum/gui/qml/components/controls/ElDialog.qml | 3 ++- electrum/gui/qml/components/main.qml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/LoadingWalletDialog.qml b/electrum/gui/qml/components/LoadingWalletDialog.qml index b7c4ed5a1..cbe8408bb 100644 --- a/electrum/gui/qml/components/LoadingWalletDialog.qml +++ b/electrum/gui/qml/components/LoadingWalletDialog.qml @@ -13,7 +13,7 @@ ElDialog { title: qsTr('Loading Wallet') iconSource: Qt.resolvedUrl('../../icons/wallet.png') - parent: Overlay.overlay + resizeWithKeyboard: false x: Math.floor((parent.width - implicitWidth) / 2) y: Math.floor((parent.height - implicitHeight) / 2) diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 0bcce3bdb..82bafd16f 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -7,12 +7,13 @@ Dialog { property bool allowClose: true property string iconSource + property bool resizeWithKeyboard: true function doClose() { close() } - parent: Overlay.overlay.children[0] + parent: resizeWithKeyboard ? Overlay.overlay.children[0] : Overlay.overlay modal: true Overlay.modal: Rectangle { color: "#aa000000" diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 1f9b49615..461491b35 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -264,6 +264,7 @@ ApplicationWindow Item { // Item as first child in Overlay that adjusts its size to the available // screen space minus the virtual keyboard (e.g. to center dialogs in) + // see ElDialog.resizeWithKeyboard property parent: Overlay.overlay width: parent.width height: inputPanel.y From 75e5e4afd83a5efd9e3ae2d1f85b3ce1a134094f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 22 Mar 2023 13:56:14 +0100 Subject: [PATCH 0497/1143] android: set default localization to en_GB to force number formatting and parsing to en_GB --- electrum/gui/qml/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 171e50085..cd0704619 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -46,8 +46,8 @@ class ElectrumGui(Logger): @profiler def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): - set_language(config.get('language', self.get_default_language())) Logger.__init__(self) + set_language(config.get('language', self.get_default_language())) # uncomment to debug plugin and import tracing # os.environ['QML_IMPORT_TRACE'] = '1' @@ -57,6 +57,11 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') os.environ['QT_VIRTUALKEYBOARD_STYLE'] = 'Electrum' os.environ['QML2_IMPORT_PATH'] = 'electrum/gui/qml' + # set default locale to en_GB. This is for l10n (e.g. number formatting, number input etc), + # but not for i18n, which is handled by the Translator + # this can be removed once the backend wallet is fully l10n aware + QLocale.setDefault(QLocale('en_GB')) + self.logger.info(f"Qml GUI starting up... Qt={QT_VERSION_STR}, PyQt={PYQT_VERSION_STR}") self.logger.info("CWD=%s" % os.getcwd()) # Uncomment this call to verify objects are being properly @@ -111,5 +116,8 @@ def stop(self): self.app.quit() def get_default_language(self): + # On Android this does not return the system locale + # TODO: retrieve through Android API name = QLocale.system().name() - return name if name in languages else 'en_UK' + self.logger.debug(f'System default locale: {name}') + return name if name in languages else 'en_GB' From f89e0b80e6a312059db7dcda4485e23a13e22571 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 10:46:28 +0100 Subject: [PATCH 0498/1143] qml: wizard add label for second password entry --- electrum/gui/qml/components/wizard/WCWalletPassword.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/gui/qml/components/wizard/WCWalletPassword.qml b/electrum/gui/qml/components/wizard/WCWalletPassword.qml index 73a7e46c0..78e3903f1 100644 --- a/electrum/gui/qml/components/wizard/WCWalletPassword.qml +++ b/electrum/gui/qml/components/wizard/WCWalletPassword.qml @@ -21,6 +21,9 @@ WizardComponent { PasswordField { id: password1 } + Label { + text: qsTr('Enter password (again)') + } PasswordField { id: password2 showReveal: false From abae8157772931506fd2e3062d5dd7843d377e22 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 10:56:42 +0100 Subject: [PATCH 0499/1143] qml: TxDetails small form-factor fix, wrap buttons to below fee-bump text if width is constrained --- electrum/gui/qml/components/TxDetails.qml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 8c40c56fc..68e6894e4 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -138,17 +138,23 @@ Pane { borderColor: constants.colorWarning visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel - RowLayout { + GridLayout { width: parent.width + columns: actionButtonsLayout.implicitWidth > parent.width/2 + ? 1 + : 2 Label { Layout.fillWidth: true - text: qsTr('This transaction is still unconfirmed.') + '\n' + - qsTr('You can increase fees to speed up the transaction, or cancel this transaction') + text: qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel + ? qsTr('You can increase fees to speed up the transaction, or cancel this transaction') + : qsTr('You can increase fees to speed up the transaction')) wrapMode: Text.Wrap } ColumnLayout { + id: actionButtonsLayout Layout.alignment: Qt.AlignHCenter Pane { + Layout.alignment: Qt.AlignHCenter background: Rectangle { color: Material.dialogColor } padding: 0 visible: txdetails.canBump || txdetails.canCpfp @@ -168,6 +174,7 @@ Pane { } } Pane { + Layout.alignment: Qt.AlignHCenter background: Rectangle { color: Material.dialogColor } padding: 0 visible: txdetails.canCancel From 39097783c3a7a603a524a0a3f50f63e674dac9e9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 23 Mar 2023 11:04:58 +0100 Subject: [PATCH 0500/1143] qml: ask password to show seed --- electrum/gui/qml/qewallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index c182625dc..ec63b3f39 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -696,7 +696,7 @@ def isValidChannelBackup(self, backup_str): def requestShowSeed(self): self.retrieve_seed() - @auth_protect + @auth_protect(method='wallet') def retrieve_seed(self): try: self._seed = self.wallet.get_seed(self.password) From 8e7cbd6ca227b132903d2daa18fc5de7c083a5a4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 23 Mar 2023 12:43:43 +0100 Subject: [PATCH 0501/1143] qml: let user enter lnurl6 amount --- .../qml/components/LnurlPayRequestDialog.qml | 25 +++++++++++++------ electrum/gui/qml/qeinvoice.py | 9 +++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index 4bbbc5cd3..3c836852c 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -46,18 +46,27 @@ ElDialog { Layout.fillWidth: true wrapMode: Text.Wrap } + Label { - text: invoiceParser.lnurlData['min_sendable_sat'] == invoiceParser.lnurlData['max_sendable_sat'] - ? qsTr('Amount') - : qsTr('Amount range') + text: qsTr('Amount') color: Material.accentColor } + + BtcField { + id: amountBtc + text: Config.formatSats(invoiceParser.lnurlData['min_sendable_sat']) + enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] + fiatfield: null + Layout.preferredWidth: parent.width /3 + onTextAsSatsChanged: { + invoiceParser.amountOverride = textAsSats + } + } Label { + Layout.columnSpan: 2 text: invoiceParser.lnurlData['min_sendable_sat'] == invoiceParser.lnurlData['max_sendable_sat'] - ? invoiceParser.lnurlData['min_sendable_sat'] == 0 - ? qsTr('Unspecified') - : invoiceParser.lnurlData['min_sendable_sat'] - : invoiceParser.lnurlData['min_sendable_sat'] + ' < amount < ' + invoiceParser.lnurlData['max_sendable_sat'] + ? '' + : qsTr('Amount must be between %1 and %2').arg(Config.formatSats(invoiceParser.lnurlData['min_sendable_sat'])).arg(Config.formatSats(invoiceParser.lnurlData['max_sendable_sat'])) + Config.baseUnit } TextArea { @@ -86,7 +95,7 @@ ElDialog { icon.source: '../../icons/confirmed.png' enabled: valid onClicked: { - invoiceParser.lnurlGetInvoice(invoiceParser.lnurlData['min_sendable_sat'], comment.text) + invoiceParser.lnurlGetInvoice(comment.text) dialog.close() } } diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index b60da544e..3e4d6204b 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -552,6 +552,15 @@ def on_lnurl(self, lnurldata): def lnurlGetInvoice(self, amount, comment=None): assert self._lnurlData + amount = self.amountOverride.satsInt + if self.lnurlData['min_sendable_sat'] != 0: + try: + assert amount >= self.lnurlData['min_sendable_sat'] + assert amount <= self.lnurlData['max_sendable_sat'] + except: + self.lnurlError.emit('lnurl', _('Amount out of bounds')) + return + if self._lnurlData['comment_allowed'] == 0: comment = None From f9a5c2263380476dfac619518998a88411a58e9b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 14:00:46 +0100 Subject: [PATCH 0502/1143] qml: lnurl override disabled amount edit color, show lnurlError to user --- .../gui/qml/components/LnurlPayRequestDialog.qml | 14 ++++++++++++-- electrum/gui/qml/qeinvoice.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index 3c836852c..ead002b68 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -55,10 +55,11 @@ ElDialog { BtcField { id: amountBtc text: Config.formatSats(invoiceParser.lnurlData['min_sendable_sat']) - enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] + enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] + color: Material.foreground // override gray-out on disabled fiatfield: null Layout.preferredWidth: parent.width /3 - onTextAsSatsChanged: { + onTextAsSatsChanged: { invoiceParser.amountOverride = textAsSats } } @@ -100,4 +101,13 @@ ElDialog { } } } + + Connections { + target: invoiceParser + function onLnurlError(code, message) { + var dialog = app.messageDialog.createObject(app, { text: message }) + dialog.open() + } + } + } diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 3e4d6204b..367157d7e 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -558,7 +558,7 @@ def lnurlGetInvoice(self, amount, comment=None): assert amount >= self.lnurlData['min_sendable_sat'] assert amount <= self.lnurlData['max_sendable_sat'] except: - self.lnurlError.emit('lnurl', _('Amount out of bounds')) + self.lnurlError.emit('amount', _('Amount out of bounds')) return if self._lnurlData['comment_allowed'] == 0: From 2231057d1e4a0c92b2b41c0dd62a8609d17b264b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 23 Mar 2023 14:12:39 +0000 Subject: [PATCH 0503/1143] android build: allow specifying "x86_64" as target in build.sh --- contrib/android/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/android/build.sh b/contrib/android/build.sh index 0c58faaf8..17a5dfcb5 100755 --- a/contrib/android/build.sh +++ b/contrib/android/build.sh @@ -18,11 +18,11 @@ BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") # check arguments if [[ -n "$3" \ && ( "$1" == "kivy" || "$1" == "qml" ) \ - && ( "$2" == "all" || "$2" == "armeabi-v7a" || "$2" == "arm64-v8a" || "$2" == "x86" ) \ + && ( "$2" == "all" || "$2" == "armeabi-v7a" || "$2" == "arm64-v8a" || "$2" == "x86" || "$2" == "x86_64" ) \ && ( "$3" == "debug" || "$3" == "release" || "$3" == "release-unsigned" ) ]] ; then info "arguments $*" else - fail "usage: build.sh " + fail "usage: build.sh " exit 1 fi From 0ebcc7df631fbc76213812a9e2294e097bcb2b64 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 23 Mar 2023 14:34:47 +0000 Subject: [PATCH 0504/1143] qml: only do android-specific stuff when on android don't log an error when running on desktop --- electrum/gui/qml/qeapp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 8dc9077e3..1f07c165b 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -137,6 +137,8 @@ def notifyAndroid(self, wallet_name, message): self.logger.error(repr(e)) def bindIntent(self): + if not self.isAndroid(): + return try: from android import activity from jnius import autoclass From b2372f2d53056e0f966378d3351da63666a3f413 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 23 Mar 2023 14:38:19 +0000 Subject: [PATCH 0505/1143] android build: rm x86_64 target from "all" alias, since it is broken (and release.sh uses the "all" target) see https://github.com/spesmilo/electrum/issues/8278 --- contrib/android/make_apk.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/android/make_apk.sh b/contrib/android/make_apk.sh index d9b1bd9ef..c6d9acd30 100755 --- a/contrib/android/make_apk.sh +++ b/contrib/android/make_apk.sh @@ -93,8 +93,8 @@ if [[ "$2" == "all" ]] ; then make $TARGET #export APP_ANDROID_ARCH=x86 #make $TARGET - export APP_ANDROID_ARCH=x86_64 - make $TARGET + #export APP_ANDROID_ARCH=x86_64 + #make $TARGET else export APP_ANDROID_ARCH=$2 make $TARGET From fe968cfb4ba00bf55317604c67f327cbedf086d8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 23 Mar 2023 15:55:25 +0100 Subject: [PATCH 0506/1143] qml: (minor) change name of Amount field --- electrum/gui/qml/components/ReceiveDetailsDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 4d954a2fe..c16ab337e 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -74,7 +74,7 @@ ElDialog { } Label { - text: qsTr('Request') + text: qsTr('Amount') wrapMode: Text.WordWrap Layout.rightMargin: constants.paddingXLarge } From ee380bb747796052818b340f1999c846978c26e3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 23 Mar 2023 15:08:58 +0000 Subject: [PATCH 0507/1143] release.sh: use qml gui for android release apk --- contrib/release.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/release.sh b/contrib/release.sh index 34d1fa3ab..1e8389e8e 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -153,9 +153,9 @@ if test -f "dist/$apk1"; then info "file exists: $apk1" else if [ ! -z "$RELEASEMANAGER" ] ; then - ./contrib/android/build.sh kivy all release $password + ./contrib/android/build.sh qml all release $password else - ./contrib/android/build.sh kivy all release-unsigned + ./contrib/android/build.sh qml all release-unsigned mv "dist/$apk1_unsigned" "dist/$apk1" mv "dist/$apk2_unsigned" "dist/$apk2" fi From 743ea80a4cc072dfb07899b0753f35a40c8bf864 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 16:53:42 +0100 Subject: [PATCH 0508/1143] qml: move potentially slow tx generation in qeswaphelper to a short delay timer --- electrum/gui/qml/qeswaphelper.py | 49 ++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index dcea0173e..5730767c1 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -3,7 +3,7 @@ import math from typing import Union -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer from electrum.i18n import _ from electrum.lnutil import ln_dummy_address @@ -48,6 +48,12 @@ def __init__(self, parent=None): self._leftVoid = 0 self._rightVoid = 0 + self._fwd_swap_updatetx_timer = QTimer(self) + self._fwd_swap_updatetx_timer.setSingleShot(True) + # self._fwd_swap_updatetx_timer.setInterval(500) + self._fwd_swap_updatetx_timer.timeout.connect(self.fwd_swap_updatetx) + + walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) def wallet(self): @@ -274,14 +280,12 @@ def swap_slider_moved(self): self.userinfo = _('Adds Lightning receiving capacity.') self.isReverse = True - pay_amount = abs(position) - self._send_amount = pay_amount - self.tosend = QEAmount(amount_sat=pay_amount) + self._send_amount = abs(position) + self.tosend = QEAmount(amount_sat=self._send_amount) - receive_amount = swap_manager.get_recv_amount( - send_amount=pay_amount, is_reverse=True) - self._receive_amount = receive_amount - self.toreceive = QEAmount(amount_sat=receive_amount) + self._receive_amount = swap_manager.get_recv_amount( + send_amount=self._send_amount, is_reverse=True) + self.toreceive = QEAmount(amount_sat=self._receive_amount) # fee breakdown self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' @@ -289,33 +293,42 @@ def swap_slider_moved(self): self.server_miningfee = QEAmount(amount_sat=server_miningfee) self.miningfee = QEAmount(amount_sat=swap_manager.get_claim_fee()) + self.check_valid(self._send_amount, self._receive_amount) else: # forward (normal) swap self.userinfo = _('Adds Lightning sending capacity.') self.isReverse = False self._send_amount = position - self.update_tx(self._send_amount) - # add lockup fees, but the swap amount is position - pay_amount = position + self._tx.get_fee() if self._tx else 0 - self.tosend = QEAmount(amount_sat=pay_amount) - - receive_amount = swap_manager.get_recv_amount(send_amount=position, is_reverse=False) - self._receive_amount = receive_amount - self.toreceive = QEAmount(amount_sat=receive_amount) + self._receive_amount = swap_manager.get_recv_amount(send_amount=position, is_reverse=False) + self.toreceive = QEAmount(amount_sat=self._receive_amount) # fee breakdown self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' server_miningfee = swap_manager.normal_fee self.server_miningfee = QEAmount(amount_sat=server_miningfee) - self.miningfee = QEAmount(amount_sat=self._tx.get_fee()) if self._tx else QEAmount() - if pay_amount and receive_amount: + # the slow stuff we delegate to a delay timer which triggers after slider + # doesn't update for a while + self.valid = False # wait for timer + self._fwd_swap_updatetx_timer.start(250) + + def check_valid(self, send_amount, receive_amount): + if send_amount and receive_amount: self.valid = True else: # add more nuanced error reporting? self.userinfo = _('Swap below minimal swap size, change the slider.') self.valid = False + def fwd_swap_updatetx(self): + self.update_tx(self._send_amount) + # add lockup fees, but the swap amount is position + pay_amount = self._send_amount + self._tx.get_fee() if self._tx else 0 + self.tosend = QEAmount(amount_sat=pay_amount) + + self.miningfee = QEAmount(amount_sat=self._tx.get_fee()) if self._tx else QEAmount() + self.check_valid(pay_amount, self._receive_amount) + def do_normal_swap(self, lightning_amount, onchain_amount): assert self._tx if lightning_amount is None or onchain_amount is None: From aaff7502db9ba0b84b2175933982d9604d709b3a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 16:59:12 +0100 Subject: [PATCH 0509/1143] android: update P4A to 3c2750795ba93aa1a3e513a13c2ea2ac5bddba17 remove qt5 patch to disable avx/avx2 for x86_64 arch --- contrib/android/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 1139d7a45..d25466e08 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -180,7 +180,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "a3add4f5f2a15a0b56f0c09d729418e4dbef475f^{commit}" \ + && git checkout "3c2750795ba93aa1a3e513a13c2ea2ac5bddba17^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars From f9f57b58b40d91ead837301a6ddeb0f8ee2215ff Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 23 Mar 2023 17:01:01 +0100 Subject: [PATCH 0510/1143] Revert "android build: rm x86_64 target from "all" alias, since it is broken" This reverts commit b2372f2d53056e0f966378d3351da63666a3f413. x86_64 build should work now --- contrib/android/make_apk.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/android/make_apk.sh b/contrib/android/make_apk.sh index c6d9acd30..d9b1bd9ef 100755 --- a/contrib/android/make_apk.sh +++ b/contrib/android/make_apk.sh @@ -93,8 +93,8 @@ if [[ "$2" == "all" ]] ; then make $TARGET #export APP_ANDROID_ARCH=x86 #make $TARGET - #export APP_ANDROID_ARCH=x86_64 - #make $TARGET + export APP_ANDROID_ARCH=x86_64 + make $TARGET else export APP_ANDROID_ARCH=$2 make $TARGET From 8c1fe10f54a3982faf13e7503d8abdb977b993ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 23 Mar 2023 16:57:16 +0000 Subject: [PATCH 0511/1143] qml TxDetails: show short_id instead of block height and txpos The "TX index" (txpos) item I think was confusing. --- electrum/gui/qml/components/TxDetails.qml | 15 ++------------- electrum/gui/qml/qetxdetails.py | 20 +++++++------------- electrum/util.py | 6 ++++++ 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 68e6894e4..68000b298 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -207,24 +207,13 @@ Pane { Label { visible: txdetails.isMined - text: qsTr('Height') + text: qsTr('Mined at') color: Material.accentColor } Label { visible: txdetails.isMined - text: txdetails.height - } - - Label { - visible: txdetails.isMined - text: qsTr('TX index') - color: Material.accentColor - } - - Label { - visible: txdetails.isMined - text: txdetails.txpos + text: txdetails.shortId } Label { diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 54e3cab05..40e8176b2 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -4,7 +4,7 @@ from electrum.i18n import _ from electrum.logging import get_logger -from electrum.util import format_time, AddTransactionException +from electrum.util import format_time, AddTransactionException, TxMinedInfo from electrum.transaction import tx_from_any from electrum.network import Network @@ -56,10 +56,9 @@ def __init__(self, parent=None): self._mempool_depth = '' self._date = '' - self._height = 0 self._confirmations = 0 - self._txpos = -1 self._header_hash = '' + self._short_id = "" def on_destroy(self): self.unregister_callbacks() @@ -166,17 +165,13 @@ def mempoolDepth(self): def date(self): return self._date - @pyqtProperty(int, notify=detailsChanged) - def height(self): - return self._height - @pyqtProperty(int, notify=detailsChanged) def confirmations(self): return self._confirmations - @pyqtProperty(int, notify=detailsChanged) - def txpos(self): - return self._txpos + @pyqtProperty(str, notify=detailsChanged) + def shortId(self): + return self._short_id @pyqtProperty(str, notify=detailsChanged) def headerHash(self): @@ -296,13 +291,12 @@ def update(self): self._label = txinfo.label self.labelChanged.emit() - def update_mined_status(self, tx_mined_info): + def update_mined_status(self, tx_mined_info: TxMinedInfo): self._mempool_depth = '' self._date = format_time(tx_mined_info.timestamp) - self._height = tx_mined_info.height self._confirmations = tx_mined_info.conf - self._txpos = tx_mined_info.txpos self._header_hash = tx_mined_info.header_hash + self._short_id = tx_mined_info.short_id() or "" @pyqtSlot() @pyqtSlot(bool) diff --git a/electrum/util.py b/electrum/util.py index edd0ff6c8..9ad8fbb0f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1299,6 +1299,12 @@ class TxMinedInfo(NamedTuple): header_hash: Optional[str] = None # hash of block that mined tx wanted_height: Optional[int] = None # in case of timelock, min abs block height + def short_id(self) -> Optional[str]: + if self.txpos is not None and self.txpos >= 0: + assert self.height > 0 + return f"{self.height}x{self.txpos}" + return None + class ShortID(bytes): From f53522f0c933eb6d764c2c77962012829c6ef69d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 24 Mar 2023 10:47:50 +0000 Subject: [PATCH 0512/1143] release: also build android apk for x86_64 arch related: f9f57b58b40d91ead837301a6ddeb0f8ee2215ff (note: this should be enough to put the apk onto download.electrum.org, but it is not yet linked from the main website) --- contrib/add_cosigner | 1 + contrib/make_download | 1 + contrib/release.sh | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/contrib/add_cosigner b/contrib/add_cosigner index 63f26f2e3..dc7c5c591 100755 --- a/contrib/add_cosigner +++ b/contrib/add_cosigner @@ -51,6 +51,7 @@ files = { "win_portable": f"electrum-{version_win}-portable.exe", "apk_arm64": f"Electrum-{APK_VERSION}-arm64-v8a-release.apk", "apk_armeabi": f"Electrum-{APK_VERSION}-armeabi-v7a-release.apk", + "apk_x86_64": f"Electrum-{APK_VERSION}-x86_64-release.apk", } diff --git a/contrib/make_download b/contrib/make_download index 4b72a9200..70eb7f003 100755 --- a/contrib/make_download +++ b/contrib/make_download @@ -47,6 +47,7 @@ files = { "win_portable": f"electrum-{version_win}-portable.exe", "apk_arm64": f"Electrum-{APK_VERSION}-arm64-v8a-release.apk", "apk_armeabi": f"Electrum-{APK_VERSION}-armeabi-v7a-release.apk", + "apk_x86_64": f"Electrum-{APK_VERSION}-x86_64-release.apk", } # default signers diff --git a/contrib/release.sh b/contrib/release.sh index 1e8389e8e..ce5b59634 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -149,6 +149,8 @@ apk1="Electrum-$VERSION.0-armeabi-v7a-release.apk" apk1_unsigned="Electrum-$VERSION.0-armeabi-v7a-release-unsigned.apk" apk2="Electrum-$VERSION.0-arm64-v8a-release.apk" apk2_unsigned="Electrum-$VERSION.0-arm64-v8a-release-unsigned.apk" +apk3="Electrum-$VERSION.0-x86_64-release.apk" +apk3_unsigned="Electrum-$VERSION.0-x86_64-release-unsigned.apk" if test -f "dist/$apk1"; then info "file exists: $apk1" else @@ -158,6 +160,7 @@ else ./contrib/android/build.sh qml all release-unsigned mv "dist/$apk1_unsigned" "dist/$apk1" mv "dist/$apk2_unsigned" "dist/$apk2" + mv "dist/$apk3_unsigned" "dist/$apk3" fi fi @@ -218,6 +221,7 @@ if [ -z "$RELEASEMANAGER" ] ; then test -f "$win3" || fail "win3 not found among sftp downloads" test -f "$apk1" || fail "apk1 not found among sftp downloads" test -f "$apk2" || fail "apk2 not found among sftp downloads" + test -f "$apk3" || fail "apk3 not found among sftp downloads" test -f "$dmg" || fail "dmg not found among sftp downloads" test -f "$PROJECT_ROOT/dist/$tarball" || fail "tarball not found among built files" test -f "$PROJECT_ROOT/dist/$srctarball" || fail "srctarball not found among built files" @@ -227,6 +231,7 @@ if [ -z "$RELEASEMANAGER" ] ; then test -f "$CONTRIB/build-wine/dist/$win3" || fail "win3 not found among built files" test -f "$PROJECT_ROOT/dist/$apk1" || fail "apk1 not found among built files" test -f "$PROJECT_ROOT/dist/$apk2" || fail "apk2 not found among built files" + test -f "$PROJECT_ROOT/dist/$apk3" || fail "apk3 not found among built files" test -f "$PROJECT_ROOT/dist/$dmg" || fail "dmg not found among built files" # compare downloaded binaries against ones we built cmp --silent "$tarball" "$PROJECT_ROOT/dist/$tarball" || fail "files are different. tarball." @@ -237,11 +242,12 @@ if [ -z "$RELEASEMANAGER" ] ; then "$CONTRIB/build-wine/unsign.sh" || fail "files are different. windows." "$CONTRIB/android/apkdiff.py" "$apk1" "$PROJECT_ROOT/dist/$apk1" || fail "files are different. android." "$CONTRIB/android/apkdiff.py" "$apk2" "$PROJECT_ROOT/dist/$apk2" || fail "files are different. android." + "$CONTRIB/android/apkdiff.py" "$apk3" "$PROJECT_ROOT/dist/$apk3" || fail "files are different. android." cmp --silent "$dmg" "$PROJECT_ROOT/dist/$dmg" || fail "files are different. macos." # all files matched. sign them. rm -rf "$PROJECT_ROOT/dist/sigs/" mkdir --parents "$PROJECT_ROOT/dist/sigs/" - for fname in "$tarball" "$srctarball" "$appimage" "$win1" "$win2" "$win3" "$apk1" "$apk2" "$dmg" ; do + for fname in "$tarball" "$srctarball" "$appimage" "$win1" "$win2" "$win3" "$apk1" "$apk2" "$apk3" "$dmg" ; do signame="$fname.$GPGUSER.asc" gpg --sign --armor --detach $PUBKEY --output "$PROJECT_ROOT/dist/sigs/$signame" "$fname" done From 965e1ac9a3d8c8741b6f7a46bf4e4da91dd86aa1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 24 Mar 2023 13:48:42 +0000 Subject: [PATCH 0513/1143] android: update readme, list required apt packages for qml on desktop --- contrib/android/Readme.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/contrib/android/Readme.md b/contrib/android/Readme.md index ad77a9fdf..059370fb3 100644 --- a/contrib/android/Readme.md +++ b/contrib/android/Readme.md @@ -1,8 +1,10 @@ -# Kivy GUI +# Qml GUI -The Kivy GUI is used with Electrum on Android devices. +The Qml GUI is used with Electrum on Android devices, since Electrum 4.4. To generate an APK file, follow these instructions. +(note: older versions of Electrum for Android used the "kivy" GUI) + ## Android binary with Docker ✓ _These binaries should be reproducible, meaning you should be able to generate @@ -21,11 +23,11 @@ similar system. ``` $ ./build.sh ``` - For development, consider e.g. `$ ./build.sh kivy arm64-v8a debug` + For development, consider e.g. `$ ./build.sh qml arm64-v8a debug` If you want reproducibility, try instead e.g.: ``` - $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh kivy all release-unsigned + $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh qml all release-unsigned ``` 3. The generated binary is in `./dist`. @@ -90,7 +92,20 @@ adb logcat | grep -F "`adb shell ps | grep org.electrum.electrum | cut -c14-19`" ``` -### Kivy can be run directly on Linux Desktop. How? +### The Qml GUI can be run directly on Linux Desktop. How? +Install requirements (debian-based distros): +``` +sudo apt-get install python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtmultimedia +sudo apt-get install python3-pil +sudo apt-get install qml-module-qtquick-controls2 qml-module-qtquick-layouts \ + qml-module-qtquick-window2 qml-module-qtmultimedia \ + libqt5multimedia5-plugins qml-module-qt-labs-folderlistmodel +sudo apt-get install qtvirtualkeyboard-plugin +``` + +Run electrum with the `-g` switch: `electrum -g qml` + +### The Kivy GUI can be run directly on Linux Desktop. How? Install Kivy. Build atlas: `(cd contrib/android/; make theming)` From 78d79290ad156122075d38c7ba7786d13821d2fd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 25 Mar 2023 12:58:22 +0100 Subject: [PATCH 0514/1143] qml: create workaround for spurious textChanged events coming from TextArea. fixes #8280 This commit also fixes a gap, where a seed text change could leave the page valid for the duration of the valid check delay timer, while the seed is actually invalid. --- .../gui/qml/components/controls/SeedTextArea.qml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 49c9bc25a..0047869be 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -10,12 +10,17 @@ Pane { implicitHeight: rootLayout.height padding: 0 + property string text property alias readOnly: seedtextarea.readOnly - property alias text: seedtextarea.text property alias placeholderText: seedtextarea.placeholderText property var _suggestions: [] + onTextChanged: { + if (seedtextarea.text != text) + seedtextarea.text = text + } + background: Rectangle { color: "transparent" } @@ -84,6 +89,12 @@ Pane { } onTextChanged: { + // work around Qt issue, TextArea fires spurious textChanged events + // NOTE: might be Qt virtual keyboard, or Qt upgrade from 5.15.2 to 5.15.7 + if (root.text != text) + root.text = text + + // update suggestions _suggestions = bitcoin.mnemonicsFor(seedtextarea.text.split(' ').pop()) // TODO: cursorPosition only on suggestion apply cursorPosition = text.length From d0f3e048b993bca87622a2c464273ca844a7b677 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 25 Mar 2023 13:14:50 +0100 Subject: [PATCH 0515/1143] qml: followup 78d79290ad156122075d38c7ba7786d13821d2fd --- electrum/gui/qml/components/wizard/WCHaveSeed.qml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 6fe39f1e8..596b7ddce 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -173,7 +173,7 @@ WizardComponent { placeholderText: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') onTextChanged: { - validationTimer.restart() + startValidationTimer() } Rectangle { @@ -207,7 +207,7 @@ WizardComponent { id: extendcb Layout.columnSpan: 2 text: qsTr('Extend seed with custom words') - onCheckedChanged: validationTimer.restart() + onCheckedChanged: startValidationTimer() } TextField { id: customwordstext @@ -215,7 +215,7 @@ WizardComponent { Layout.fillWidth: true Layout.columnSpan: 2 placeholderText: qsTr('Enter your custom word(s)') - onTextChanged: validationTimer.restart() + onTextChanged: startValidationTimer() } } } @@ -226,6 +226,12 @@ WizardComponent { onValidationMessageChanged: validationtext.text = validationMessage } + function startValidationTimer() { + valid = false + contentText.text = '' + validationTimer.restart() + } + Timer { id: validationTimer interval: 500 From 1e6b69251670dd3bf55ccac2d0646e1b5af8d460 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Mar 2023 11:46:00 +0200 Subject: [PATCH 0516/1143] qml: bind invoice.amount to internal property. fixes #8262 --- electrum/gui/qml/components/InvoiceDialog.qml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 4e31ed965..01f65b15a 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -23,6 +23,8 @@ ElDialog { property bool _canMax: invoice.invoiceType == Invoice.OnchainInvoice + property Amount _invoice_amount: invoice.amount + ColumnLayout { anchors.fill: parent spacing: 0 @@ -184,7 +186,7 @@ ElDialog { Label { Layout.columnSpan: 2 Layout.fillWidth: true - visible: invoice.amount.isMax + visible: _invoice_amount.isMax font.pixelSize: constants.fontSizeXLarge font.bold: true text: qsTr('All on-chain funds') @@ -193,7 +195,7 @@ ElDialog { Label { Layout.columnSpan: 2 Layout.fillWidth: true - visible: invoice.amount.isEmpty + visible: _invoice_amount.isEmpty font.pixelSize: constants.fontSizeXLarge color: constants.mutedForeground text: qsTr('not specified') @@ -201,7 +203,7 @@ ElDialog { Label { Layout.alignment: Qt.AlignRight - visible: !invoice.amount.isMax && !invoice.amount.isEmpty + visible: !_invoice_amount.isMax && !_invoice_amount.isEmpty font.pixelSize: constants.fontSizeXLarge font.family: FixedFont font.bold: true @@ -210,7 +212,7 @@ ElDialog { Label { Layout.fillWidth: true - visible: !invoice.amount.isMax && !invoice.amount.isEmpty + visible: !_invoice_amount.isMax && !_invoice_amount.isEmpty text: Config.baseUnit color: Material.accentColor font.pixelSize: constants.fontSizeXLarge @@ -219,7 +221,7 @@ ElDialog { Label { id: fiatValue Layout.alignment: Qt.AlignRight - visible: Daemon.fx.enabled && !invoice.amount.isMax && !invoice.amount.isEmpty + visible: Daemon.fx.enabled && !_invoice_amount.isMax && !_invoice_amount.isEmpty text: Daemon.fx.fiatValue(invoice.amount, false) font.pixelSize: constants.fontSizeMedium color: constants.mutedForeground @@ -227,7 +229,7 @@ ElDialog { Label { Layout.fillWidth: true - visible: Daemon.fx.enabled && !invoice.amount.isMax && !invoice.amount.isEmpty + visible: Daemon.fx.enabled && !_invoice_amount.isMax && !_invoice_amount.isEmpty text: Daemon.fx.fiatCurrency font.pixelSize: constants.fontSizeMedium color: constants.mutedForeground From 229047de1991a13ebda8a9e6af62e5df4bba3294 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Mar 2023 11:54:28 +0200 Subject: [PATCH 0517/1143] qml: followup 1e6b69251670dd3bf55ccac2d0646e1b5af8d460 --- electrum/gui/qml/components/InvoiceDialog.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 01f65b15a..b72e757a6 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -207,7 +207,7 @@ ElDialog { font.pixelSize: constants.fontSizeXLarge font.family: FixedFont font.bold: true - text: Config.formatSats(invoice.amount, false) + text: Config.formatSats(_invoice_amount, false) } Label { @@ -222,7 +222,7 @@ ElDialog { id: fiatValue Layout.alignment: Qt.AlignRight visible: Daemon.fx.enabled && !_invoice_amount.isMax && !_invoice_amount.isEmpty - text: Daemon.fx.fiatValue(invoice.amount, false) + text: Daemon.fx.fiatValue(_invoice_amount, false) font.pixelSize: constants.fontSizeMedium color: constants.mutedForeground } @@ -240,7 +240,7 @@ ElDialog { GridLayout { Layout.fillWidth: true visible: amountContainer.editmode - enabled: !(invoice.status == Invoice.Expired && invoice.amount.isEmpty) + enabled: !(invoice.status == Invoice.Expired && _invoice_amount.isEmpty) columns: 3 From cc9b0220895a2f27049a64d474988a72caf5deea Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Mar 2023 12:01:55 +0200 Subject: [PATCH 0518/1143] qml: don't update wizard valid state from wizard pages that are not the current page --- electrum/gui/qml/components/wizard/Wizard.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index c2c38329e..add892484 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -63,6 +63,8 @@ ElDialog { Object.assign(wdata_copy, wdata) var page = comp.createObject(pages, {wizard_data: wdata_copy}) page.validChanged.connect(function() { + if (page != pages.currentItem) + return pages.pagevalid = page.valid } ) page.lastChanged.connect(function() { From edffbee92daf0b1b6072c582bd288e9c44c6b640 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 27 Mar 2023 12:03:46 +0200 Subject: [PATCH 0519/1143] qml: same for last --- electrum/gui/qml/components/wizard/Wizard.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index add892484..80818c0db 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -66,10 +66,12 @@ ElDialog { if (page != pages.currentItem) return pages.pagevalid = page.valid - } ) + }) page.lastChanged.connect(function() { + if (page != pages.currentItem) + return pages.lastpage = page.last - } ) + }) page.next.connect(function() { var newview = wiz.submit(page.wizard_data) if (newview.view) { @@ -81,7 +83,6 @@ ElDialog { }) page.prev.connect(function() { var wdata = wiz.prev() - // console.log('prev view data: ' + JSON.stringify(wdata)) }) pages.pagevalid = page.valid From 5fd6d2af4b902022f545b7324de38eee23acf018 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Mar 2023 14:44:21 +0000 Subject: [PATCH 0520/1143] qml: flip and fix auto_connect in ServerConnectWizard --- electrum/gui/qml/components/wizard/WCServerConfig.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/wizard/WCServerConfig.qml b/electrum/gui/qml/components/wizard/WCServerConfig.qml index d5d1bbef8..3788386bc 100644 --- a/electrum/gui/qml/components/wizard/WCServerConfig.qml +++ b/electrum/gui/qml/components/wizard/WCServerConfig.qml @@ -9,7 +9,7 @@ WizardComponent { last: true function apply() { - wizard_data['autoconnect'] = !sc.auto_connect + wizard_data['autoconnect'] = sc.auto_connect wizard_data['server'] = sc.address } From fc7ff8198ad8341cb510587dc67b44b59f9aa4df Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Mar 2023 22:16:36 +0000 Subject: [PATCH 0521/1143] build: don't assume git repo in make_packages.sh to be able to run it from an unpacked sdist --- contrib/make_packages.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contrib/make_packages.sh b/contrib/make_packages.sh index a7e970ef6..60dc6e661 100755 --- a/contrib/make_packages.sh +++ b/contrib/make_packages.sh @@ -8,6 +8,7 @@ PROJECT_ROOT="$CONTRIB"/.. PACKAGES="$PROJECT_ROOT"/packages/ test -n "$CONTRIB" -a -d "$CONTRIB" || exit +cd "$CONTRIB" if [ -d "$PACKAGES" ]; then rm -r "$PACKAGES" @@ -21,7 +22,7 @@ python3 -m venv "$venv_dir" source "$venv_dir"/bin/activate # installing pinned build-time requirements, such as pip/wheel/setuptools -python -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ +python3 -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ -r "$CONTRIB"/deterministic-build/requirements-build-base.txt # opt out of compiling C extensions @@ -34,7 +35,7 @@ export FROZENLIST_NO_EXTENSIONS=1 # if we end up having to compile something, at least give reproducibility a fighting chance export LC_ALL=C export TZ=UTC -export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)" +export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct 2>/dev/null || printf 1530212462)" export PYTHONHASHSEED="$SOURCE_DATE_EPOCH" export BUILD_DATE="$(LC_ALL=C TZ=UTC date +'%b %e %Y' -d @$SOURCE_DATE_EPOCH)" export BUILD_TIME="$(LC_ALL=C TZ=UTC date +'%H:%M:%S' -d @$SOURCE_DATE_EPOCH)" @@ -49,4 +50,4 @@ export BUILD_TIME="$(LC_ALL=C TZ=UTC date +'%H:%M:%S' -d @$SOURCE_DATE_EPOCH)" # note: --no-build-isolation is needed so that pip uses the locally available setuptools and wheel, # instead of downloading the latest ones python3 -m pip install --no-build-isolation --no-compile --no-dependencies --no-binary :all: \ - -r "$CONTRIB"/deterministic-build/requirements.txt -t "$CONTRIB"/../packages + -r "$CONTRIB"/deterministic-build/requirements.txt -t "$PACKAGES" From f25e3846543ea34c8b87cf7d85d14c66aef882d4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Mar 2023 22:34:07 +0000 Subject: [PATCH 0522/1143] build: fail if not inside git clone related: https://github.com/spesmilo/electrum/issues/8284 --- contrib/android/make_apk.sh | 2 ++ contrib/build-linux/appimage/make_appimage.sh | 6 ++++-- contrib/build-linux/sdist/make_sdist.sh | 2 ++ contrib/build-wine/make_win.sh | 2 ++ contrib/osx/make_osx.sh | 2 ++ 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/contrib/android/make_apk.sh b/contrib/android/make_apk.sh index d9b1bd9ef..5f0d383fb 100755 --- a/contrib/android/make_apk.sh +++ b/contrib/android/make_apk.sh @@ -10,6 +10,8 @@ LOCALE="$PROJECT_ROOT"/electrum/locale/ . "$CONTRIB"/build_tools_util.sh +git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." + # arguments have been checked in build.sh export ELEC_APK_GUI=$1 diff --git a/contrib/build-linux/appimage/make_appimage.sh b/contrib/build-linux/appimage/make_appimage.sh index 6d3dca24d..5db025f8b 100755 --- a/contrib/build-linux/appimage/make_appimage.sh +++ b/contrib/build-linux/appimage/make_appimage.sh @@ -12,6 +12,10 @@ CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" export DLL_TARGET_DIR="$CACHEDIR/dlls" PIP_CACHE_DIR="$CONTRIB_APPIMAGE/.cache/pip_cache" +. "$CONTRIB"/build_tools_util.sh + +git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." + export GCC_STRIP_BINARIES="1" # pinned versions @@ -22,8 +26,6 @@ PKG2APPIMAGE_COMMIT="a9c85b7e61a3a883f4a35c41c5decb5af88b6b5d" VERSION=$(git describe --tags --dirty --always) APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage" -. "$CONTRIB"/build_tools_util.sh - rm -rf "$BUILDDIR" mkdir -p "$APPDIR" "$CACHEDIR" "$PIP_CACHE_DIR" "$DISTDIR" "$DLL_TARGET_DIR" diff --git a/contrib/build-linux/sdist/make_sdist.sh b/contrib/build-linux/sdist/make_sdist.sh index 20d4f5d50..73b658810 100755 --- a/contrib/build-linux/sdist/make_sdist.sh +++ b/contrib/build-linux/sdist/make_sdist.sh @@ -10,6 +10,8 @@ LOCALE="$PROJECT_ROOT/electrum/locale" . "$CONTRIB"/build_tools_util.sh +git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." + # note that at least py3.7 is needed, to have https://bugs.python.org/issue30693 python3 --version || fail "python interpreter not found" diff --git a/contrib/build-wine/make_win.sh b/contrib/build-wine/make_win.sh index 51ddaae54..5d9423c34 100755 --- a/contrib/build-wine/make_win.sh +++ b/contrib/build-wine/make_win.sh @@ -35,6 +35,8 @@ export WINE_PYTHON="wine $WINE_PYHOME/python.exe -OO -B" . "$CONTRIB"/build_tools_util.sh +git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." + info "Clearing $here/build and $here/dist..." rm "$here"/build/* -rf rm "$here"/dist/* -rf diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index af205c663..615e6dbe7 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -25,6 +25,8 @@ mkdir -p "$CACHEDIR" "$DLL_TARGET_DIR" cd "$PROJECT_ROOT" +git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." + which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue" which xcodebuild > /dev/null 2>&1 || fail "Please install xcode command line tools to continue" From ea7dbb19ce96cc4eefe67cdfaf436610c776fb27 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 29 Mar 2023 11:02:15 +0200 Subject: [PATCH 0523/1143] qml: remove network status indicator icon from server line --- electrum/gui/qml/components/NetworkOverview.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index e5c13fa2c..7f2a78612 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -58,7 +58,6 @@ Pane { Label { text: Network.server } - OnchainNetworkStatusIndicator {} } Label { text: qsTr('Local Height:'); From 31bff4d2a8a74a36cb8b97d9f8a32ed34ca550d2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 29 Mar 2023 11:10:00 +0200 Subject: [PATCH 0524/1143] receive_tab: initialize fields --- electrum/gui/qt/receive_tab.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 00842f2e6..76cd3aa6c 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -28,6 +28,14 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): + # strings updated by update_current_request + addr = '' + lnaddr = '' + URI = '' + address_help = '' + URI_help = '' + ln_help = '' + def __init__(self, window: 'ElectrumWindow'): QWidget.__init__(self, window) Logger.__init__(self) From aaad1cf286d019a40c75e8273c777450662fc5bc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 29 Mar 2023 11:15:12 +0200 Subject: [PATCH 0525/1143] qml: wrap long server names (e.g tor onion address) --- electrum/gui/qml/components/NetworkOverview.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 7f2a78612..623752090 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -54,10 +54,10 @@ Pane { text: qsTr('Server:'); color: Material.accentColor } - RowLayout { - Label { - text: Network.server - } + Label { + text: Network.server + wrapMode: Text.WrapAnywhere + Layout.fillWidth: true } Label { text: qsTr('Local Height:'); From 288b7cd3bc635d109554f851c81d485e2fb1d690 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 29 Mar 2023 11:39:40 +0200 Subject: [PATCH 0526/1143] qml: show explanatory text when sharing partially signed tx after creating a multisig transaction --- electrum/gui/qml/components/ExportTxDialog.qml | 7 +++---- electrum/gui/qml/components/TxDetails.qml | 7 +++++-- electrum/gui/qml/components/WalletMainView.qml | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index b69461803..0db305af4 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -51,13 +51,12 @@ ElDialog { } } - Label { + InfoTextArea { + Layout.fillWidth: true + Layout.margins: constants.paddingLarge visible: dialog.text_help text: dialog.text_help - wrapMode: Text.Wrap - Layout.fillWidth: true } - } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 68000b298..a7b803225 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -24,8 +24,11 @@ Pane { app.stack.pop() } - function showExport() { - var dialog = exportTxDialog.createObject(root, { txdetails: txdetails }) + function showExport(helptext) { + var dialog = exportTxDialog.createObject(root, { + txdetails: txdetails, + text_help: helptext + }) dialog.open() } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 38d94360e..c4fd23c55 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -400,7 +400,7 @@ Item { onFinishedSave: { // tx was (partially) signed and saved. Show QR for co-signers or online wallet var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { txid: txid }) - page.showExport() + page.showExport(qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer')) _confirmPaymentDialog.destroy() } } From 5721b7da4b9ea3cbe8d027c1c45a8afbc9f5087a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 29 Mar 2023 12:15:07 +0200 Subject: [PATCH 0527/1143] qml: add userinfo to invoices where amount needs to be filled by user --- electrum/gui/qml/qeinvoice.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 367157d7e..5707e0228 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -348,6 +348,7 @@ def determine_can_pay(self): self.canSave = True if amount.isEmpty: # unspecified amount + self.userinfo = _('Enter the amount you want to send') return if self.invoiceType == QEInvoice.Type.LightningInvoice: @@ -368,8 +369,8 @@ def determine_can_pay(self): self.userinfo = { PR_EXPIRED: _('Invoice is expired'), PR_PAID: _('Invoice is already paid'), - PR_INFLIGHT: _('Invoice is already being paid'), - PR_ROUTING: _('Invoice is already being paid'), + PR_INFLIGHT: _('Payment in progress...'), + PR_ROUTING: _('Payment in progress'), PR_UNKNOWN: _('Invoice has unknown status'), }[self.status] elif self.invoiceType == QEInvoice.Type.OnchainInvoice: From a270bb5c436f70ac297bef98cf3398767c701227 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 29 Mar 2023 16:24:55 +0200 Subject: [PATCH 0528/1143] qml swap dialog: show normal swap amount without mining fee, to be consistent with the qt GUI. --- electrum/gui/qml/components/SwapDialog.qml | 9 ++------- electrum/gui/qml/qeswaphelper.py | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 0a1b25ca5..e5aac63c5 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -47,7 +47,6 @@ ElDialog { source: swaphelper.isReverse ? '../../icons/lightning.png' : '../../icons/bitcoin.png' - visible: swaphelper.valid } } @@ -58,12 +57,10 @@ ElDialog { id: tosend text: Config.formatSats(swaphelper.tosend) font.family: FixedFont - visible: swaphelper.valid } Label { text: Config.baseUnit color: Material.accentColor - visible: swaphelper.valid } } @@ -82,7 +79,6 @@ ElDialog { source: swaphelper.isReverse ? '../../icons/bitcoin.png' : '../../icons/lightning.png' - visible: swaphelper.valid } } @@ -93,12 +89,10 @@ ElDialog { id: toreceive text: Config.formatSats(swaphelper.toreceive) font.family: FixedFont - visible: swaphelper.valid } Label { text: Config.baseUnit color: Material.accentColor - visible: swaphelper.valid } } @@ -140,11 +134,12 @@ ElDialog { Label { text: Config.formatSats(swaphelper.miningfee) font.family: FixedFont + visible: swaphelper.valid } Label { - Layout.fillWidth: true text: Config.baseUnit color: Material.accentColor + visible: swaphelper.valid } } } diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 5730767c1..5e09d4952 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -298,6 +298,7 @@ def swap_slider_moved(self): self.userinfo = _('Adds Lightning sending capacity.') self.isReverse = False self._send_amount = position + self.tosend = QEAmount(amount_sat=self._send_amount) self._receive_amount = swap_manager.get_recv_amount(send_amount=position, is_reverse=False) self.toreceive = QEAmount(amount_sat=self._receive_amount) @@ -324,8 +325,6 @@ def fwd_swap_updatetx(self): self.update_tx(self._send_amount) # add lockup fees, but the swap amount is position pay_amount = self._send_amount + self._tx.get_fee() if self._tx else 0 - self.tosend = QEAmount(amount_sat=pay_amount) - self.miningfee = QEAmount(amount_sat=self._tx.get_fee()) if self._tx else QEAmount() self.check_valid(pay_amount, self._receive_amount) From 7fcf347eb00132d75323935d616c70cdc6a406fd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 29 Mar 2023 16:49:10 +0200 Subject: [PATCH 0529/1143] qml: channel details dialog: clarify whether object is a channel or a channel backup --- electrum/gui/qml/components/ChannelDetails.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 9b52680c8..99d9fdd01 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -38,7 +38,7 @@ Pane { Heading { Layout.columnSpan: 2 - text: qsTr('Channel details') + text: !channeldetails.isBackup ? qsTr('Lightning Channel') : qsTr('Channel Backup') } Label { From 05d6c5155cfd3ab41a120bdfa8b82ae2ba77833a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 29 Mar 2023 18:03:15 +0200 Subject: [PATCH 0530/1143] qml: use invoice.amount directly in amounts display --- electrum/gui/qml/components/InvoiceDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index b72e757a6..688e93720 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -207,7 +207,7 @@ ElDialog { font.pixelSize: constants.fontSizeXLarge font.family: FixedFont font.bold: true - text: Config.formatSats(_invoice_amount, false) + text: Config.formatSats(invoice.amount, false) } Label { @@ -222,7 +222,7 @@ ElDialog { id: fiatValue Layout.alignment: Qt.AlignRight visible: Daemon.fx.enabled && !_invoice_amount.isMax && !_invoice_amount.isEmpty - text: Daemon.fx.fiatValue(_invoice_amount, false) + text: Daemon.fx.fiatValue(invoice.amount, false) font.pixelSize: constants.fontSizeMedium color: constants.mutedForeground } From f4e66810e7c2a38d43f9eab03510b33de5864231 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Mar 2023 15:24:40 +0000 Subject: [PATCH 0531/1143] qml: ElectrumGui to inherit BaseElectrumGui --- electrum/gui/qml/__init__.py | 7 ++++--- electrum/gui/qml/qeapp.py | 4 +++- electrum/gui/qml/qenetwork.py | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index cd0704619..114551751 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -23,6 +23,7 @@ from electrum.plugin import run_hook from electrum.util import profiler from electrum.logging import Logger +from electrum.gui import BaseElectrumGui if TYPE_CHECKING: from electrum.daemon import Daemon @@ -42,10 +43,11 @@ def translate(self, context, source_text, disambiguation, n): return language.gettext(source_text) -class ElectrumGui(Logger): +class ElectrumGui(BaseElectrumGui, Logger): @profiler def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): + BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) set_language(config.get('language', self.get_default_language())) @@ -80,8 +82,7 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" self.gui_thread = threading.current_thread() - self.plugins = plugins - self.app = ElectrumQmlApplication(sys.argv, config, daemon, plugins) + self.app = ElectrumQmlApplication(sys.argv, config=config, daemon=daemon, plugins=plugins) self.translator = ElectrumTranslator() self.app.installTranslator(self.translator) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 1f07c165b..f63270043 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -44,6 +44,8 @@ if TYPE_CHECKING: from electrum.simple_config import SimpleConfig from electrum.wallet import Abstract_Wallet + from electrum.daemon import Daemon + from electrum.plugin import Plugins notification = None @@ -290,7 +292,7 @@ class ElectrumQmlApplication(QGuiApplication): _valid = True - def __init__(self, args, config, daemon, plugins): + def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): super().__init__(args) self.logger = get_logger(__name__) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 234451911..0ca856e83 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.logging import get_logger @@ -8,6 +10,11 @@ from .util import QtEventListener, event_listener from .qeserverlistmodel import QEServerListModel +if TYPE_CHECKING: + from .qeconfig import QEConfig + from electrum.network import Network + + class QENetwork(QObject, QtEventListener): _logger = get_logger(__name__) @@ -38,7 +45,7 @@ class QENetwork(QObject, QtEventListener): _gossipDbChannels = 0 _gossipDbPolicies = 0 - def __init__(self, network, qeconfig, parent=None): + def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): super().__init__(parent) self.network = network self._qeconfig = qeconfig From 512b63c4248b465372dc10ec359ce3148be9ee9a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Mar 2023 15:45:15 +0000 Subject: [PATCH 0532/1143] exchange_rate: FxThread does not need network --- electrum/commands.py | 2 +- electrum/daemon.py | 2 +- electrum/exchange_rate.py | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index fbae8858e..a475fdc36 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -771,7 +771,7 @@ async def onchain_history(self, year=None, show_addresses=False, show_fiat=False kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) if show_fiat: from .exchange_rate import FxThread - fx = FxThread(self.config, None) + fx = FxThread(config=self.config) kwargs['fx'] = fx return json_normalize(wallet.get_detailed_history(**kwargs)) diff --git a/electrum/daemon.py b/electrum/daemon.py index f6efa490e..6c87fee07 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -395,7 +395,7 @@ def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): self.network = None if not config.get('offline'): self.network = Network(config, daemon=self) - self.fx = FxThread(config, self.network) + self.fx = FxThread(config=config) self.gui_object = None # path -> wallet; make sure path is standardized. self._wallets = {} # type: Dict[str, Abstract_Wallet] diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 5bb8bc428..fa3d0de19 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -519,10 +519,9 @@ def get_exchanges_by_ccy(history=True): class FxThread(ThreadJob, EventListener): - def __init__(self, config: SimpleConfig, network: Optional[Network]): + def __init__(self, *, config: SimpleConfig): ThreadJob.__init__(self) self.config = config - self.network = network self.register_callbacks() self.ccy = self.get_currency() self.history_used_spot = False @@ -610,8 +609,8 @@ def set_currency(self, ccy: str): self.on_quotes() def trigger_update(self): - if self.network: - self.network.asyncio_loop.call_soon_threadsafe(self._trigger.set) + loop = util.get_asyncio_loop() + loop.call_soon_threadsafe(self._trigger.set) def set_exchange(self, name): class_ = globals().get(name) or globals().get(DEFAULT_EXCHANGE) From 37d0a67e5b7aa2185141d4411b4c09b40fecd37e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 15:46:03 +0000 Subject: [PATCH 0533/1143] qml: proxy config: fix socks4/socks5 dropdown --- electrum/gui/qml/components/ProxyConfigDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ProxyConfigDialog.qml b/electrum/gui/qml/components/ProxyConfigDialog.qml index 503bf57ac..73a85416c 100644 --- a/electrum/gui/qml/components/ProxyConfigDialog.qml +++ b/electrum/gui/qml/components/ProxyConfigDialog.qml @@ -57,7 +57,7 @@ ElDialog { proxyconfig.proxy_port = p['port'] proxyconfig.username = p['user'] proxyconfig.password = p['password'] - proxyconfig.proxy_type = proxyconfig.proxy_types.map(function(x) { + proxyconfig.proxy_type = proxyconfig.proxy_type_map.map(function(x) { return x.value }).indexOf(p['mode']) } else { From faa53c71da410a67b48096a0d374eb4fc3e1a943 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 16:15:35 +0000 Subject: [PATCH 0534/1143] qml: (trivial) fix warning in NetworkOverview.qml 10.95 | W | gui.qml.qeapp | file:///home/user/wspace/electrum/electrum/gui/qml/components/NetworkOverview.qml:220:25: Unable to assign [undefined] to QString --- electrum/gui/qml/components/NetworkOverview.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 623752090..30327c5e7 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -216,7 +216,7 @@ Pane { } Label { visible: 'mode' in Network.proxy - text: Network.isProxyTor ? 'TOR' : Network.proxy['mode'] + text: Network.isProxyTor ? 'TOR' : (Network.proxy['mode'] || '') } } From 57786049e9828bee3788146f142bbb39480088b8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 16:37:16 +0000 Subject: [PATCH 0535/1143] qml: network dialog to update "status" more often --- electrum/gui/qml/qenetwork.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 0ca856e83..f059c4dfc 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -58,6 +58,7 @@ def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): @event_listener def on_event_network_updated(self, *args): self.networkUpdated.emit() + self._update_network_status() @event_listener def on_event_blockchain_updated(self): @@ -77,12 +78,15 @@ def on_event_proxy_set(self, *args): self.proxySet.emit() self.proxyTorChanged.emit() - @event_listener - def on_event_status(self, *args): + def _update_network_status(self): network_status = self.network.get_status() if self._network_status != network_status: self._network_status = network_status self.statusChanged.emit() + + @event_listener + def on_event_status(self, *args): + self._update_network_status() server_status = self.network.connection_status self._logger.debug('server_status updated: %s' % server_status) if self._server_status != server_status: From 7efd6fe1e2db66de93292ead2259138bde1b4ade Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 29 Mar 2023 18:25:29 +0200 Subject: [PATCH 0536/1143] qml: don't show ln payment dialog, update info text instead --- electrum/gui/qml/components/InvoiceDialog.qml | 25 +++++++++++++++++-- .../gui/qml/components/WalletMainView.qml | 5 ---- electrum/gui/qml/qeinvoice.py | 4 ++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 688e93720..20c944905 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -47,10 +47,11 @@ ElDialog { columns: 2 InfoTextArea { + id: helpText Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: invoice.userinfo + visible: text text: invoice.userinfo iconStyle: InfoTextArea.IconStyle.Warn } @@ -457,14 +458,34 @@ ElDialog { // save invoice if new or modified invoice.save_invoice() } - dialog.close() doPay() // only signal here + helpText.text = qsTr('Payment in progress...') } } } } + Connections { + target: Daemon.currentWallet + function onPaymentSucceeded(key) { + if (key != invoice.key) { + console.log('wrong invoice ' + key + ' != ' + invoice.key) + return + } + console.log('payment succeeded!') + helpText.text = qsTr('Paid!') + } + function onPaymentFailed(key, reason) { + if (key != invoice.key) { + console.log('wrong invoice ' + key + ' != ' + invoice.key) + return + } + console.log('payment failed: ' + reason) + helpText.text = qsTr('Payment failed: ' + reason) + } + } + Component.onCompleted: { if (invoice_key != '') { invoice.initFromKey(invoice_key) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index c4fd23c55..91f008575 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -312,13 +312,8 @@ Item { console.log('No invoice key, aborting') return } - var dialog = lightningPaymentProgressDialog.createObject(mainView, { - invoice_key: invoice.key - }) - dialog.open() Daemon.currentWallet.pay_lightning_invoice(invoice.key) } - close() } onClosed: destroy() diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 5707e0228..f887b7d0f 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -347,8 +347,10 @@ def determine_can_pay(self): self.canSave = True - if amount.isEmpty: # unspecified amount + if self.amount.isEmpty: self.userinfo = _('Enter the amount you want to send') + + if amount.isEmpty and self.status == PR_UNPAID: # unspecified amount return if self.invoiceType == QEInvoice.Type.LightningInvoice: From cc60ab0b206ca6c7ce6e66397ee62ebd0c26296b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 29 Mar 2023 19:06:35 +0200 Subject: [PATCH 0537/1143] qml: move payment progress info text updates fully into qeinvoice, qeinvoice now updates itself directly from backend wallet callbacks --- electrum/gui/qml/components/InvoiceDialog.qml | 25 ++-------------- electrum/gui/qml/qeinvoice.py | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 20c944905..adff3ca83 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -53,7 +53,9 @@ ElDialog { Layout.bottomMargin: constants.paddingLarge visible: text text: invoice.userinfo - iconStyle: InfoTextArea.IconStyle.Warn + iconStyle: invoice.status == Invoice.Failed || invoice.status == Invoice.Expired + ? InfoTextArea.IconStyle.Warn + : InfoTextArea.IconStyle.Info } Label { @@ -459,33 +461,12 @@ ElDialog { invoice.save_invoice() } doPay() // only signal here - helpText.text = qsTr('Payment in progress...') } } } } - Connections { - target: Daemon.currentWallet - function onPaymentSucceeded(key) { - if (key != invoice.key) { - console.log('wrong invoice ' + key + ' != ' + invoice.key) - return - } - console.log('payment succeeded!') - helpText.text = qsTr('Paid!') - } - function onPaymentFailed(key, reason) { - if (key != invoice.key) { - console.log('wrong invoice ' + key + ' != ' + invoice.key) - return - } - console.log('payment failed: ' + reason) - helpText.text = qsTr('Payment failed: ' + reason) - } - } - Component.onCompleted: { if (invoice_key != '') { invoice.initFromKey(invoice_key) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index f887b7d0f..d850fd41c 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -23,7 +23,7 @@ from .qetypes import QEAmount from .qewallet import QEWallet -from .util import status_update_timer_interval +from .util import status_update_timer_interval, QtEventListener, event_listener class QEInvoice(QObject): @@ -120,7 +120,7 @@ def get_max_spendable_onchain(self): def get_max_spendable_lightning(self): return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0 -class QEInvoiceParser(QEInvoice): +class QEInvoiceParser(QEInvoice, QtEventListener): _logger = get_logger(__name__) invoiceChanged = pyqtSignal() @@ -160,6 +160,31 @@ def __init__(self, parent=None): self.clear() + self.register_callbacks() + self.destroyed.connect(lambda: self.on_destroy()) + + def on_destroy(self): + self.unregister_callbacks() + + @event_listener + def on_event_payment_succeeded(self, wallet, key): + if wallet == self._wallet.wallet and key == self.key: + self.statusChanged.emit() + self.userinfo = _('Paid!') + + @event_listener + def on_event_payment_failed(self, wallet, key, reason): + if wallet == self._wallet.wallet and key == self.key: + self.statusChanged.emit() + self.userinfo = _('Payment failed: ') + reason + + @event_listener + def on_event_invoice_status(self, wallet, key, status): + if wallet == self._wallet.wallet and key == self.key: + self.statusChanged.emit() + if status in [PR_INFLIGHT, PR_ROUTING]: + self.userinfo = _('In progress...') + @pyqtProperty(int, notify=invoiceChanged) def invoiceType(self): return self._invoiceType From d189fdce69e445dd26585fbe1613ac2edb10b3bc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 21:23:00 +0000 Subject: [PATCH 0538/1143] qml: crash reporter: fix "show never" option 102.82 | E | gui.qml.qeapp.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qml/qeapp.py", line 271, in showNever self.config.set_key(BaseCrashReporter.config_key, False) AttributeError: 'QEAppController' object has no attribute 'config' --- electrum/gui/qml/qeapp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index f63270043..3dbbc36ea 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -6,7 +6,7 @@ import html import threading import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Set from PyQt5.QtCore import (pyqtSlot, pyqtSignal, pyqtProperty, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer, QSortFilterProxyModel) @@ -58,12 +58,13 @@ class QEAppController(BaseCrashReporter, QObject): sendingBugreportSuccess = pyqtSignal(str) sendingBugreportFailure = pyqtSignal(str) - def __init__(self, qedaemon, plugins): + def __init__(self, qedaemon: 'QEDaemon', plugins: 'Plugins'): BaseCrashReporter.__init__(self, None, None, None) QObject.__init__(self) self._qedaemon = qedaemon self._plugins = plugins + self.config = qedaemon.daemon.config self._crash_user_text = '' self._app_started = False From d46d23b10371feaf3a1c2f696bd546affde60b7d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 21:48:46 +0000 Subject: [PATCH 0539/1143] network: add method init_parameters_from_config --- electrum/network.py | 54 +++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index e5242bc89..9a6fb84d5 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -294,18 +294,7 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): self._allowed_protocols = {PREFERRED_NETWORK_PROTOCOL} - # Server for addresses and transactions - self.default_server = self.config.get('server', None) - # Sanitize default server - if self.default_server: - try: - self.default_server = ServerAddr.from_str(self.default_server) - except: - self.logger.warning('failed to parse server-string; falling back to localhost:1:s.') - self.default_server = ServerAddr.from_str("localhost:1:s") - else: - self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols) - assert isinstance(self.default_server, ServerAddr), f"invalid type for default_server: {self.default_server!r}" + self._init_parameters_from_config() self.taskgroup = None @@ -337,11 +326,6 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): self.interfaces = {} # these are the ifaces in "initialised and usable" state self._closing_ifaces = set() - self.auto_connect = self.config.get('auto_connect', True) - self.proxy = None - self.tor_proxy = False - self._maybe_set_oneserver() - # Dump network messages (all interfaces). Set at runtime from the console. self.debug = False @@ -509,6 +493,12 @@ def get_parameters(self) -> NetworkParameters: auto_connect=self.auto_connect, oneserver=self.oneserver) + def _init_parameters_from_config(self) -> None: + self.auto_connect = self.config.get('auto_connect', True) + self._set_default_server() + self._set_proxy(deserialize_proxy(self.config.get('proxy'))) + self._maybe_set_oneserver() + def get_donation_address(self): if self.is_connected(): return self.donation_address @@ -603,6 +593,20 @@ def _get_next_server_to_try(self) -> Optional[ServerAddr]: return server return None + def _set_default_server(self) -> None: + # Server for addresses and transactions + server = self.config.get('server', None) + # Sanitize default server + if server: + try: + self.default_server = ServerAddr.from_str(server) + except: + self.logger.warning(f'failed to parse server-string ({server!r}); falling back to localhost:1:s.') + self.default_server = ServerAddr.from_str("localhost:1:s") + else: + self.default_server = pick_random_server(allowed_protocols=self._allowed_protocols) + assert isinstance(self.default_server, ServerAddr), f"invalid type for default_server: {self.default_server!r}" + def _set_proxy(self, proxy: Optional[dict]): self.proxy = proxy dns_hacks.configure_dns_depending_on_proxy(bool(proxy)) @@ -639,14 +643,17 @@ async def set_parameters(self, net_params: NetworkParameters): or self.config.get('oneserver') != net_params.oneserver: return + proxy_changed = self.proxy != proxy + oneserver_changed = self.oneserver != net_params.oneserver + default_server_changed = self.default_server != server + self._init_parameters_from_config() + async with self.restart_lock: - self.auto_connect = net_params.auto_connect - if self.proxy != proxy or self.oneserver != net_params.oneserver: - # Restart the network defaulting to the given server + if proxy_changed or oneserver_changed: + # Restart the network await self.stop(full_shutdown=False) - self.default_server = server await self._start() - elif self.default_server != server: + elif default_server_changed: await self.switch_to_interface(server) else: await self.switch_lagging_interface() @@ -1236,8 +1243,7 @@ async def _start(self): assert not self._closing_ifaces self.logger.info('starting network') self._clear_addr_retry_times() - self._set_proxy(deserialize_proxy(self.config.get('proxy'))) - self._maybe_set_oneserver() + self._init_parameters_from_config() await self.taskgroup.spawn(self._run_new_interface(self.default_server)) async def main(): From 9ef6d6a56fdeae0bfa9a9e08ba47142a48925797 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 21:56:33 +0000 Subject: [PATCH 0540/1143] qml: rm QEConfig.serverString. Network.server is sufficient If there is no network object, it's ok not to be able to customise it. --- electrum/gui/qml/components/ServerConfigDialog.qml | 1 - electrum/gui/qml/components/ServerConnectWizard.qml | 1 - electrum/gui/qml/components/controls/ServerConfig.qml | 2 +- electrum/gui/qml/qeconfig.py | 10 ---------- 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/electrum/gui/qml/components/ServerConfigDialog.qml b/electrum/gui/qml/components/ServerConfigDialog.qml index b9664220f..0fa56d4d1 100644 --- a/electrum/gui/qml/components/ServerConfigDialog.qml +++ b/electrum/gui/qml/components/ServerConfigDialog.qml @@ -42,7 +42,6 @@ ElDialog { icon.source: '../../icons/confirmed.png' onClicked: { Config.autoConnect = serverconfig.auto_connect - Config.serverString = serverconfig.address Network.server = serverconfig.address rootItem.close() } diff --git a/electrum/gui/qml/components/ServerConnectWizard.qml b/electrum/gui/qml/components/ServerConnectWizard.qml index 3f08c7642..cfffa0f52 100644 --- a/electrum/gui/qml/components/ServerConnectWizard.qml +++ b/electrum/gui/qml/components/ServerConnectWizard.qml @@ -24,7 +24,6 @@ Wizard { Config.autoConnect = wizard_data['autoconnect'] if (!wizard_data['autoconnect']) { Network.server = wizard_data['server'] - Config.serverString = wizard_data['server'] } } diff --git a/electrum/gui/qml/components/controls/ServerConfig.qml b/electrum/gui/qml/components/controls/ServerConfig.qml index 6538be1a9..5991a9fdf 100644 --- a/electrum/gui/qml/components/controls/ServerConfig.qml +++ b/electrum/gui/qml/components/controls/ServerConfig.qml @@ -93,7 +93,7 @@ Item { Component.onCompleted: { root.auto_connect = Config.autoConnectDefined ? Config.autoConnect : false - root.address = Config.serverString ? Config.serverString : Network.server + root.address = Network.server // TODO: initial setup should not connect already, is Network.server defined? } } diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index af479941d..007a0e3d0 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -57,16 +57,6 @@ def autoConnect(self, auto_connect): def autoConnectDefined(self): return self.config.get('auto_connect') is not None - serverStringChanged = pyqtSignal() - @pyqtProperty('QString', notify=serverStringChanged) - def serverString(self): - return self.config.get('server') - - @serverString.setter - def serverString(self, server): - self.config.set_key('server', server, True) - self.serverStringChanged.emit() - manualServerChanged = pyqtSignal() @pyqtProperty(bool, notify=manualServerChanged) def manualServer(self): From 15306689607a6aca298d9b4343377469e226a9f3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 22:09:46 +0000 Subject: [PATCH 0541/1143] qt/qml: delay starting network until after first-start-network-setup The qt, qml, and kivy GUIs have a first-start network-setup screen that allows the user customising the network settings before creating a wallet. Previously the daemon used to create the network and start it, before this screen, before the GUI even starts. If the user changed network settings, those would be set on the already running network, potentially including restarting the network. Now it becomes the responsibility of the GUI to start the network, allowing this first-start customisation to take place before starting the network at all. The qt and the qml GUIs are adapted to make use of this. Kivy, and the other prototype GUIs are not adapted and just start the network right away, as before. --- electrum/daemon.py | 47 +++++++++++++++++---------- electrum/gui/kivy/__init__.py | 1 + electrum/gui/qml/components/main.qml | 2 ++ electrum/gui/qml/qedaemon.py | 7 +++- electrum/gui/qml/qeserverlistmodel.py | 2 +- electrum/gui/qt/__init__.py | 7 ++-- electrum/gui/stdio.py | 4 ++- electrum/gui/text.py | 1 + electrum/lnwatcher.py | 2 +- electrum/network.py | 7 ++++ run_electrum | 2 +- 11 files changed, 57 insertions(+), 25 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 6c87fee07..2a10b106a 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -53,7 +53,7 @@ from .exchange_rate import FxThread from .logging import get_logger, Logger from . import GuiImportError -from .plugin import run_hook +from .plugin import run_hook, Plugins if TYPE_CHECKING: from electrum import gui @@ -376,11 +376,19 @@ async def add_sweep_tx(self, *args): class Daemon(Logger): - network: Optional[Network] - gui_object: Optional['gui.BaseElectrumGui'] + network: Optional[Network] = None + gui_object: Optional['gui.BaseElectrumGui'] = None + watchtower: Optional['WatchTowerServer'] = None @profiler - def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): + def __init__( + self, + config: SimpleConfig, + fd=None, + *, + listen_jsonrpc: bool = True, + start_network: bool = True, # setting to False allows customising network settings before starting it + ): Logger.__init__(self) self.config = config self.listen_jsonrpc = listen_jsonrpc @@ -392,11 +400,9 @@ def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): self.logger.warning("Ignoring parameter 'wallet_path' for daemon. " "Use the load_wallet command instead.") self.asyncio_loop = util.get_asyncio_loop() - self.network = None if not config.get('offline'): self.network = Network(config, daemon=self) self.fx = FxThread(config=config) - self.gui_object = None # path -> wallet; make sure path is standardized. self._wallets = {} # type: Dict[str, Abstract_Wallet] self._wallet_lock = threading.RLock() @@ -406,23 +412,14 @@ def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True): if listen_jsonrpc: self.commands_server = CommandsServer(self, fd) daemon_jobs.append(self.commands_server.run()) - # server-side watchtower - self.watchtower = None - watchtower_address = self.config.get_netaddress('watchtower_address') - if not config.get('offline') and watchtower_address: - self.watchtower = WatchTowerServer(self.network, watchtower_address) - daemon_jobs.append(self.watchtower.run) - if self.network: - self.network.start(jobs=[self.fx.run]) - # prepare lightning functionality, also load channel db early - if self.config.get('use_gossip', False): - self.network.start_gossip() self._stop_entered = False self._stopping_soon_or_errored = threading.Event() self._stopped_event = threading.Event() self.taskgroup = OldTaskGroup() asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop) + if start_network and self.network: + self.start_network() @log_exceptions async def _run(self, jobs: Iterable = None): @@ -442,6 +439,20 @@ async def _run(self, jobs: Iterable = None): # not see the exception (especially if the GUI did not start yet). self._stopping_soon_or_errored.set() + def start_network(self): + self.logger.info(f"starting network.") + assert not self.config.get('offline') + assert self.network + # server-side watchtower + if watchtower_address := self.config.get_netaddress('watchtower_address'): + self.watchtower = WatchTowerServer(self.network, watchtower_address) + asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.watchtower.run), self.asyncio_loop) + + self.network.start(jobs=[self.fx.run]) + # prepare lightning functionality, also load channel db early + if self.config.get('use_gossip', False): + self.network.start_gossip() + def with_wallet_lock(func): def func_wrapper(self: 'Daemon', *args, **kwargs): with self._wallet_lock: @@ -566,7 +577,7 @@ async def stop(self): self.logger.info("stopped") self._stopped_event.set() - def run_gui(self, config, plugins): + def run_gui(self, config: 'SimpleConfig', plugins: 'Plugins'): threading.current_thread().name = 'GUI' gui_name = config.get('gui', 'qt') if gui_name in ['lite', 'classic']: diff --git a/electrum/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py index b7c9249a6..da2fd2b76 100644 --- a/electrum/gui/kivy/__init__.py +++ b/electrum/gui/kivy/__init__.py @@ -64,6 +64,7 @@ def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugin self.network = daemon.network def main(self): + self.daemon.start_network() from .main_window import ElectrumWindow w = ElectrumWindow( config=self.config, diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 461491b35..7baaf45ee 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -409,6 +409,7 @@ ApplicationWindow Qt.callLater(Qt.quit) }) dialog.accepted.connect(function() { + Daemon.startNetwork() var newww = app.newWalletWizard.createObject(app) newww.walletCreated.connect(function() { Daemon.availableWallets.reload() @@ -419,6 +420,7 @@ ApplicationWindow }) dialog.open() } else { + Daemon.startNetwork() if (Daemon.availableWallets.rowCount() > 0) { Daemon.load_wallet() } else { diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 569b24ad8..de959e931 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -10,6 +10,7 @@ from electrum.wallet import Abstract_Wallet from electrum.plugin import run_hook from electrum.lnchannel import ChannelState +from electrum.daemon import Daemon from .auth import AuthMixin, auth_protect from .qefx import QEFX @@ -135,7 +136,7 @@ class QEDaemon(AuthMixin, QObject): walletOpenError = pyqtSignal([str], arguments=["error"]) walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message']) - def __init__(self, daemon, parent=None): + def __init__(self, daemon: 'Daemon', parent=None): super().__init__(parent) self.daemon = daemon self.qefx = QEFX(daemon.fx, daemon.config) @@ -336,3 +337,7 @@ def serverConnectWizard(self): self._server_connect_wizard = QEServerConnectWizard(self) return self._server_connect_wizard + + @pyqtSlot() + def startNetwork(self): + self.daemon.start_network() diff --git a/electrum/gui/qml/qeserverlistmodel.py b/electrum/gui/qml/qeserverlistmodel.py index d839b1b79..97a635b85 100644 --- a/electrum/gui/qml/qeserverlistmodel.py +++ b/electrum/gui/qml/qeserverlistmodel.py @@ -108,7 +108,7 @@ def init_model(self): server['address'] = i.server.to_friendly_name() server['height'] = i.tip - self._logger.debug(f'adding server: {repr(server)}') + #self._logger.debug(f'adding server: {repr(server)}') servers.append(server) # disconnected servers diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 73ba81e53..890475fe0 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -428,12 +428,15 @@ def close_window(self, window: ElectrumWindow): self.daemon.stop_wallet(window.wallet.storage.path) def init_network(self): - # Show network dialog if config does not exist + """Start the network, including showing a first-start network dialog if config does not exist.""" if self.daemon.network: + # first-start network-setup if self.config.get('auto_connect') is None: wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self) wizard.init_network(self.daemon.network) wizard.terminate() + # start network + self.daemon.start_network() def main(self): # setup Ctrl-C handling and tear-down code first, so that user can easily exit whenever @@ -443,7 +446,7 @@ def main(self): signal.signal(signal.SIGINT, lambda *args: self.app.quit()) # hook for crash reporter Exception_Hook.maybe_setup(config=self.config) - # first-start network-setup + # start network, and maybe show first-start network-setup try: self.init_network() except UserCancelled: diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py index ef77a8bf5..f8d05af45 100644 --- a/electrum/gui/stdio.py +++ b/electrum/gui/stdio.py @@ -177,7 +177,9 @@ def print_list(self, lst, firstline): def main(self): - while self.done == 0: self.main_command() + self.daemon.start_network() + while self.done == 0: + self.main_command() def do_send(self): if not is_address(self.str_recipient): diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 4f08d18fc..22f6361e1 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -517,6 +517,7 @@ def run_banner_tab(self, c): pass def main(self): + self.daemon.start_network() tty.setraw(sys.stdin) try: while self.tab != -1: diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 109490df2..51b741b42 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -141,7 +141,7 @@ class LNWatcher(Logger, EventListener): LOGGING_SHORTCUT = 'W' - def __init__(self, adb, network: 'Network'): + def __init__(self, adb: 'AddressSynchronizer', network: 'Network'): Logger.__init__(self) self.adb = adb diff --git a/electrum/network.py b/electrum/network.py index 9a6fb84d5..71467d048 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -331,6 +331,7 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): self._set_status('disconnected') self._has_ever_managed_to_connect_to_server = False + self._was_started = False # lightning network if self.config.get('run_watchtower', False): @@ -647,6 +648,8 @@ async def set_parameters(self, net_params: NetworkParameters): oneserver_changed = self.oneserver != net_params.oneserver default_server_changed = self.default_server != server self._init_parameters_from_config() + if not self._was_started: + return async with self.restart_lock: if proxy_changed or oneserver_changed: @@ -1268,11 +1271,15 @@ def start(self, jobs: Iterable = None): Note: the jobs will *restart* every time the network restarts, e.g. on proxy setting changes. """ + self._was_started = True self._jobs = jobs or [] asyncio.run_coroutine_threadsafe(self._start(), self.asyncio_loop) @log_exceptions async def stop(self, *, full_shutdown: bool = True): + if not self._was_started: + self.logger.info("not stopping network as it was never started") + return self.logger.info("stopping network") # timeout: if full_shutdown, it is up to the caller to time us out, # otherwise if e.g. restarting due to proxy changes, we time out fast diff --git a/run_electrum b/run_electrum index fb27bf45a..c587d7bed 100755 --- a/run_electrum +++ b/run_electrum @@ -434,7 +434,7 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): fd = daemon.get_file_descriptor(config) if fd is not None: plugins = init_plugins(config, config.get('gui', 'qt')) - d = daemon.Daemon(config, fd) + d = daemon.Daemon(config, fd, start_network=False) try: d.run_gui(config, plugins) except BaseException as e: From 44f91ab88f2a58c3790cd7275f06a52b9c2c3625 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Mar 2023 22:32:44 +0000 Subject: [PATCH 0542/1143] qml: add TODO about --offline missing --- electrum/gui/qml/qenetwork.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index f059c4dfc..33ed405bb 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -47,6 +47,7 @@ class QENetwork(QObject, QtEventListener): def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): super().__init__(parent) + assert network, "--offline is not yet implemented for this GUI" # TODO self.network = network self._qeconfig = qeconfig self._serverListModel = None From 101958e02210e94c53a67f6ce4a12e5fb586b76e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 00:10:26 +0000 Subject: [PATCH 0543/1143] qt network_dialog: fix right-click "Use as server" for raw IPv6 servers `server.net_addr_str()` cuts off the trailing protocol marker, while `str(server)` has it. `parent.set_server` then called `ServerAddr.from_str_with_inference` trying to guess the just cut off protocol, but fails if given an IPv6 address. --- electrum/gui/qt/network_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py index ddc78adaa..3ab73b9bb 100644 --- a/electrum/gui/qt/network_dialog.py +++ b/electrum/gui/qt/network_dialog.py @@ -116,7 +116,7 @@ def create_menu(self, position): elif item_type == self.ItemType.DISCONNECTED_SERVER: server = item.data(0, self.SERVER_ADDR_ROLE) # type: ServerAddr def func(): - self.parent.server_e.setText(server.net_addr_str()) + self.parent.server_e.setText(str(server)) self.parent.set_server() menu.addAction(_("Use as server"), func) elif item_type == self.ItemType.CHAIN: From 04df286519047771148d82c116e12db066fc59a0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 00:27:52 +0000 Subject: [PATCH 0544/1143] interface: fix ServerAddr.from_str_with_inference() for raw IPv6 addr and add tests --- electrum/interface.py | 14 ++++++++-- electrum/tests/test_interface.py | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 electrum/tests/test_interface.py diff --git a/electrum/interface.py b/electrum/interface.py index 9edbd41dc..4e68141c1 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -292,6 +292,7 @@ def __init__(self, host: str, port: Union[int, str], *, protocol: str = None): @classmethod def from_str(cls, s: str) -> 'ServerAddr': + """Constructs a ServerAddr or raises ValueError.""" # host might be IPv6 address, hence do rsplit: host, port, protocol = str(s).rsplit(':', 2) return ServerAddr(host=host, port=port, protocol=protocol) @@ -299,20 +300,29 @@ def from_str(cls, s: str) -> 'ServerAddr': @classmethod def from_str_with_inference(cls, s: str) -> Optional['ServerAddr']: """Construct ServerAddr from str, guessing missing details. + Does not raise - just returns None if guessing failed. Ongoing compatibility not guaranteed. """ if not s: return None + host = "" + if s[0] == "[" and "]" in s: # IPv6 address + host_end = s.index("]") + host = s[1:host_end] + s = s[host_end+1:] items = str(s).rsplit(':', 2) if len(items) < 2: return None # although maybe we could guess the port too? - host = items[0] + host = host or items[0] port = items[1] if len(items) >= 3: protocol = items[2] else: protocol = PREFERRED_NETWORK_PROTOCOL - return ServerAddr(host=host, port=port, protocol=protocol) + try: + return ServerAddr(host=host, port=port, protocol=protocol) + except ValueError: + return None def to_friendly_name(self) -> str: # note: this method is closely linked to from_str_with_inference diff --git a/electrum/tests/test_interface.py b/electrum/tests/test_interface.py new file mode 100644 index 000000000..84e223e09 --- /dev/null +++ b/electrum/tests/test_interface.py @@ -0,0 +1,48 @@ +from electrum.interface import ServerAddr + +from . import ElectrumTestCase + + +class TestServerAddr(ElectrumTestCase): + + def test_from_str(self): + self.assertEqual(ServerAddr(host="104.198.149.61", port=80, protocol="t"), + ServerAddr.from_str("104.198.149.61:80:t")) + self.assertEqual(ServerAddr(host="ecdsa.net", port=110, protocol="s"), + ServerAddr.from_str("ecdsa.net:110:s")) + self.assertEqual(ServerAddr(host="2400:6180:0:d1::86b:e001", port=50002, protocol="s"), + ServerAddr.from_str("[2400:6180:0:d1::86b:e001]:50002:s")) + self.assertEqual(ServerAddr(host="localhost", port=8080, protocol="s"), + ServerAddr.from_str("localhost:8080:s")) + + def test_from_str_with_inference(self): + self.assertEqual(None, ServerAddr.from_str_with_inference("104.198.149.61")) + self.assertEqual(None, ServerAddr.from_str_with_inference("ecdsa.net")) + self.assertEqual(None, ServerAddr.from_str_with_inference("2400:6180:0:d1::86b:e001")) + self.assertEqual(None, ServerAddr.from_str_with_inference("[2400:6180:0:d1::86b:e001]")) + + self.assertEqual(ServerAddr(host="104.198.149.61", port=80, protocol="s"), + ServerAddr.from_str_with_inference("104.198.149.61:80")) + self.assertEqual(ServerAddr(host="ecdsa.net", port=110, protocol="s"), + ServerAddr.from_str_with_inference("ecdsa.net:110")) + self.assertEqual(ServerAddr(host="2400:6180:0:d1::86b:e001", port=50002, protocol="s"), + ServerAddr.from_str_with_inference("[2400:6180:0:d1::86b:e001]:50002")) + + self.assertEqual(ServerAddr(host="104.198.149.61", port=80, protocol="t"), + ServerAddr.from_str_with_inference("104.198.149.61:80:t")) + self.assertEqual(ServerAddr(host="ecdsa.net", port=110, protocol="s"), + ServerAddr.from_str_with_inference("ecdsa.net:110:s")) + self.assertEqual(ServerAddr(host="2400:6180:0:d1::86b:e001", port=50002, protocol="s"), + ServerAddr.from_str_with_inference("[2400:6180:0:d1::86b:e001]:50002:s")) + + def test_to_friendly_name(self): + self.assertEqual("104.198.149.61:80:t", + ServerAddr(host="104.198.149.61", port=80, protocol="t").to_friendly_name()) + self.assertEqual("ecdsa.net:110", + ServerAddr(host="ecdsa.net", port=110, protocol="s").to_friendly_name()) + self.assertEqual("ecdsa.net:50001:t", + ServerAddr(host="ecdsa.net", port=50001, protocol="t").to_friendly_name()) + self.assertEqual("[2400:6180:0:d1::86b:e001]:50002", + ServerAddr(host="2400:6180:0:d1::86b:e001", port=50002, protocol="s").to_friendly_name()) + self.assertEqual("[2400:6180:0:d1::86b:e001]:50001:t", + ServerAddr(host="2400:6180:0:d1::86b:e001", port=50001, protocol="t").to_friendly_name()) From 3149ccf729f01de928c58b74a3e70ef610e8bed8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 00:40:55 +0000 Subject: [PATCH 0545/1143] qml: update server in network dialog on more events "defaultServerChanged" was not the right event to listen to. It is only sent *after* the interface is ready. "network_updated" is a bit overkill as it is triggered every time any of the interfaces goes down or a new one is created, still, better to trigger a few more times than to be stale. In particular, if there is no internet connection, the server string is now updated as expected, instead of showing stale values and ignoring trying to change servers. Also, a further state that did not exist before: just like it worked in the kivy GUI, if the main server was changed but it is not yet connected, instead of showing the old server still, we now show f"{new_server} (connecting...)". --- .../gui/qml/components/NetworkOverview.qml | 2 +- electrum/gui/qml/qenetwork.py | 33 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 30327c5e7..f58978d59 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -55,7 +55,7 @@ Pane { color: Material.accentColor } Label { - text: Network.server + text: Network.serverWithStatus wrapMode: Text.WrapAnywhere Layout.fillWidth: true } diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 33ed405bb..59d327c8b 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -21,7 +21,6 @@ class QENetwork(QObject, QtEventListener): networkUpdated = pyqtSignal() blockchainUpdated = pyqtSignal() heightChanged = pyqtSignal([int], arguments=['height']) - defaultServerChanged = pyqtSignal() proxySet = pyqtSignal() proxyChanged = pyqtSignal() statusChanged = pyqtSignal() @@ -34,6 +33,7 @@ class QENetwork(QObject, QtEventListener): dataChanged = pyqtSignal() _height = 0 + _server = "" _server_status = "" _network_status = "" _chaintips = 1 @@ -59,7 +59,7 @@ def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): @event_listener def on_event_network_updated(self, *args): self.networkUpdated.emit() - self._update_network_status() + self._update_status() @event_listener def on_event_blockchain_updated(self): @@ -71,7 +71,7 @@ def on_event_blockchain_updated(self): @event_listener def on_event_default_server_changed(self, *args): - self.defaultServerChanged.emit() + self._update_status() @event_listener def on_event_proxy_set(self, *args): @@ -79,15 +79,15 @@ def on_event_proxy_set(self, *args): self.proxySet.emit() self.proxyTorChanged.emit() - def _update_network_status(self): + def _update_status(self): + server = str(self.network.get_parameters().server) + if self._server != server: + self._server = server + self.statusChanged.emit() network_status = self.network.get_status() if self._network_status != network_status: self._network_status = network_status self.statusChanged.emit() - - @event_listener - def on_event_status(self, *args): - self._update_network_status() server_status = self.network.connection_status self._logger.debug('server_status updated: %s' % server_status) if self._server_status != server_status: @@ -104,6 +104,10 @@ def on_event_status(self, *args): self._islagging = server_lag > 1 self.isLaggingChanged.emit() + @event_listener + def on_event_status(self, *args): + self._update_status() + @event_listener def on_event_fee_histogram(self, histogram): self._logger.debug(f'fee histogram updated: {repr(histogram)}') @@ -167,12 +171,12 @@ def on_gossip_setting_changed(self): def height(self): return self._height - @pyqtProperty(str, notify=defaultServerChanged) + @pyqtProperty(str, notify=statusChanged) def server(self): - return str(self.network.get_parameters().server) + return self._server @server.setter - def server(self, server): + def server(self, server: str): net_params = self.network.get_parameters() try: server = ServerAddr.from_str_with_inference(server) @@ -182,6 +186,13 @@ def server(self, server): net_params = net_params._replace(server=server, auto_connect=self._qeconfig.autoConnect) self.network.run_from_another_thread(self.network.set_parameters(net_params)) + @pyqtProperty(str, notify=statusChanged) + def serverWithStatus(self): + server = self._server + if self._server_status != "connected": # connecting or disconnected + return f"{server} (connecting...)" + return server + @pyqtProperty(str, notify=statusChanged) def status(self): return self._network_status From 81761c2ef1949d77191ac48e91d38552115d7222 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 00:57:20 +0000 Subject: [PATCH 0546/1143] qml: BalanceSummary to treat server_status=="connecting" same as DC-ed --- electrum/gui/qml/components/controls/BalanceSummary.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index f16d6e913..6c2571cb5 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -42,7 +42,7 @@ Item { GridLayout { id: balanceLayout columns: 3 - opacity: Daemon.currentWallet.synchronizing || Network.server_status == 'disconnected' ? 0 : 1 + opacity: Daemon.currentWallet.synchronizing || Network.server_status != 'connected' ? 0 : 1 Label { font.pixelSize: constants.fontSizeXLarge @@ -140,7 +140,7 @@ Item { } Label { - opacity: Daemon.currentWallet.synchronizing && Network.server_status != 'disconnected' ? 1 : 0 + opacity: Daemon.currentWallet.synchronizing && Network.server_status == 'connected' ? 1 : 0 anchors.centerIn: balancePane text: Daemon.currentWallet.synchronizingProgress color: Material.accentColor @@ -148,7 +148,7 @@ Item { } Label { - opacity: Network.server_status == 'disconnected' ? 1 : 0 + opacity: Network.server_status != 'connected' ? 1 : 0 anchors.centerIn: balancePane text: qsTr('Disconnected') color: Material.accentColor From bd725b50d1f190cc1a868e3a64ad0db8d5db8a40 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 01:28:56 +0000 Subject: [PATCH 0547/1143] update block header checkpoints --- electrum/checkpoints.json | 68 ++++++++++++ electrum/checkpoints_testnet.json | 168 ++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) diff --git a/electrum/checkpoints.json b/electrum/checkpoints.json index 2211894cd..6add59d86 100644 --- a/electrum/checkpoints.json +++ b/electrum/checkpoints.json @@ -1482,5 +1482,73 @@ [ "00000000000000000001095f6deb27964f80c74f38217a32044c20265e0f40e3", 956871428990014096800480126306339386063092842672160768 + ], + [ + "0000000000000000000073616e48a83e3e27b70ba27cd38c683d0014189b41b9", + 950899733299880027476699870079860653644778702301560832 + ], + [ + "000000000000000000016ca393ebd4f388b294134f5633a62d4268b3e4b544dc", + 870306687010904716955275873664553942808871958151692288 + ], + [ + "000000000000000000028a4784e2bd24e775437108c0e7a90469bff4a62895d2", + 841292956506611632223096322365470292302662385308532736 + ], + [ + "0000000000000000000136eb131bc275961ef87f4a06f56d849ef7de6067a83c", + 859664032087861081904916640712713969859737457373741056 + ], + [ + "0000000000000000000009f301f2215237cab791aedc296f102fd7b9dfbff456", + 757060771140682373435345150716700036747812369126653952 + ], + [ + "00000000000000000007ac57aa98595dd1daa3db89f17a817b445b083d696e0f", + 731886405437657570669286679473162061734238931073892352 + ], + [ + "00000000000000000006e6f83c247026057e769aace3815f8138941b256a3ad2", + 733349368576625804490408567990711061036914519549411328 + ], + [ + "00000000000000000001348162a93f4734709f6a142b19aeefd8714f46d0b8f9", + 729612308889970685728561745873455525355654300037021696 + ], + [ + "0000000000000000000428fc10bebcc825140bc83b01b8be32488bcd408e1389", + 787270009984312136754615316208945606764100494789967872 + ], + [ + "0000000000000000000152525a810f2033976f89ba434694c0fe5bf97730f625", + 762342638057996256581733267702136683580848909336969216 + ], + [ + "00000000000000000001fe51e048f4f42e4212098de90827f929403e17b71988", + 790751306884434347505776493480475792916920926107336704 + ], + [ + "000000000000000000064d0f3321a86b27d4883ab1ccedd12cf0941fb74f01a7", + 717191006474295341826748628480199835971598529354268672 + ], + [ + "000000000000000000053916f5f3b68319baf095c4205ad4edca517cd89b4a51", + 685105199528332699160504931662746558558072186305773568 + ], + [ + "00000000000000000006813d0260b4726b64e83025e904165ffaaf06d17227d7", + 688509036841676372057001313638142781710850853198364672 + ], + [ + "00000000000000000005c39a2670a916dc4075afaaf8fe30e495c13ba323e141", + 626181838016062686207286970262124176054603953970610176 + ], + [ + "00000000000000000005e07bf1518ede202f3a18b1a710e9700046755b3c7550", + 619023402996415923713925321951479821824329196375113728 + ], + [ + "00000000000000000005776a4242de5ebfc59d247a114188851e3d24fd173f61", + 575524729764536260159429050275345090310309676098519040 ] ] \ No newline at end of file diff --git a/electrum/checkpoints_testnet.json b/electrum/checkpoints_testnet.json index ad6891d5c..a21ed95ae 100644 --- a/electrum/checkpoints_testnet.json +++ b/electrum/checkpoints_testnet.json @@ -4638,5 +4638,173 @@ [ "0000000000000ae07f7535851cb685259a447d1ad5d3206fc4ee3693bb7421a3", 0 + ], + [ + "00000000000000d41e23f89aad486957071e016d836382605770b65d7539d161", + 0 + ], + [ + "0000000000000016b6843174d892b13a0fa39cf807879b56b723075de7492118", + 0 + ], + [ + "000000000000001a7c215c98d09179f0558b18f6987150cfcf5e57afca65b98a", + 0 + ], + [ + "0000000000000027150d0a4ebf9001e210ebed81ab239535aca8fb5a489a1ead", + 0 + ], + [ + "00000000000000312d9a4fbd4bbde5e3f2266047e65f9a5e84474d62afea0514", + 0 + ], + [ + "00000000f82b6cc148557cb060b8cc3d697e38250630bc2e7188ad4500291b5e", + 0 + ], + [ + "0000000083ccf3997bad3cb32d0e46ff7875a0f454a3c48c2ff910d010801ad2", + 0 + ], + [ + "000000000000f40bd4d7c3d374a984d0c8a744c3816b713e8f43b9dcf75f7848", + 0 + ], + [ + "0000000000002c0374e865c02fed16da853c3086a28b0c212591469c95a71205", + 0 + ], + [ + "0000000000008301f16f29f38442d1b1f521650eba2382ea1e0055a291ca6422", + 0 + ], + [ + "000000000000a954b023180407c904341edba6375a982627b0724afc56f16505", + 0 + ], + [ + "000000000000c59c851bff2090533bc3009ae76cb1ee89e247dfaf11a5c77e7f", + 0 + ], + [ + "000000000000d779b30aab849a7f8a3af7f283bb95579d2d05714b6c3aeed955", + 0 + ], + [ + "00000000000016bca79f9de99fd3d0399f812f0fd5be4f84bd7ee442b846498f", + 0 + ], + [ + "000000000000dfdeb639143c64a17b99b2025a7d9bbb53e993ceb7c1656ceac1", + 0 + ], + [ + "00000000000023e3d31bc565be6041cad487f77bb23109aeaa804d72bb22d6df", + 0 + ], + [ + "00000000000007ebb67ae92e144382f52aebbc63a4604c8a07bfebfcf8a19546", + 0 + ], + [ + "0000000000000136c4a1582c01a5824f4fde4ddf91d653899b53994c4da9a3e1", + 0 + ], + [ + "000000000000001c197b662a51a6b9d5a4eb9521bc52c82f06aee07e8b58f47a", + 0 + ], + [ + "00000000ff1ae8e1ad7dc6a82747e125d99e099969d5fff2f193246529b225c9", + 0 + ], + [ + "0000000048af658629f65a2ef6052fc8cca3234f33d3fc329a0a2b4a73fadefc", + 0 + ], + [ + "0000000000008cef9b1eb41f402fa0f1f6a1ff6641ef3484d7decd9ceb7f1efe", + 0 + ], + [ + "0000000000003ff9199a773f976eda5420300f1f42f213d9d793ca002c17a5fe", + 0 + ], + [ + "0000000000001c6f16503cf6ce37c09f31eceac19e9a46eda67061bec5c6abac", + 0 + ], + [ + "0000000000002ee7040adca7f697117c59b8df1dc65519e3145d720b01add98e", + 0 + ], + [ + "0000000000009718224588a74633c646a7539d05ed503064e38f7734b146be9e", + 0 + ], + [ + "00000000000046fd759769b3296aa5636c3d113a309d633743e092b463072842", + 0 + ], + [ + "0000000000003594adc1bf018bbb36c059ac293164d83357817eb9b7b3ea320a", + 0 + ], + [ + "00000000000069f72a110745c74ead6d9f488906ee79cd2e6dfaa77f9371c300", + 0 + ], + [ + "0000000000001763cf5fffd8e1b0122a7d4bb0e1c2cb17bdf22fa25b70fa6e49", + 0 + ], + [ + "0000000000000c91abea6900d1ee2c168bc34abef1260776a164caecaaf283db", + 0 + ], + [ + "000000000000034624e683ff5df51b1a78fe027c67633c77258d3b1fca48a124", + 0 + ], + [ + "00000000000000b8afa5359b5cc460a77b047cbf8b1aaf640b8779c036c1cc77", + 0 + ], + [ + "000000000000001733362d084551627b7a71cf84b9365d4a8b1131d8e1f0fae9", + 0 + ], + [ + "000000000000001c3cd1aef22fb58f8917110976d342a0573aed0a466702adca", + 0 + ], + [ + "0000000000000015a4454fac29770dce9fc9786152a68807322ef00d74da0640", + 0 + ], + [ + "0000000000000005628d26b0af6507d1218a6665e8e6867ab37b84a69ba15cd8", + 0 + ], + [ + "000000000000001551c40d57827ecb548a09f2512ab66a3d3dd86f00f8083ae1", + 0 + ], + [ + "0000000000000014b20ef449f75f3c015bcef0e19d302e440f9ecb4c183300dc", + 0 + ], + [ + "000000000000000e814b868e01fe7a4a78d966e4ef73f5293c633005ef5718dc", + 0 + ], + [ + "000000000000001e4d4ba22dad9356f9753b1065765799c080807a075964f8a8", + 0 + ], + [ + "000000000000000c2726580d5aaf194818abcb0dc9275266fa6604b792dcc41c", + 0 ] ] \ No newline at end of file From e40ab26bd38f69f319c7bfe4e504f964a345f83f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Mar 2023 09:29:02 +0200 Subject: [PATCH 0548/1143] qml: qenetwork log server_status only when changing, log network_status updates --- electrum/gui/qml/qenetwork.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 59d327c8b..dfd838eee 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -86,11 +86,12 @@ def _update_status(self): self.statusChanged.emit() network_status = self.network.get_status() if self._network_status != network_status: + self._logger.debug('network_status updated: %s' % network_status) self._network_status = network_status self.statusChanged.emit() server_status = self.network.connection_status - self._logger.debug('server_status updated: %s' % server_status) if self._server_status != server_status: + self._logger.debug('server_status updated: %s' % server_status) self._server_status = server_status self.statusChanged.emit() chains = len(self.network.get_blockchains()) From 2cbb16ae4b8e58278fc551490f2e8296856e59b5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Mar 2023 10:41:01 +0200 Subject: [PATCH 0549/1143] qml: move save_tx to qewallet --- electrum/gui/qml/components/TxDetails.qml | 12 ++++++++++-- electrum/gui/qml/qetxdetails.py | 12 +----------- electrum/gui/qml/qewallet.py | 24 ++++++++++++++++++++--- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index a7b803225..3561b3516 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -421,7 +421,13 @@ Pane { }) dialog.open() } - onSaveTxSuccess: { + } + + Connections { + target: Daemon.currentWallet + function onSaveTxSuccess(txid) { + if (txid != txdetails.txid) + return var dialog = app.messageDialog.createObject(app, { text: qsTr('Transaction added to wallet history.') + '\n\n' + qsTr('Note: this is an offline transaction, if you want the network to see it, you need to broadcast it.') @@ -429,7 +435,9 @@ Pane { dialog.open() root.close() } - onSaveTxError: { + function onSaveTxError(txid, code, message) { + if (txid != txdetails.txid) + return var dialog = app.messageDialog.createObject(app, { text: message }) dialog.open() } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 40e8176b2..d4a7e82c9 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -386,17 +386,7 @@ def save(self): if not self._tx: return - try: - if not self._wallet.wallet.adb.add_transaction(self._tx): - self.saveTxError.emit('conflict', - _("Transaction could not be saved.") + "\n" + _("It conflicts with current history.")) - return - self._wallet.wallet.save_db() - self.saveTxSuccess.emit() - self._wallet.historyModel.init_model(True) - except AddTransactionException as e: - self.saveTxError.emit('error', str(e)) - finally: + if self._wallet.save_tx(self._tx): self._can_save_as_local = False self._can_remove = True self.detailsChanged.emit() diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index ec63b3f39..20789e020 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -12,8 +12,8 @@ from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID from electrum.logging import get_logger from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.transaction import PartialTxOutput -from electrum.util import (parse_max_spend, InvalidPassword, event_listener) +from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.util import parse_max_spend, InvalidPassword, event_listener, AddTransactionException from electrum.plugin import run_hook from electrum.wallet import Multisig_Wallet from electrum.crypto import pw_decode_with_version_and_mac @@ -65,6 +65,8 @@ def getInstanceFor(cls, wallet): transactionSigned = pyqtSignal([str], arguments=['txid']) broadcastSucceeded = pyqtSignal([str], arguments=['txid']) broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason']) + saveTxSuccess = pyqtSignal([str], arguments=['txid']) + saveTxError = pyqtSignal([str,str], arguments=['txid', 'code', 'message']) importChannelBackupFailed = pyqtSignal([str], arguments=['message']) labelsUpdated = pyqtSignal() otpRequested = pyqtSignal() @@ -509,7 +511,7 @@ def do_sign(self, tx, broadcast): if broadcast: self.broadcast(tx) else: - # not broadcasted, so add to history now + # not broadcasted, so refresh history here self.historyModel.init_model(True) # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok @@ -553,6 +555,22 @@ def broadcast_thread(): #TODO: properly catch server side errors, e.g. bad-txns-inputs-missingorspent + def save_tx(self, tx: 'PartialTransaction'): + assert tx + + try: + if not self.wallet.adb.add_transaction(tx): + self.saveTxError.emit(tx.txid(), 'conflict', + _("Transaction could not be saved.") + "\n" + _("It conflicts with current history.")) + return + self.wallet.save_db() + self.saveTxSuccess.emit(tx.txid()) + self.historyModel.init_model(True) + return True + except AddTransactionException as e: + self.saveTxError.emit(tx.txid(), 'error', str(e)) + return False + paymentAuthRejected = pyqtSignal() def ln_auth_rejected(self): self.paymentAuthRejected.emit() From 5ef7fabc73601b8bb9793a5193fb71d396266473 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 30 Mar 2023 11:24:50 +0200 Subject: [PATCH 0550/1143] qml InvoiceDialog: merge status and userinfo --- electrum/gui/qml/components/Constants.qml | 2 + electrum/gui/qml/components/InvoiceDialog.qml | 42 +++++-------------- .../qml/components/controls/InfoTextArea.qml | 19 +++++++-- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 1852d4a46..87c3c6fd2 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -38,6 +38,8 @@ Item { property color colorInfo: Material.accentColor property color colorWarning: 'yellow' property color colorError: '#ffff8080' + property color colorProgress: '#ffffff80' + property color colorDone: '#ff80ff80' property color colorMine: "yellow" property color colorLightningLocal: "blue" diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index adff3ca83..16ba9a645 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -52,10 +52,18 @@ ElDialog { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge visible: text - text: invoice.userinfo - iconStyle: invoice.status == Invoice.Failed || invoice.status == Invoice.Expired + text: invoice.userinfo ? invoice.userinfo : invoice.status_str + iconStyle: invoice.status == Invoice.Failed || invoice.status == Invoice.Unknown ? InfoTextArea.IconStyle.Warn - : InfoTextArea.IconStyle.Info + : invoice.status == Invoice.Expired + ? InfoTextArea.IconStyle.Error + : invoice.status == Invoice.Inflight || invoice.status == Invoice.Routing || invoice.status == Invoice.Unconfirmed + ? InfoTextArea.IconStyle.Progress + : invoice.status == Invoice.Paid + ? InfoTextArea.IconStyle.Done + : invoice.status == Invoice.Unpaid && invoice.expiration > 0 + ? InfoTextArea.IconStyle.Pending + : InfoTextArea.IconStyle.Info } Label { @@ -85,34 +93,6 @@ ElDialog { } } - Label { - text: qsTr('Status') - color: Material.accentColor - } - - RowLayout { - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - source: invoice.status == Invoice.Expired - ? '../../icons/expired.png' - : invoice.status == Invoice.Unpaid - ? '../../icons/unpaid.png' - : invoice.status == Invoice.Failed || invoice.status == Invoice.Unknown - ? '../../icons/warning.png' - : invoice.status == Invoice.Inflight || invoice.status == Invoice.Routing - ? '../../icons/status_waiting.png' - : invoice.status == Invoice.Unconfirmed - ? '../../icons/unconfirmed.png' - : invoice.status == Invoice.Paid - ? '../../icons/confirmed.png' - : '' - } - Label { - text: invoice.status_str - } - } - Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index a660a722d..b8867eb77 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -8,7 +8,10 @@ TextHighlightPane { None, Info, Warn, - Error + Error, + Progress, + Pending, + Done } property alias text: infotext.text @@ -21,7 +24,11 @@ TextHighlightPane { ? constants.colorWarning : iconStyle == InfoTextArea.IconStyle.Error ? constants.colorError - : constants.colorInfo + : iconStyle == InfoTextArea.IconStyle.Progress + ? constants.colorProgress + : iconStyle == InfoTextArea.IconStyle.Done + ? constants.colorDone + : constants.colorInfo padding: constants.paddingXLarge RowLayout { @@ -35,7 +42,13 @@ TextHighlightPane { ? "../../../icons/warning.png" : iconStyle == InfoTextArea.IconStyle.Error ? "../../../icons/expired.png" - : "" + : iconStyle == InfoTextArea.IconStyle.Progress + ? "../../../icons/unconfirmed.png" + : iconStyle == InfoTextArea.IconStyle.Pending + ? "../../../icons/unpaid.png" + : iconStyle == InfoTextArea.IconStyle.Done + ? "../../../icons/confirmed.png" + : "" Layout.preferredWidth: constants.iconSizeMedium Layout.preferredHeight: constants.iconSizeMedium } From 6bec49856084e6fc5270938ceea26dfce89fee82 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 30 Mar 2023 12:26:35 +0200 Subject: [PATCH 0551/1143] qml: use the same button for copy and paste --- electrum/gui/qml/components/SendDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index 49ee44afa..75001f99b 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -63,8 +63,8 @@ ElDialog { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 - icon.source: '../../icons/paste.png' - text: qsTr('Paste from clipboard') + icon.source: '../../icons/copy_bw.png' + text: qsTr('Paste') onClicked: dialog.dispatch(AppController.clipboardToText()) } } From c08ca945913031742d8843eb24e0e39063659528 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Mar 2023 12:26:28 +0200 Subject: [PATCH 0552/1143] qml: support create & save transaction on watch-only wallet, refactor showExport and supply relevant help text when sharing a transaction --- .../gui/qml/components/ExportTxDialog.qml | 9 +--- electrum/gui/qml/components/TxDetails.qml | 33 ++++++------ .../gui/qml/components/WalletMainView.qml | 50 ++++++++++++++++--- electrum/gui/qml/qetxdetails.py | 7 +-- electrum/gui/qml/qetxfinalizer.py | 21 ++++---- electrum/gui/qml/qewallet.py | 13 +++++ 6 files changed, 90 insertions(+), 43 deletions(-) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 0db305af4..8a555727f 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -8,9 +8,7 @@ import "controls" ElDialog { id: dialog - property QtObject txdetails - - property string text + required property string text property string text_qr // if text_qr is undefined text will be used property string text_help @@ -97,9 +95,4 @@ ElDialog { } } } - - Component.onCompleted: { - text = dialog.txdetails.serializedTx(false) - text_qr = dialog.txdetails.serializedTx(true) - } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 3561b3516..8f61e901a 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -24,14 +24,6 @@ Pane { app.stack.pop() } - function showExport(helptext) { - var dialog = exportTxDialog.createObject(root, { - txdetails: txdetails, - text_help: helptext - }) - dialog.open() - } - ColumnLayout { anchors.fill: parent spacing: 0 @@ -378,9 +370,24 @@ Pane { Layout.preferredWidth: 1 icon.source: '../../icons/qrcode_white.png' text: qsTr('Share') + enabled: !txdetails.isUnrelated onClicked: { - var dialog = exportTxDialog.createObject(root, { txdetails: txdetails }) - dialog.open() + var msg = '' + if (txdetails.isComplete) { + // TODO: iff offline wallet? + // TODO: or also if just temporarily offline? + msg = qsTr('This transaction is complete. Please share it with an online device') + } else if (txdetails.wallet.isWatchOnly) { + msg = qsTr('This transaction should be signed. Present this QR code to the signing device') + } else if (txdetails.wallet.isMultisig && txdetails.wallet.walletType != '2fa') { + if (txdetails.canSign) { + msg = qsTr('Note: this wallet can sign, but has not signed this transaction yet') + } else { + msg = qsTr('Transaction is partially signed by this wallet. Present this QR code to the next co-signer') + } + } + + app.stack.getRoot().showExport(txdetails.getSerializedTx(false), txdetails.getSerializedTx(true), msg) } } @@ -522,10 +529,4 @@ Pane { } } - Component { - id: exportTxDialog - ExportTxDialog { - onClosed: destroy() - } - } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 91f008575..a6c12bf65 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -50,6 +50,19 @@ Item { } } + function showExportByTxid(txid, helptext) { + showExport(Daemon.currentWallet.getSerializedTx(txid, false), Daemon.currentWallet.getSerializedTx(txid, true), helptext) + } + + function showExport(data, data_qr, helptext) { + var dialog = exportTxDialog.createObject(app, { + text: data, + text_qr: data_qr, + text_help: helptext + }) + dialog.open() + } + property QtObject menu: Menu { parent: Overlay.overlay dim: true @@ -251,7 +264,7 @@ Item { if (code == 'ln') { var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) dialog.yesClicked.connect(function() { - createRequest(true, false) + createRequest(true, false) }) } else if (code == 'reuse_addr') { var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) @@ -287,6 +300,8 @@ Item { Component { id: invoiceDialog InvoiceDialog { + id: _invoiceDialog + width: parent.width height: parent.height @@ -300,7 +315,11 @@ Item { var canComplete = !Daemon.currentWallet.isWatchOnly && Daemon.currentWallet.canSignWithoutCosigner dialog.txaccepted.connect(function() { if (!canComplete) { - dialog.finalizer.signAndSave() + if (Daemon.currentWallet.isWatchOnly) { + dialog.finalizer.save() + } else { + dialog.finalizer.signAndSave() + } } else { dialog.finalizer.signAndSend() } @@ -317,6 +336,13 @@ Item { } onClosed: destroy() + + Connections { + target: Daemon.currentWallet + function onSaveTxSuccess(txid) { + _invoiceDialog.close() + } + } } } @@ -371,7 +397,7 @@ Item { console.log('rejected') } onClosed: destroy() - } + } } Component { @@ -393,9 +419,13 @@ Item { wallet: Daemon.currentWallet canRbf: true onFinishedSave: { - // tx was (partially) signed and saved. Show QR for co-signers or online wallet - var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { txid: txid }) - page.showExport(qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer')) + if (wallet.isWatchOnly) { + // tx was saved. Show QR for signer(s) + showExportByTxid(txid, qsTr('Transaction created. Present this QR code to the signing device')) + } else { + // tx was (partially) signed and saved. Show QR for co-signers or online wallet + showExportByTxid(txid, qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer')) + } _confirmPaymentDialog.destroy() } } @@ -432,5 +462,13 @@ Item { onClosed: destroy() } } + + Component { + id: exportTxDialog + ExportTxDialog { + onClosed: destroy() + } + } + } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index d4a7e82c9..397db4ba8 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -393,8 +393,9 @@ def save(self): @pyqtSlot(result=str) @pyqtSlot(bool, result=str) - def serializedTx(self, for_qr=False): + def getSerializedTx(self, for_qr=False): + tx = self._tx if for_qr: - return self._tx.to_qr_data() + return tx.to_qr_data() else: - return str(self._tx) + return str(tx) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 2a53f2416..62b0d8493 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -341,6 +341,15 @@ def update(self): self._valid = True self.validChanged.emit() + @pyqtSlot() + def save(self): + if not self._valid or not self._tx: + self._logger.debug('no valid tx') + return + + if self._wallet.save_tx(self._tx): + self.finishedSave.emit(self._tx.txid()) + @pyqtSlot() def signAndSend(self): if not self._valid or not self._tx: @@ -379,18 +388,10 @@ def onSigned(self, txid): self._logger.debug('onSigned') self._wallet.transactionSigned.disconnect(self.onSigned) - if not self._wallet.wallet.adb.add_transaction(self._tx): + if not self._wallet.save_tx(self._tx): self._logger.error('Could not save tx') - - self.finishedSave.emit(self._tx.txid()) - - @pyqtSlot(result=str) - @pyqtSlot(bool, result=str) - def serializedTx(self, for_qr=False): - if for_qr: - return self._tx.to_qr_data() else: - return str(self._tx) + self.finishedSave.emit(self._tx.txid()) # mixin for watching an existing TX based on its txid for verified event # requires self._wallet to contain a QEWallet instance diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 20789e020..9e8011448 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -328,6 +328,10 @@ def canHaveLightning(self): def walletType(self): return self.wallet.wallet_type + @pyqtProperty(bool, notify=dataChanged) + def isMultisig(self): + return isinstance(self.wallet, Multisig_Wallet) + @pyqtProperty(bool, notify=dataChanged) def hasSeed(self): return self.wallet.has_seed() @@ -723,3 +727,12 @@ def retrieve_seed(self): self._seed = '' self.dataChanged.emit() + + @pyqtSlot(str, result=str) + @pyqtSlot(str, bool, result=str) + def getSerializedTx(self, txid, for_qr=False): + tx = self.wallet.db.get_transaction(txid) + if for_qr: + return tx.to_qr_data() + else: + return str(tx) From d064b38f1c6c0190392bd96e01633b63206b1b6d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Mar 2023 13:23:14 +0200 Subject: [PATCH 0553/1143] qml: split updating userinfo from determine_can_pay, check determine_can_pay also in event handlers --- electrum/gui/qml/qeinvoice.py | 62 ++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index d850fd41c..6cedce037 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -170,12 +170,14 @@ def on_destroy(self): def on_event_payment_succeeded(self, wallet, key): if wallet == self._wallet.wallet and key == self.key: self.statusChanged.emit() + self.determine_can_pay() self.userinfo = _('Paid!') @event_listener def on_event_payment_failed(self, wallet, key, reason): if wallet == self._wallet.wallet and key == self.key: self.statusChanged.emit() + self.determine_can_pay() self.userinfo = _('Payment failed: ') + reason @event_listener @@ -183,6 +185,7 @@ def on_event_invoice_status(self, wallet, key, status): if wallet == self._wallet.wallet and key == self.key: self.statusChanged.emit() if status in [PR_INFLIGHT, PR_ROUTING]: + self.determine_can_pay() self.userinfo = _('In progress...') @pyqtProperty(int, notify=invoiceChanged) @@ -231,6 +234,7 @@ def amount(self, new_amount): if self._effectiveInvoice: self._effectiveInvoice.amount_msat = '!' if new_amount.isMax else int(new_amount.satsInt * 1000) + self.update_userinfo() self.determine_can_pay() self.invoiceChanged.emit() @@ -242,12 +246,11 @@ def amountOverride(self): def amountOverride(self, new_amount): self._logger.debug(f'set new override amount {repr(new_amount)}') self._amountOverride.copyFrom(new_amount) - - self.determine_can_pay() self.amountOverrideChanged.emit() @pyqtSlot() def _on_amountoverride_value_changed(self): + self.update_userinfo() self.determine_can_pay() @pyqtProperty('quint64', notify=invoiceChanged) @@ -338,6 +341,7 @@ def set_effective_invoice(self, invoice: Invoice): self.set_lnprops() + self.update_userinfo() self.determine_can_pay() self.invoiceChanged.emit() @@ -353,6 +357,7 @@ def set_status_timer(self): self._timer.setInterval(interval) # msec self._timer.start() else: + self.update_userinfo() self.determine_can_pay() # status went to PR_EXPIRED @pyqtSlot() @@ -360,9 +365,7 @@ def updateStatusString(self): self.statusChanged.emit() self.set_status_timer() - def determine_can_pay(self): - self.canPay = False - self.canSave = False + def update_userinfo(self): self.userinfo = '' if not self.amountOverride.isEmpty: @@ -370,8 +373,6 @@ def determine_can_pay(self): else: amount = self.amount - self.canSave = True - if self.amount.isEmpty: self.userinfo = _('Enter the amount you want to send') @@ -384,13 +385,9 @@ def determine_can_pay(self): lnaddr = self._effectiveInvoice._lnaddr if lnaddr.amount and amount.satsInt < lnaddr.amount * COIN: self.userinfo = _('Cannot pay less than the amount specified in the invoice') - else: - self.canPay = True - elif self.address and self.get_max_spendable_onchain() > amount.satsInt: + elif self.address and self.get_max_spendable_onchain() < amount.satsInt: # TODO: validate address? # TODO: subtract fee? - self.canPay = True - else: self.userinfo = _('Insufficient balance') else: self.userinfo = { @@ -402,13 +399,7 @@ def determine_can_pay(self): }[self.status] elif self.invoiceType == QEInvoice.Type.OnchainInvoice: if self.status in [PR_UNPAID, PR_FAILED]: - if amount.isMax and self.get_max_spendable_onchain() > 0: - # TODO: dust limit? - self.canPay = True - elif self.get_max_spendable_onchain() >= amount.satsInt: - # TODO: subtract fee? - self.canPay = True - else: + if not ((amount.isMax and self.get_max_spendable_onchain() > 0) or (self.get_max_spendable_onchain() >= amount.satsInt)): self.userinfo = _('Insufficient balance') else: self.userinfo = { @@ -418,6 +409,39 @@ def determine_can_pay(self): PR_UNKNOWN: _('Invoice has unknown status'), }[self.status] + def determine_can_pay(self): + self.canPay = False + self.canSave = False + + if not self.amountOverride.isEmpty: + amount = self.amountOverride + else: + amount = self.amount + + self.canSave = True + + if amount.isEmpty and self.status == PR_UNPAID: # unspecified amount + return + + if self.invoiceType == QEInvoice.Type.LightningInvoice: + if self.status in [PR_UNPAID, PR_FAILED]: + if self.get_max_spendable_lightning() >= amount.satsInt: + lnaddr = self._effectiveInvoice._lnaddr + if not (lnaddr.amount and amount.satsInt < lnaddr.amount * COIN): + self.canPay = True + elif self.address and self.get_max_spendable_onchain() > amount.satsInt: + # TODO: validate address? + # TODO: subtract fee? + self.canPay = True + elif self.invoiceType == QEInvoice.Type.OnchainInvoice: + if self.status in [PR_UNPAID, PR_FAILED]: + if amount.isMax and self.get_max_spendable_onchain() > 0: + # TODO: dust limit? + self.canPay = True + elif self.get_max_spendable_onchain() >= amount.satsInt: + # TODO: subtract fee? + self.canPay = True + def setValidOnchainInvoice(self, invoice: Invoice): self._logger.debug('setValidOnchainInvoice') if invoice.is_lightning(): From 4517b3c2bb307c940c0678661b56a74773cde3c6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Mar 2023 16:00:36 +0200 Subject: [PATCH 0554/1143] qml: handle DataOverflow in qeqr.py and QRImage --- .../gui/qml/components/controls/QRImage.qml | 11 +++++- electrum/gui/qml/qeqr.py | 38 +++++++++++++------ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/components/controls/QRImage.qml b/electrum/gui/qml/components/controls/QRImage.qml index 638773aea..b9a4d900d 100644 --- a/electrum/gui/qml/components/controls/QRImage.qml +++ b/electrum/gui/qml/components/controls/QRImage.qml @@ -1,4 +1,5 @@ import QtQuick 2.6 +import QtQuick.Controls 2.15 Item { id: root @@ -17,10 +18,10 @@ Item { } Image { - source: qrdata && render ? 'image://qrgen/' + qrdata : '' + source: qrdata && render && qrprops.modules > 0 ? 'image://qrgen/' + qrdata : '' Rectangle { - visible: root.render + visible: root.render && qrprops.valid color: 'white' x: (parent.width - width) / 2 y: (parent.height - height) / 2 @@ -28,6 +29,7 @@ Item { height: qrprops.icon_modules * qrprops.box_size Image { + visible: qrprops.valid source: '../../../icons/electrum.png' x: 1 y: 1 @@ -36,5 +38,10 @@ Item { scale: 0.9 } } + Label { + visible: !qrprops.valid + text: qsTr('Data too big for QR') + anchors.centerIn: parent + } } } diff --git a/electrum/gui/qml/qeqr.py b/electrum/gui/qml/qeqr.py index 1bd9b1e35..cc3d8a4f0 100644 --- a/electrum/gui/qml/qeqr.py +++ b/electrum/gui/qml/qeqr.py @@ -1,12 +1,14 @@ import asyncio import qrcode +from qrcode.exceptions import DataOverflowError + import math import urllib from PIL import Image, ImageQt from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRect, QPoint -from PyQt5.QtGui import QImage,QColor +from PyQt5.QtGui import QImage, QColor from PyQt5.QtQuick import QQuickImageProvider from electrum.logging import get_logger @@ -143,13 +145,20 @@ def requestImage(self, qstr, size): # calculate best box_size pixelsize = min(self._max_size, 400) - modules = 17 + 4 * qr.best_fit() + qr.border * 2 - qr.box_size = math.floor(pixelsize/modules) - - qr.make(fit=True) - - pimg = qr.make_image(fill_color='black', back_color='white') - self.qimg = ImageQt.ImageQt(pimg) + try: + modules = 17 + 4 * qr.best_fit() + qr.border * 2 + qr.box_size = math.floor(pixelsize/modules) + + qr.make(fit=True) + + pimg = qr.make_image(fill_color='black', back_color='white') + self.qimg = ImageQt.ImageQt(pimg) + except DataOverflowError: + # fake it + modules = 17 + qr.border * 2 + box_size = math.floor(pixelsize/modules) + self.qimg = QImage(box_size * modules, box_size * modules, QImage.Format_RGB32) + self.qimg.fill(QColor('gray')) return self.qimg, self.qimg.size() # helper for placing icon exactly where it should go on the QR code @@ -167,12 +176,17 @@ def getDimensions(self, qstr): # calculate best box_size pixelsize = min(self._max_size, 400) - modules = 17 + 4 * qr.best_fit() + qr.border * 2 - qr.box_size = math.floor(pixelsize/modules) + try: + modules = 17 + 4 * qr.best_fit() + qr.border * 2 + valid = True + except DataOverflowError: + # fake it + modules = 17 + qr.border * 2 + valid = False + qr.box_size = math.floor(pixelsize/modules) # calculate icon width in modules icon_modules = int(modules / 5) icon_modules += (icon_modules+1)%2 # force odd - return { 'modules': modules, 'box_size': qr.box_size, 'icon_modules': icon_modules } - + return { 'modules': modules, 'box_size': qr.box_size, 'icon_modules': icon_modules, 'valid' : valid } From 8a2372a133a6660f224624c9d62ac808186bfe99 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 30 Mar 2023 16:05:10 +0200 Subject: [PATCH 0555/1143] followup 4517b3c2bb307c940c0678661b56a74773cde3c6 --- electrum/gui/qml/components/controls/QRImage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/QRImage.qml b/electrum/gui/qml/components/controls/QRImage.qml index b9a4d900d..875b116b3 100644 --- a/electrum/gui/qml/components/controls/QRImage.qml +++ b/electrum/gui/qml/components/controls/QRImage.qml @@ -18,7 +18,7 @@ Item { } Image { - source: qrdata && render && qrprops.modules > 0 ? 'image://qrgen/' + qrdata : '' + source: qrdata && render ? 'image://qrgen/' + qrdata : '' Rectangle { visible: root.render && qrprops.valid From 4647fda04f08b5cf3ce82a7891a2e26bfd53b982 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 30 Mar 2023 15:16:23 +0200 Subject: [PATCH 0556/1143] qml: show invoices/requests lists through long press I think we can re-enable the requests list, because requests are now created explicitly by pressing the create request button. Since this is an advanced feature, it should not be in the way of people who do not want to see it. Here is a solution that might work. --- electrum/gui/qml/components/SendDialog.qml | 12 ------------ electrum/gui/qml/components/WalletMainView.qml | 7 ++++++- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index 75001f99b..3615a3e11 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -48,18 +48,6 @@ ElDialog { ButtonContainer { Layout.fillWidth: true - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - icon.source: '../../icons/tab_receive.png' - text: qsTr('Saved Invoices') - enabled: Daemon.currentWallet.invoiceModel.rowCount() // TODO: only count non-expired - onClicked: { - dialog.close() - app.stack.push(Qt.resolvedUrl('Invoices.qml')) - } - } - FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index a6c12bf65..f31b7a692 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -188,8 +188,10 @@ Item { var dialog = receiveDetailsDialog.createObject(mainView) dialog.open() } + onPressAndHold: { + app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) + } } - FlatButton { visible: Daemon.currentWallet Layout.fillWidth: true @@ -197,6 +199,9 @@ Item { icon.source: '../../icons/tab_send.png' text: qsTr('Send') onClicked: openSendDialog() + onPressAndHold: { + app.stack.push(Qt.resolvedUrl('Invoices.qml')) + } } } } From 0a3e286f1d6fb463bf3726c8a6f3dc1ad4ae374c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 14:32:31 +0000 Subject: [PATCH 0557/1143] qt tx dialog: show_qr to warn if QR code is missing data When exporting a tx as qr code, the prev txs are omitted to save space. This causes problems with offline signers: software electrum signers will just warn and then proceed, but hw devices will typically error. --- electrum/gui/kivy/uix/dialogs/tx_dialog.py | 2 +- electrum/gui/qml/qetxdetails.py | 6 +++--- electrum/gui/qml/qewallet.py | 2 +- electrum/gui/qt/transaction_dialog.py | 10 ++++++++-- electrum/tests/test_wallet_vertical.py | 6 ++++-- electrum/transaction.py | 11 ++++++++--- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index 0c8406c22..c96b6972f 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -332,7 +332,7 @@ def do_broadcast(self): def show_qr(self): original_raw_tx = str(self.tx) - qr_data = self.tx.to_qr_data() + qr_data = self.tx.to_qr_data()[0] self.app.qr_dialog(_("Raw Transaction"), qr_data, text_for_clipboard=original_raw_tx) def remove_local_tx(self): diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 397db4ba8..69587c63d 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -5,7 +5,7 @@ from electrum.i18n import _ from electrum.logging import get_logger from electrum.util import format_time, AddTransactionException, TxMinedInfo -from electrum.transaction import tx_from_any +from electrum.transaction import tx_from_any, Transaction from electrum.network import Network from .qewallet import QEWallet @@ -31,7 +31,7 @@ def __init__(self, parent=None): self._rawtx = '' self._label = '' - self._tx = None + self._tx = None # type: Optional[Transaction] self._status = '' self._amount = QEAmount() @@ -396,6 +396,6 @@ def save(self): def getSerializedTx(self, for_qr=False): tx = self._tx if for_qr: - return tx.to_qr_data() + return tx.to_qr_data()[0] else: return str(tx) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 9e8011448..a5641503c 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -733,6 +733,6 @@ def retrieve_seed(self): def getSerializedTx(self, txid, for_qr=False): tx = self.wallet.db.get_transaction(txid) if for_qr: - return tx.to_qr_data() + return tx.to_qr_data()[0] else: return str(tx) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index e66a861fe..de648ee38 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -595,9 +595,15 @@ def copy_to_clipboard(self, *, tx: Transaction = None): def show_qr(self, *, tx: Transaction = None): if tx is None: tx = self.tx - qr_data = tx.to_qr_data() + qr_data, is_complete = tx.to_qr_data() + help_text = None + if not is_complete: + help_text = _( + """Warning: Some data (prev txs / "full utxos") was left """ + """out of the QR code as it would not fit. This might cause issues if signing offline. """ + """As a workaround, try exporting the tx as file or text instead.""") try: - self.main_window.show_qrcode(qr_data, 'Transaction', parent=self) + self.main_window.show_qrcode(qr_data, 'Transaction', parent=self, help_text=help_text) except qrcode.exceptions.DataOverflowError: self.show_error(_('Failed to display QR code.') + '\n' + _('Transaction is too large in size.')) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 54c547359..c0d07dfba 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -2846,9 +2846,10 @@ async def test_sending_offline_xprv_online_xpub_p2pkh(self, mock_save_db): with self.subTest(msg="uses_qr_code", uses_qr_code=uses_qr_code): tx = copy.deepcopy(orig_tx) if uses_qr_code: - partial_tx = tx.to_qr_data() + partial_tx, is_complete = tx.to_qr_data() self.assertEqual("8VXO.MYW+UE2.+5LGGVQP.$087REZNQ8:6*U1CLU+NW7:.T7K04HTV.JW78BXOF$IM*4YYL6LWVSZ4QA0Q-1*8W38XJH833$K3EUK:87-TGQ86XAQ3/RD*PZKM1RLVRAVCFG/8.UHCF8IX*ED1HXNGI*WQ37K*HWJ:XXNKMU.M2A$IYUM-AR:*P34/.EGOQF-YUJ.F0UF$LMW-YXWQU$$CMXD4-L21B7X5/OL7MKXCAD5-9IL/TDP5J2$13KFIH2K5B0/2F*/-XCY:/G-+8K*+1U$56WUE3:J/8KOGSRAN66CNZLG7Y4IB$Y*.S64CC2A9Q/-P5TQFZCF7F+CYG+V363/ME.W0WTPXJM3BC.YPH+Y3K7VIF2+0D.O.JS4LYMZ", partial_tx) + self.assertFalse(is_complete) else: partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff010074010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980200000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001976a9149b308d0b3efd4e3469441bc83c3521afde4072b988ac1c391400000100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220602ab053d10eda769fab03ab52ee4f1692730288751369643290a8506e31d1e80f00c233d2ae40000000002000000000022020327295144ffff9943356c2d6625f5e2d6411bab77fd56dce571fda6234324e3d90c233d2ae4010000000000000000", @@ -2946,9 +2947,10 @@ async def test_sending_offline_xprv_online_xpub_p2wpkh(self, mock_save_db): with self.subTest(msg="uses_qr_code", uses_qr_code=uses_qr_code): tx = copy.deepcopy(orig_tx) if uses_qr_code: - partial_tx = tx.to_qr_data() + partial_tx, is_complete = tx.to_qr_data() self.assertEqual("FP:A9SADM6+OGU/3KZ/RCI$7/Y2R7OZYNZXB1.$0Y9K69-BXZZ1EAWLM0/*SYX7G:1/0N9+E5YWF0KRPK/Y-GJSJ7TM/A0N0RO.H*S**8E*$W1P7-3RA-+I.1BA77$P8CSX55OHNIIG735$UEH5XTW5DDVD/HK*EQNTI:E3PO:K3$MSN4C3+LIR/-U91-Z9NS/AF*9BZ53VN.XPKD0$.GN*9HOFL3L7MA7ECA86IPZ1J-HJY:$EPZC*3D:+T-L195ULV7:DJ$$Q$H9:+UR:8:5X*S:YC9/HV-$+XQY8/*S1UN9UCE8R786.RW8V$TGQPUCP$KHFM-18I0Q7*RIHI-U0ULUSCG6L3YAS*O4:AEBQLHB37RHRI1E91", partial_tx) + self.assertFalse(is_complete) else: partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef0100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000", diff --git a/electrum/transaction.py b/electrum/transaction.py index 5d6090e6b..aa9e264dc 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -938,14 +938,19 @@ def create_script_sig(txin: TxInput) -> bytes: else: return nVersion + txins + txouts + nLocktime - def to_qr_data(self) -> str: - """Returns tx as data to be put into a QR code. No side-effects.""" + def to_qr_data(self) -> Tuple[str, bool]: + """Returns (serialized_tx, is_complete). The tx is serialized to be put inside a QR code. No side-effects. + As space in a QR code is limited, some data might have to be omitted. This is signalled via is_complete=False. + """ + is_complete = True tx = copy.deepcopy(self) # make copy as we mutate tx if isinstance(tx, PartialTransaction): # this makes QR codes a lot smaller (or just possible in the first place!) + # note: will not apply if all inputs are taproot, due to new sighash. tx.convert_all_utxos_to_witness_utxos() + is_complete = False tx_bytes = tx.serialize_as_bytes() - return base_encode(tx_bytes, base=43) + return base_encode(tx_bytes, base=43), is_complete def txid(self) -> Optional[str]: if self._cached_txid is None: From 31fde2484f8d845526a12b4effdbdc6a8a6a93fb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 14:56:33 +0000 Subject: [PATCH 0558/1143] qt network dialog: use icon for selected server, instead of "*" Previously we added a " *" suffix to distinguish the selected/main server in the list. However in case of an .onion address, anything we put as a suffix inline is elided/truncated - as the address itself does not fit. Hence we could instead use a prefix - and then why not use an icon. --- electrum/gui/icons/chevron-right.png | Bin 0 -> 1044 bytes electrum/gui/qt/network_dialog.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 electrum/gui/icons/chevron-right.png diff --git a/electrum/gui/icons/chevron-right.png b/electrum/gui/icons/chevron-right.png new file mode 100644 index 0000000000000000000000000000000000000000..97ebfb0b4bf1927607e417639d05e25e13d8ee22 GIT binary patch literal 1044 zcmV+v1nc{WP)iNNZw==s=1vL@#IW@5VFDBzC} zT1W^{2s#Hr|4Iu%MsVWAmk1#S!OMH_`Xng?uRjBBJRw9O=&tnZfovgw_kdyMz#h&^H}Ht|x|Y8$o9gLJUICcM$YZVhF$Dm{UTCFUs38tOt7uAO-+#Jc9g^ z9)f;C2r&plZYqLYO$=ZZzIuxgVi0=W#qfGRaVxrl6K)D2L>Fc0&(Ml~0^B%4h(gfA z$su^zL;x`WwVQ;X7ZO8w1h2~pAqt^d0Izow-vu{uvO9wiVi0`wLHhMTPUE~2LWn|; z{pr^O-Aw?tAr4S?J|YCu5J&&PU^Sl*;;m?9h7jj6&bY~h5Q7l(s|@SGQT~(m>=37y z972$O-UXH+PSA(&ou#pJC|>ZoEP04C2KiL?mEwyX6TEEA(2DA@wii8#l7*)sjvh-7 zLBF*2T6T!z$A0?CDr>K;6~cr?$wM5kf5Xc>Yp<;pysXdAiq7GG(wp+?Z9we2Fg%ZMxDY ztkm3`Yc*r&={pF0FE<^(Xd%y(tp|KT!!}&m&~*Ibg`i)jFTNfszR-rVMlbO-Rm&H( zd-4XaeyecBSF2fktBLDgcd_`5#jie0Ui=_K99Pr0=vHqg4_AWR;J~JMxNf_he)G;_Hu`h+;OlRsZDR)_AxwlC=0zqK+w-K zEV_GG{O02G0}fU~-F+2aZYA#fw^8d%Gm~uIblbyc1K O0000 1: connected_servers_item.addChild(x) From 8291018c0e1b1139cf2de7ea83d7c8e8bf04b973 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 15:34:27 +0000 Subject: [PATCH 0559/1143] interface: workaround electrs erroring on 'blockchain.estimatefee' 15.00 | E | i/interface.[electrum.blockstream.info:50002] | Exception in run: ProtocolError(-32600, 'ill-formed response error object: cannot estimate fee for 25 blocks') Traceback (most recent call last): File "...\electrum\electrum\util.py", line 1261, in wrapper return await func(*args, **kwargs) File "...\electrum\electrum\interface.py", line 516, in wrapper_func return await func(self, *args, **kwargs) File "...\electrum\electrum\interface.py", line 539, in run await self.open_session(ssl_context) File "...\electrum\electrum\interface.py", line 689, in open_session async with self.taskgroup as group: File "...\aiorpcX\aiorpcx\curio.py", line 304, in __aexit__ await self.join() File "...\electrum\electrum\util.py", line 1423, in join task.result() File "...\electrum\electrum\interface.py", line 726, in request_fee_estimates async with OldTaskGroup() as group: File "...\aiorpcX\aiorpcx\curio.py", line 304, in __aexit__ await self.join() File "...\electrum\electrum\util.py", line 1423, in join task.result() File "...\electrum\electrum\interface.py", line 1128, in get_estimatefee res = await self.session.send_request('blockchain.estimatefee', [num_blocks]) File "...\electrum\electrum\interface.py", line 171, in send_request response = await asyncio.wait_for( File "...\Python310\lib\asyncio\tasks.py", line 408, in wait_for return await fut File "...\aiorpcX\aiorpcx\session.py", line 540, in send_request return await self._send_concurrent(message, future, 1) File "...\aiorpcX\aiorpcx\session.py", line 512, in _send_concurrent return await future File "...\aiorpcX\aiorpcx\jsonrpc.py", line 721, in receive_message item, request_id = self._protocol.message_to_item(message) File "...\aiorpcX\aiorpcx\jsonrpc.py", line 273, in message_to_item return cls._process_response(payload) File "...\aiorpcX\aiorpcx\jsonrpc.py", line 220, in _process_response raise cls._error(code, message, False, request_id) aiorpcx.jsonrpc.ProtocolError: (-32600, 'ill-formed response error object: cannot estimate fee for 25 blocks') --- electrum/interface.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/electrum/interface.py b/electrum/interface.py index 4e68141c1..737c94363 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -1121,11 +1121,21 @@ async def get_relay_fee(self) -> int: async def get_estimatefee(self, num_blocks: int) -> int: """Returns a feerate estimate for getting confirmed within num_blocks blocks, in sat/kbyte. + Returns -1 if the server could not provide an estimate. """ if not is_non_negative_integer(num_blocks): raise Exception(f"{repr(num_blocks)} is not a num_blocks") # do request - res = await self.session.send_request('blockchain.estimatefee', [num_blocks]) + try: + res = await self.session.send_request('blockchain.estimatefee', [num_blocks]) + except aiorpcx.jsonrpc.ProtocolError as e: + # The protocol spec says the server itself should already have returned -1 + # if it cannot provide an estimate, however apparently electrs does not conform + # and sends an error instead. Convert it here: + if "cannot estimate fee" in e.message: + res = -1 + else: + raise # check response if res != -1: assert_non_negative_int_or_float(res) From 84d19457a6e8f81e9aa2fc0162391c19ce565c90 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 15:40:45 +0000 Subject: [PATCH 0560/1143] lnpeer: handle NoDynamicFeeEstimates in co-op close note that the existing fallback was insufficient as config.fee_per_kb() can still return None --- electrum/lnpeer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 7cd1a01d5..1110e1726 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -2001,16 +2001,21 @@ async def send_shutdown(self, chan: Channel): def get_shutdown_fee_range(self, chan, closing_tx, is_local): """ return the closing fee and fee range we initially try to enforce """ config = self.network.config + our_fee = None if config.get('test_shutdown_fee'): our_fee = config.get('test_shutdown_fee') else: fee_rate_per_kb = config.eta_target_to_fee(FEE_LN_ETA_TARGET) - if not fee_rate_per_kb: # fallback + if fee_rate_per_kb is None: # fallback fee_rate_per_kb = self.network.config.fee_per_kb() - our_fee = fee_rate_per_kb * closing_tx.estimated_size() // 1000 + if fee_rate_per_kb is not None: + our_fee = fee_rate_per_kb * closing_tx.estimated_size() // 1000 # TODO: anchors: remove this, as commitment fee rate can be below chain head fee rate? # BOLT2: The sending node MUST set fee less than or equal to the base fee of the final ctx max_fee = chan.get_latest_fee(LOCAL if is_local else REMOTE) + if our_fee is None: # fallback + self.logger.warning(f"got no fee estimates for co-op close! falling back to chan.get_latest_fee") + our_fee = max_fee our_fee = min(our_fee, max_fee) # config modern_fee_negotiation can be set in tests if config.get('test_shutdown_legacy'): From 8a394c3e3f31c8b54e4cd7287fe202328779f425 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 30 Mar 2023 16:38:19 +0000 Subject: [PATCH 0561/1143] update locale --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index a87f9a043..14bd5cb70 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit a87f9a04302682bc23ce13bfa6672c12f0de798f +Subproject commit 14bd5cb703cc99b87c70346656ca729f8e8a2df4 From bcd2ec3d70fe51ee54074504c953477148850cb7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 01:04:54 +0000 Subject: [PATCH 0562/1143] (trivial) qt/util: add some leftover type-hints --- electrum/gui/qt/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 63db8c344..5dd43e195 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1081,12 +1081,12 @@ def getSaveFileName( return selected_path -def icon_path(icon_basename): +def icon_path(icon_basename: str): return resource_path('gui', 'icons', icon_basename) @lru_cache(maxsize=1000) -def read_QIcon(icon_basename): +def read_QIcon(icon_basename: str) -> QIcon: return QIcon(icon_path(icon_basename)) class IconLabel(QWidget): From 4a626a113d0a91788e65a848e81ce4db55af309d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 01:14:08 +0000 Subject: [PATCH 0563/1143] qt receive_tab: fix "show_address_on_hw" functionality follow-up b07fe970bfb44f60b77c377847a0c995a76ee556 I don't like this being hidden in the toolbar menu. The other items in the toolbar menu are ~settings or generic actions independent of the current request. This one is dependent on the current request, and even the active "tab"... does not make sense to show this when the lightning tab is active. It is more difficult to discover it in the first place than previously, and it being less visible goes against encouraging hw device users of using it, which is what we should be doing. Anyway, this commit just makes it functional as-is. --- electrum/gui/qt/receive_tab.py | 4 ++++ electrum/plugins/hw_wallet/qt.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index 76cd3aa6c..e5a4ed4c0 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -252,6 +252,8 @@ def update_current_request(self): req = self.wallet.get_request(key) if key else None if req is None: self.receive_e.setText('') + self.addr = self.URI = self.lnaddr = '' + self.address_help = self.URI_help = self.ln_help = '' return help_texts = self.wallet.get_help_texts_for_receive_request(req) self.addr = (req.get_address() or '') if not help_texts.address_is_error else '' @@ -357,6 +359,8 @@ def get_bitcoin_address_for_request(self, amount) -> Optional[str]: def do_clear(self): self.receive_e.setText('') + self.addr = self.URI = self.lnaddr = '' + self.address_help = self.URI_help = self.ln_help = '' self.receive_widget.setVisible(False) self.toggle_qr_button.setEnabled(False) self.toggle_view_button.setEnabled(False) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 17f1ebb0d..e3b7deac0 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -284,13 +284,13 @@ def add_show_address_on_hw_device_button_for_receive_addr(self, wallet: 'Abstrac keystore: 'Hardware_KeyStore', main_window: ElectrumWindow): plugin = keystore.plugin - receive_address_e = main_window.receive_tab.receive_address_e + receive_tab = main_window.receive_tab def show_address(): - addr = str(receive_address_e.text()) + addr = str(receive_tab.addr) keystore.thread.add(partial(plugin.show_address, wallet, addr, keystore)) dev_name = f"{plugin.device} ({keystore.label})" - main_window.receive_tab.toolbar_menu.addAction(read_QIcon("eye1.png"), _("Show address on {}").format(dev_name), show_address) + receive_tab.toolbar_menu.addAction(read_QIcon("eye1.png"), _("Show address on {}").format(dev_name), show_address) def create_handler(self, window: Union[ElectrumWindow, InstallWizard]) -> 'QtHandlerBase': raise NotImplementedError() From 83dbf36d99effb19d79ada01f79951ce44caa84d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 10:07:05 +0200 Subject: [PATCH 0564/1143] follow-up 4647fda04f08b5cf3ce82a7891a2e26bfd53b982 --- electrum/gui/qml/qeinvoicelistmodel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index bdd9b78b2..1bd39dd4d 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -223,8 +223,7 @@ def invoice_to_model(self, invoice: Invoice): return item def get_invoice_list(self): - # disable for now, as QERequestListModel isn't used in UI - return [] #self.wallet.get_unpaid_requests() + return self.wallet.get_unpaid_requests() def get_invoice_for_key(self, key: str): return self.wallet.get_request(key) From 1babc969138613adcce277d7215afcd6eb60c025 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 10:13:36 +0200 Subject: [PATCH 0565/1143] qml: rename Invoices -> Saved Invoices, Receive Requests -> Pending Requests --- electrum/gui/qml/components/Invoices.qml | 2 +- electrum/gui/qml/components/ReceiveRequests.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index a7a28f895..865856ca6 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -14,7 +14,7 @@ Pane { anchors.fill: parent Heading { - text: qsTr('Invoices') + text: qsTr('Saved Invoices') } Frame { diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index 0885d4375..42ac17e72 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -17,7 +17,7 @@ Pane { anchors.fill: parent Heading { - text: qsTr('Receive requests') + text: qsTr('Pending requests') } Frame { From ea46d3c318f4c54cf8308ead0dbb5b213117144e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 10:19:43 +0200 Subject: [PATCH 0566/1143] qml create request: if no address is available, show how to access the list of pending requests. --- electrum/gui/qml/components/WalletMainView.qml | 18 +++--------------- electrum/gui/qml/qewallet.py | 12 ++++++------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index f31b7a692..147056ca0 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -265,21 +265,9 @@ Item { function onRequestCreateSuccess(key) { openRequest(key) } - function onRequestCreateError(code, error) { - if (code == 'ln') { - var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) - dialog.yesClicked.connect(function() { - createRequest(true, false) - }) - } else if (code == 'reuse_addr') { - var dialog = app.messageDialog.createObject(app, {text: error, yesno: true}) - dialog.yesClicked.connect(function() { - createRequest(false, true) - }) - } else { - console.log(error) - var dialog = app.messageDialog.createObject(app, {text: error}) - } + function onRequestCreateError(error) { + console.log(error) + var dialog = app.messageDialog.createObject(app, {text: error}) dialog.open() } } diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index a5641503c..c314a6974 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -55,7 +55,7 @@ def getInstanceFor(cls, wallet): requestStatusChanged = pyqtSignal([str,int], arguments=['key','status']) requestCreateSuccess = pyqtSignal([str], arguments=['key']) - requestCreateError = pyqtSignal([str,str], arguments=['code','error']) + requestCreateError = pyqtSignal([str], arguments=['error']) invoiceStatusChanged = pyqtSignal([str,int], arguments=['key','status']) invoiceCreateSuccess = pyqtSignal() invoiceCreateError = pyqtSignal([str,str], arguments=['code','error']) @@ -621,16 +621,16 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, lightni else: has_lightning = self.wallet.has_lightning() msg = [ - _('No more unused addresses in your wallet.'), - _('All your addresses are used by unpaid requests.'), + _('No address available.'), + _('All your addresses are used in pending requests.'), + _('To see the list, press and hold the Receive button.'), ] - msg.append(_('Do you wish to create a lightning-only request?') if has_lightning else _('Do you want to reuse an address?')) - self.requestCreateError.emit('ln' if has_lightning else 'reuse_addr', ' '.join(msg)) + self.requestCreateError.emit(' '.join(msg)) return key = self.wallet.create_request(amount, message, expiration, addr) except InvoiceError as e: - self.requestCreateError.emit('fatal',_('Error creating payment request') + ':\n' + str(e)) + self.requestCreateError.emit(_('Error creating payment request') + ':\n' + str(e)) return assert key is not None From df44a5c3612d2ff13226be3ee91af3159d95fabd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 10:49:36 +0200 Subject: [PATCH 0567/1143] qml: port over 'show_qr to warn if QR code is missing data' --- electrum/gui/qml/components/ExportTxDialog.qml | 12 ++++++++++++ electrum/gui/qml/components/TxDetails.qml | 9 +++++---- electrum/gui/qml/components/WalletMainView.qml | 13 ++++++++----- electrum/gui/qml/qetxdetails.py | 12 ++++-------- electrum/gui/qml/qewallet.py | 11 ++++------- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 8a555727f..63c362e92 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -12,6 +12,7 @@ ElDialog { property string text_qr // if text_qr is undefined text will be used property string text_help + property string text_warn title: qsTr('Share Transaction') @@ -55,6 +56,17 @@ ElDialog { visible: dialog.text_help text: dialog.text_help } + + InfoTextArea { + Layout.fillWidth: true + Layout.margins: constants.paddingLarge + Layout.topMargin: dialog.text_help + ? 0 + : constants.paddingLarge + visible: dialog.text_warn + text: dialog.text_warn + iconStyle: InfoTextArea.IconStyle.Warn + } } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 8f61e901a..3e050f24c 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -374,9 +374,10 @@ Pane { onClicked: { var msg = '' if (txdetails.isComplete) { - // TODO: iff offline wallet? - // TODO: or also if just temporarily offline? - msg = qsTr('This transaction is complete. Please share it with an online device') + if (!txdetails.isMined && !txdetails.mempoolDepth) // local + // TODO: iff offline wallet? + // TODO: or also if just temporarily offline? + msg = qsTr('This transaction is complete. Please share it with an online device') } else if (txdetails.wallet.isWatchOnly) { msg = qsTr('This transaction should be signed. Present this QR code to the signing device') } else if (txdetails.wallet.isMultisig && txdetails.wallet.walletType != '2fa') { @@ -387,7 +388,7 @@ Pane { } } - app.stack.getRoot().showExport(txdetails.getSerializedTx(false), txdetails.getSerializedTx(true), msg) + app.stack.getRoot().showExport(txdetails.getSerializedTx(), msg) } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 147056ca0..34c487b14 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -51,14 +51,17 @@ Item { } function showExportByTxid(txid, helptext) { - showExport(Daemon.currentWallet.getSerializedTx(txid, false), Daemon.currentWallet.getSerializedTx(txid, true), helptext) + showExport(Daemon.currentWallet.getSerializedTx(txid), helptext) } - function showExport(data, data_qr, helptext) { + function showExport(data, helptext) { var dialog = exportTxDialog.createObject(app, { - text: data, - text_qr: data_qr, - text_help: helptext + text: data[0], + text_qr: data[1], + text_help: helptext, + text_warn: data[2] + ? '' + : qsTr('Warning: Some data (prev txs / "full utxos") was left out of the QR code as it would not fit. This might cause issues if signing offline. As a workaround, try exporting the tx as file or text instead.') }) dialog.open() } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 69587c63d..44bfca552 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -391,11 +391,7 @@ def save(self): self._can_remove = True self.detailsChanged.emit() - @pyqtSlot(result=str) - @pyqtSlot(bool, result=str) - def getSerializedTx(self, for_qr=False): - tx = self._tx - if for_qr: - return tx.to_qr_data()[0] - else: - return str(tx) + @pyqtSlot(result='QVariantList') + def getSerializedTx(self): + txqr = self._tx.to_qr_data() + return [str(self._tx), txqr[0], txqr[1]] diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index c314a6974..466eec50f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -728,11 +728,8 @@ def retrieve_seed(self): self.dataChanged.emit() - @pyqtSlot(str, result=str) - @pyqtSlot(str, bool, result=str) - def getSerializedTx(self, txid, for_qr=False): + @pyqtSlot(str, result='QVariantList') + def getSerializedTx(self, txid): tx = self.wallet.db.get_transaction(txid) - if for_qr: - return tx.to_qr_data()[0] - else: - return str(tx) + txqr = tx.to_qr_data() + return [str(tx), txqr[0], txqr[1]] From a1da0c015039b76783f351966bf3e267cd9ecdf4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 11:01:47 +0200 Subject: [PATCH 0568/1143] qml: virtual keyboard ~20% bigger --- .../gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml index 6091d8bc9..547affc09 100644 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml @@ -55,7 +55,7 @@ KeyboardStyle { } keyboardDesignWidth: 2560 - keyboardDesignHeight: 1200 + keyboardDesignHeight: 1440 keyboardRelativeLeftMargin: 114 / keyboardDesignWidth keyboardRelativeRightMargin: 114 / keyboardDesignWidth keyboardRelativeTopMargin: 13 / keyboardDesignHeight From d99a220c66a43125885c5d7976a8656be12d3721 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 12:32:02 +0200 Subject: [PATCH 0569/1143] qml: add new 'removed_transaction' callback in wallet.py, hook up callback in qewallet and emit balanceChanged events for add_transaction and remove_transaction --- electrum/gui/qml/qeinvoice.py | 4 ---- electrum/gui/qml/qetxdetails.py | 2 +- electrum/gui/qml/qewallet.py | 9 +++++++++ electrum/wallet.py | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 6cedce037..78c84673d 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -261,10 +261,6 @@ def time(self): def expiration(self): return self._effectiveInvoice.exp if self._effectiveInvoice else 0 - @pyqtProperty('quint64', notify=invoiceChanged) - def time(self): - return self._effectiveInvoice.time if self._effectiveInvoice else 0 - statusChanged = pyqtSignal() @pyqtProperty(int, notify=statusChanged) def status(self): diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 44bfca552..3349ce8c7 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -365,6 +365,7 @@ def onBroadcastFailed(self, txid, code, reason): @pyqtSlot() @pyqtSlot(bool) def removeLocalTx(self, confirm = False): + assert self._can_remove txid = self._txid if not confirm: @@ -379,7 +380,6 @@ def removeLocalTx(self, confirm = False): self._wallet.wallet.adb.remove_transaction(txid) self._wallet.wallet.save_db() - self._wallet.historyModel.init_model(True) @pyqtSlot() def save(self): diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 466eec50f..c9211fec7 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -183,6 +183,15 @@ def on_event_new_transaction(self, wallet, tx): self.add_tx_notification(tx) self.addressModel.setDirty() self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after + self.balanceChanged.emit() + + @qt_event_listener + def on_event_removed_transaction(self, wallet, tx): + if wallet == self.wallet: + self._logger.info(f'removed transaction {tx.txid()}') + self.addressModel.setDirty() + self.historyModel.init_model(True) #setDirty() + self.balanceChanged.emit() @qt_event_listener def on_event_wallet_updated(self, wallet): diff --git a/electrum/wallet.py b/electrum/wallet.py index 9d7d68925..99cf5ceef 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -510,6 +510,7 @@ def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction): if not self.tx_is_related(tx): return self.clear_tx_parents_cache() + util.trigger_callback('removed_transaction', self, tx) @event_listener def on_event_adb_added_verified_tx(self, adb, tx_hash): From e476e602470e7c42c238df5352af26971ae362fb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 12:47:43 +0200 Subject: [PATCH 0570/1143] qml: add note regarding validity of qetxdetails instance and reset tx/txid members --- electrum/gui/qml/qetxdetails.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 3349ce8c7..a4ba47971 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -381,6 +381,11 @@ def removeLocalTx(self, confirm = False): self._wallet.wallet.adb.remove_transaction(txid) self._wallet.wallet.save_db() + # NOTE: from here, the tx/txid is unknown and all properties are invalid. + # UI should close TxDetails and avoid interacting with this qetxdetails instance. + self._txid = None + self._tx = None + @pyqtSlot() def save(self): if not self._tx: From b8aa87ded83e2312a18f6af2656c06d1a1bf556c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 13:21:11 +0200 Subject: [PATCH 0571/1143] qml: handle phase-2 lnurl errors from within WalletMainView, add sanity check on the bolt11 invoice we get from the service --- electrum/gui/qml/components/LnurlPayRequestDialog.qml | 8 -------- electrum/gui/qml/components/WalletMainView.qml | 5 +++++ electrum/gui/qml/qeinvoice.py | 11 ++++++++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index ead002b68..468db200a 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -102,12 +102,4 @@ ElDialog { } } - Connections { - target: invoiceParser - function onLnurlError(code, message) { - var dialog = app.messageDialog.createObject(app, { text: message }) - dialog.open() - } - } - } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 34c487b14..a8ce5687b 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -235,9 +235,14 @@ Item { onInvoiceCreateError: console.log(code + ' ' + message) onLnurlRetrieved: { + closeSendDialog() var dialog = lnurlPayDialog.createObject(app, { invoiceParser: invoiceParser }) dialog.open() } + onLnurlError: { + var dialog = app.messageDialog.createObject(app, { text: message }) + dialog.open() + } } Connections { diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 78c84673d..a5de70498 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -620,16 +620,21 @@ def fetch_invoice_task(): params['comment'] = comment coro = callback_lnurl(self._lnurlData['callback_url'], params) fut = asyncio.run_coroutine_threadsafe(coro, self._wallet.wallet.network.asyncio_loop) - self.on_lnurl_invoice(fut.result()) + self.on_lnurl_invoice(amount, fut.result()) except Exception as e: - self.lnurlError.emit('lnurl', repr(e)) + self._logger.error(repr(e)) + self.lnurlError.emit('lnurl', str(e)) threading.Thread(target=fetch_invoice_task).start() - def on_lnurl_invoice(self, invoice): + def on_lnurl_invoice(self, orig_amount, invoice): self._logger.debug('on_lnurl_invoice') self._logger.debug(f'{repr(invoice)}') + # assure no shenanigans with the bolt11 invoice we get back + lninvoice = Invoice.from_bech32(invoice) + assert orig_amount * 1000 == lninvoice.amount_msat + invoice = invoice['pr'] self.recipient = invoice From f7e7b4c9dbf5845db1fde3f08e50fbdfbf971fe8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 11:50:26 +0000 Subject: [PATCH 0572/1143] qml: mempool histogram color bar: flip sign in feerate label I think this is more intuitive as a "greater than" relation sign than to use a signal that the label is for the leftmost point in the coloured bar. As in, "feerates not displayed towards that direction are even higher than this value". --- electrum/gui/qml/components/NetworkOverview.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index f58978d59..b6ffb4e0d 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -128,7 +128,7 @@ Pane { RowLayout { Layout.fillWidth: true Label { - text: '< ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) + text: '> ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) font.pixelSize: constants.fontSizeXSmall color: Material.accentColor } From 771ffa371caddba6f1839b8df65351d4992fca82 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 11:54:02 +0000 Subject: [PATCH 0573/1143] qml: mempool histogram color bar: show tooltips to teach meanings of colours and positions in the bar --- electrum/gui/qml/components/NetworkOverview.qml | 7 +++++++ electrum/gui/qml/qenetwork.py | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index b6ffb4e0d..d655d81bf 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -91,6 +91,13 @@ Pane { Layout.fillWidth: true height: parent.height color: Qt.hsva(2/3-(2/3*(Math.log(Math.min(600, modelData[0]))/Math.log(600))), 0.8, 1, 1) + ToolTip.text: modelData[0] + " sat/vB around depth " + (modelData[2]/1000000).toFixed(2) + " MB" + ToolTip.visible: ma.containsMouse + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + } } } } diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index dfd838eee..6abe6d683 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -126,7 +126,11 @@ def update_histogram(self, histogram): break slot = min(item[1], bytes_limit-bytes_current) bytes_current += slot - capped_histogram.append([max(FEERATE_DEFAULT_RELAY/1000, item[0]), slot]) # clamped to [FEERATE_DEFAULT_RELAY/1000,inf] + capped_histogram.append([ + max(FEERATE_DEFAULT_RELAY/1000, item[0]), # clamped to [FEERATE_DEFAULT_RELAY/1000,inf[ + slot, # width of bucket + bytes_current, # cumulative depth at far end of bucket + ]) # add clamping attributes for the GUI self._fee_histogram = { From b1b71002e67ea01da75b7857141d605e0b1f6aba Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 14:05:14 +0200 Subject: [PATCH 0574/1143] qml: followup b8aa87ded83e2312a18f6af2656c06d1a1bf556c --- electrum/gui/qml/qeinvoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index a5de70498..45475d6c5 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -599,6 +599,7 @@ def on_lnurl(self, lnurldata): @pyqtSlot('quint64', str) def lnurlGetInvoice(self, amount, comment=None): assert self._lnurlData + self._logger.debug(f'{repr(self._lnurlData)}') amount = self.amountOverride.satsInt if self.lnurlData['min_sendable_sat'] != 0: @@ -632,7 +633,7 @@ def on_lnurl_invoice(self, orig_amount, invoice): self._logger.debug(f'{repr(invoice)}') # assure no shenanigans with the bolt11 invoice we get back - lninvoice = Invoice.from_bech32(invoice) + lninvoice = Invoice.from_bech32(invoice['pr']) assert orig_amount * 1000 == lninvoice.amount_msat invoice = invoice['pr'] From 168efa6cb487d878aca3f701016038731048364c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 14:42:03 +0200 Subject: [PATCH 0575/1143] qml: handle scenario for non-lightning wallet scanning lightning invoice with fallback address --- electrum/gui/qml/qeinvoice.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 45475d6c5..7759d268b 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -506,7 +506,6 @@ def validateRecipient(self, recipient): maybe_lightning_invoice = self._bip21['lightning'] except InvalidBitcoinURI as e: self._bip21 = None - self._logger.debug(repr(e)) lninvoice = None maybe_lightning_invoice = maybe_extract_lightning_payment_identifier(maybe_lightning_invoice) @@ -536,12 +535,11 @@ def validateRecipient(self, recipient): if lninvoice: if not self._wallet.wallet.has_lightning(): if not self._bip21: - # TODO: lightning onchain fallback in ln invoice - #self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet')) - self.setValidLightningInvoice(lninvoice) - self.validationSuccess.emit() - # self.clear() - return + if lninvoice.get_address(): + self.setValidLightningInvoice(lninvoice) + self.validationSuccess.emit() + else: + self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.')) else: self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') self.setValidOnchainInvoice(self._bip21['address']) From 56e685feaa56045b2e219c2f20e2a4af2706e645 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 14:25:40 +0200 Subject: [PATCH 0576/1143] invoices: Use the same base method to export invoices and requests. This fixes an inconsistency where the 'expiration' field was relative for invoices, and absolute timestamp for requests. This in turn fixes QML the timer refreshing the request list. In order to prevent any API using that field from being silently broken, the 'expiration' field is renamed as 'expiry'. --- electrum/commands.py | 8 ++--- electrum/gui/qml/qeinvoicelistmodel.py | 6 ++-- electrum/invoices.py | 19 +++++++++++- electrum/wallet.py | 42 +++++--------------------- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index a475fdc36..46cc6ad11 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -975,7 +975,7 @@ async def getunusedaddress(self, wallet: Abstract_Wallet = None): return wallet.get_unused_address() @command('w') - async def add_request(self, amount, memo='', expiration=3600, force=False, wallet: Abstract_Wallet = None): + async def add_request(self, amount, memo='', expiry=3600, force=False, wallet: Abstract_Wallet = None): """Create a payment request, using the first unused address of the wallet. The address will be considered as used after this operation. If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.""" @@ -986,8 +986,8 @@ async def add_request(self, amount, memo='', expiration=3600, force=False, walle else: return False amount = satoshis(amount) - expiration = int(expiration) if expiration else None - key = wallet.create_request(amount, memo, expiration, addr) + expiry = int(expiry) if expiry else None + key = wallet.create_request(amount, memo, expiry, addr) req = wallet.get_request(key) return wallet.export_request(req) @@ -1429,7 +1429,7 @@ def eval_bool(x: str) -> bool: 'addtransaction': (None,'Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet'), 'domain': ("-D", "List of addresses"), 'memo': ("-m", "Description of the request"), - 'expiration': (None, "Time in seconds"), + 'expiry': (None, "Time in seconds"), 'timeout': (None, "Timeout in seconds"), 'force': (None, "Create new address beyond gap limit, if no more addresses are available."), 'pending': (None, "Show only pending requests."), diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 1bd39dd4d..a983708ab 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -15,7 +15,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel): # define listmodel rolemap _ROLE_NAMES=('key', 'is_lightning', 'timestamp', 'date', 'message', 'amount', - 'status', 'status_str', 'address', 'expiration', 'type', 'onchain_fallback', + 'status', 'status_str', 'address', 'expiry', 'type', 'onchain_fallback', 'lightning_invoice') _ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) @@ -132,8 +132,8 @@ def set_status_timer(self): nearest_interval = LN_EXPIRY_NEVER for invoice in self.invoices: if invoice['status'] != PR_EXPIRED: - if invoice['expiration'] > 0 and invoice['expiration'] != LN_EXPIRY_NEVER: - interval = status_update_timer_interval(invoice['timestamp'] + invoice['expiration']) + if invoice['expiry'] > 0 and invoice['expiry'] != LN_EXPIRY_NEVER: + interval = status_update_timer_interval(invoice['timestamp'] + invoice['expiry']) if interval > 0: nearest_interval = nearest_interval if nearest_interval < interval else interval diff --git a/electrum/invoices.py b/electrum/invoices.py index 7aa2e0488..49819807c 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -6,7 +6,7 @@ from .json_db import StoredObject from .i18n import _ -from .util import age, InvoiceError +from .util import age, InvoiceError, format_satoshis from .lnutil import hex_to_bytes from .lnaddr import lndecode, LnAddr from . import constants @@ -222,6 +222,23 @@ def get_id(self) -> str: else: # on-chain return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time) + def as_dict(self, status): + d = { + 'is_lightning': self.is_lightning(), + 'amount_BTC': format_satoshis(self.get_amount_sat()), + 'message': self.message, + 'timestamp': self.get_time(), + 'expiry': self.exp, + 'status': status, + 'status_str': self.get_status_str(status), + 'id': self.get_id(), + 'amount_sat': int(self.get_amount_sat()), + } + if self.is_lightning(): + d['amount_msat'] = self.get_amount_msat() + return d + + @attr.s class Invoice(BaseInvoice): lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] diff --git a/electrum/wallet.py b/electrum/wallet.py index 99cf5ceef..d246c4caa 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2459,34 +2459,20 @@ def get_formatted_request(self, request_id): def export_request(self, x: Request) -> Dict[str, Any]: key = x.get_id() status = self.get_invoice_status(x) - status_str = x.get_status_str(status) - is_lightning = x.is_lightning() - address = x.get_address() - d = { - 'is_lightning': is_lightning, - 'amount_BTC': format_satoshis(x.get_amount_sat()), - 'message': x.message, - 'timestamp': x.get_time(), - 'expiration': x.get_expiration_date(), - 'status': status, - 'status_str': status_str, - 'request_id': key, - "tx_hashes": [] - } - if is_lightning: + d = x.as_dict(status) + d['request_id'] = d.pop('id') + if x.is_lightning(): d['rhash'] = x.rhash d['lightning_invoice'] = self.get_bolt11_invoice(x) - d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_receive'] = self.lnworker.can_receive_invoice(x) - if address: - d['amount_sat'] = int(x.get_amount_sat()) + if address := x.get_address(): d['address'] = address d['URI'] = self.get_request_URI(x) # if request was paid onchain, add relevant fields # note: addr is reused when getting paid on LN! so we check for that. _, conf, tx_hashes = self._is_onchain_invoice_paid(x) - if not is_lightning or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID: + if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID: if conf is not None: d['confirmations'] = conf d['tx_hashes'] = tx_hashes @@ -2496,27 +2482,15 @@ def export_request(self, x: Request) -> Dict[str, Any]: def export_invoice(self, x: Invoice) -> Dict[str, Any]: key = x.get_id() status = self.get_invoice_status(x) - status_str = x.get_status_str(status) - is_lightning = x.is_lightning() - d = { - 'is_lightning': is_lightning, - 'amount_BTC': format_satoshis(x.get_amount_sat()), - 'message': x.message, - 'timestamp': x.time, - 'expiration': x.exp, - 'status': status, - 'status_str': status_str, - 'invoice_id': key, - } - if is_lightning: + d = x.as_dict(status) + d['invoice_id'] = d.pop('id') + if x.is_lightning(): d['lightning_invoice'] = x.lightning_invoice - d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_pay'] = self.lnworker.can_pay_invoice(x) else: amount_sat = x.get_amount_sat() assert isinstance(amount_sat, (int, str, type(None))) - d['amount_sat'] = amount_sat d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()] if x.bip70: d['bip70'] = x.bip70 From 0f541be6f11a372d202c99476e6d051184006bba Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 13:03:26 +0000 Subject: [PATCH 0577/1143] log a warning if asserts are disabled Maybe we should refuse to start, if launched with the main script. But note that __debug__ is False on Android atm, as python is launched with -OO. --- electrum/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/electrum/__init__.py b/electrum/__init__.py index cea6ac59b..f806c548b 100644 --- a/electrum/__init__.py +++ b/electrum/__init__.py @@ -29,6 +29,11 @@ class GuiImportError(ImportError): from .transaction import Transaction from .plugin import BasePlugin from .commands import Commands, known_commands +from .logging import get_logger __version__ = ELECTRUM_VERSION + +_logger = get_logger(__name__) +if not __debug__: + _logger.warning(f"__debug__ is False. running with asserts disabled!") From cf2ba2a5bd7c971756187ff11d1a4a1db91b3efa Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 15:03:44 +0200 Subject: [PATCH 0578/1143] qml: replace assert by exception --- electrum/gui/qml/qeinvoice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 7759d268b..894f880b3 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -632,10 +632,10 @@ def on_lnurl_invoice(self, orig_amount, invoice): # assure no shenanigans with the bolt11 invoice we get back lninvoice = Invoice.from_bech32(invoice['pr']) - assert orig_amount * 1000 == lninvoice.amount_msat + if orig_amount * 1000 != lninvoice.amount_msat: + raise Exception('Unexpected amount in invoice, differs from lnurl-pay specified amount') - invoice = invoice['pr'] - self.recipient = invoice + self.recipient = invoice['pr'] @pyqtSlot() def save_invoice(self): From 244ead2624cb928bb26867a53bb6174016996109 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 15:04:27 +0200 Subject: [PATCH 0579/1143] qml: auto-pay if entering InvoiceDialog from a lnurl-pay dialog --- electrum/gui/qml/components/InvoiceDialog.qml | 5 +++++ electrum/gui/qml/components/LnurlPayRequestDialog.qml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 16ba9a645..3fdd19db6 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -456,5 +456,10 @@ ElDialog { } else if (invoice.amount.isMax) { amountMax.checked = true } + if (invoice.lnurlData) { + // we arrive from a lnurl-pay confirm dialog where the user already indicated the intent to pay. + if (invoice.canPay) + doPay() + } } } diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index 468db200a..e815729f7 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -92,7 +92,7 @@ ElDialog { FlatButton { Layout.topMargin: constants.paddingLarge Layout.fillWidth: true - text: qsTr('Proceed') + text: qsTr('Pay') icon.source: '../../icons/confirmed.png' enabled: valid onClicked: { From ec2f903573fa65bcf1926781b018c7d12e4d36ac Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 15:38:53 +0200 Subject: [PATCH 0580/1143] qml: force Pin dialog above other dialogs --- electrum/gui/qml/components/Pin.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index 180c4f0c9..fb9b9a62d 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -12,6 +12,7 @@ ElDialog { title: qsTr('PIN') iconSource: '../../../icons/lock.png' + z: 1000 width: parent.width * 3/4 From 2bdc303662bee6034ce563eebf0cb3e129db756b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 15:39:29 +0200 Subject: [PATCH 0581/1143] qml: keep lnurlData even after bolt11 has been retrieved, add isLnurlPay property and save bolt11 before triggering pay --- electrum/gui/qml/components/InvoiceDialog.qml | 8 ++++++-- electrum/gui/qml/qeinvoice.py | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 3fdd19db6..9757c5165 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -456,10 +456,14 @@ ElDialog { } else if (invoice.amount.isMax) { amountMax.checked = true } - if (invoice.lnurlData) { + if (invoice.isLnurlPay) { // we arrive from a lnurl-pay confirm dialog where the user already indicated the intent to pay. - if (invoice.canPay) + if (invoice.canPay) { + if (invoice_key == '') { + invoice.save_invoice() + } doPay() + } } } } diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 894f880b3..c70381048 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -203,10 +203,8 @@ def recipient(self): @recipient.setter def recipient(self, recipient: str): - #if self._recipient != recipient: self.canPay = False self._recipient = recipient - self._lnurlData = None self.amountOverride = QEAmount() if recipient: self.validateRecipient(recipient) @@ -216,6 +214,10 @@ def recipient(self, recipient: str): def lnurlData(self): return self._lnurlData + @pyqtProperty(bool, notify=lnurlRetrieved) + def isLnurlPay(self): + return not self._lnurlData is None + @pyqtProperty(str, notify=invoiceChanged) def message(self): return self._effectiveInvoice.message if self._effectiveInvoice else '' From 478937b8d2aaeb3ba53db286de5a61aa789145b6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 31 Mar 2023 15:46:51 +0200 Subject: [PATCH 0582/1143] make flake8 not not happy --- electrum/gui/qml/qeinvoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index c70381048..32c09c0db 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -216,7 +216,7 @@ def lnurlData(self): @pyqtProperty(bool, notify=lnurlRetrieved) def isLnurlPay(self): - return not self._lnurlData is None + return self._lnurlData is not None @pyqtProperty(str, notify=invoiceChanged) def message(self): From c98b9e8d7b58002c8d45799293fbbd394ae31de3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 15:39:54 +0200 Subject: [PATCH 0583/1143] qml: dashboard Balance details are shown if the user presses and holds the balance area; the idea is that this should be less chaotic than if the popup is triggerred by a simple click. However, we might as well try with a simple click, because we already do it with transaction details; I am not sure what is the best option, we should try both. This also makes 'new channel' and 'swap' buttons available from theBalance details, so that users do not need to visit the channels list. --- .../gui/qml/components/BalanceDetails.qml | 227 ++++++++++++++++++ electrum/gui/qml/components/Channels.qml | 54 ----- electrum/gui/qml/components/WalletDetails.qml | 88 ------- .../components/controls/BalanceSummary.qml | 35 +-- 4 files changed, 238 insertions(+), 166 deletions(-) create mode 100644 electrum/gui/qml/components/BalanceDetails.qml diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml new file mode 100644 index 000000000..1500181db --- /dev/null +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -0,0 +1,227 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +Pane { + id: rootItem + objectName: 'BalanceDetails' + + padding: 0 + + property bool _is2fa: Daemon.currentWallet && Daemon.currentWallet.walletType == '2fa' + + function enableLightning() { + var dialog = app.messageDialog.createObject(rootItem, + {'text': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) + dialog.yesClicked.connect(function() { + Daemon.currentWallet.enableLightning() + }) + dialog.open() + } + + function deleteWallet() { + var dialog = app.messageDialog.createObject(rootItem, + {'text': qsTr('Really delete this wallet?'), 'yesno': true}) + dialog.yesClicked.connect(function() { + Daemon.checkThenDeleteWallet(Daemon.currentWallet) + }) + dialog.open() + } + + function changePassword() { + // trigger dialog via wallet (auth then signal) + Daemon.startChangePassword() + } + + function importAddressesKeys() { + var dialog = importAddressesKeysDialog.createObject(rootItem) + dialog.open() + } + + ColumnLayout { + id: rootLayout + anchors.fill: parent + spacing: 0 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + + contentHeight: flickableRoot.height + clip:true + interactive: height < contentHeight + + Pane { + id: flickableRoot + width: parent.width + padding: constants.paddingLarge + + ColumnLayout { + width: parent.width + spacing: constants.paddingLarge + + Heading { + text: qsTr('Wallet balance') + } + + Piechart { + id: piechart + visible: Daemon.currentWallet.totalBalance.satsInt > 0 + Layout.preferredWidth: parent.width + implicitHeight: 220 // TODO: sane value dependent on screen + innerOffset: 6 + function updateSlices() { + var totalB = Daemon.currentWallet.totalBalance.satsInt + var onchainB = Daemon.currentWallet.confirmedBalance.satsInt + var frozenB = Daemon.currentWallet.frozenBalance.satsInt + var lnB = Daemon.currentWallet.lightningBalance.satsInt + piechart.slices = [ + { v: lnB/totalB, color: constants.colorPiechartLightning, text: 'Lightning' }, + { v: (onchainB-frozenB)/totalB, color: constants.colorPiechartOnchain, text: 'On-chain' }, + { v: frozenB/totalB, color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, + ] + } + } + + GridLayout { + Layout.alignment: Qt.AlignHCenter + visible: Daemon.currentWallet + columns: 3 + + Item { + visible: !Daemon.currentWallet.totalBalance.isEmpty + Layout.preferredWidth: 1; Layout.preferredHeight: 1 + } + Label { + visible: !Daemon.currentWallet.totalBalance.isEmpty + text: qsTr('Total') + } + FormattedAmount { + visible: !Daemon.currentWallet.totalBalance.isEmpty + amount: Daemon.currentWallet.totalBalance + } + + Rectangle { + visible: !Daemon.currentWallet.lightningBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartLightning + } + Label { + visible: !Daemon.currentWallet.lightningBalance.isEmpty + text: qsTr('Lightning') + + } + FormattedAmount { + amount: Daemon.currentWallet.lightningBalance + visible: !Daemon.currentWallet.lightningBalance.isEmpty + } + + Rectangle { + visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartOnchain + } + Label { + visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + text: qsTr('On-chain') + + } + FormattedAmount { + amount: Daemon.currentWallet.confirmedBalance + visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + } + + Rectangle { + visible: !Daemon.currentWallet.frozenBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartFrozen + } + Label { + visible: !Daemon.currentWallet.frozenBalance.isEmpty + text: qsTr('Frozen') + } + FormattedAmount { + amount: Daemon.currentWallet.frozenBalance + visible: !Daemon.currentWallet.frozenBalance.isEmpty + } + } + } + } + } + + ButtonContainer { + Layout.fillWidth: true + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Lightning swap'); + visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 + icon.source: Qt.resolvedUrl('../../icons/update.png') + onClicked: { + var swaphelper = app.swaphelper.createObject(app) + swaphelper.swapStarted.connect(function() { + var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + dialog.open() + }) + var dialog = swapDialog.createObject(rootItem, { swaphelper: swaphelper }) + dialog.open() + } + } + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Open Channel') + visible: Daemon.currentWallet.isLightning + onClicked: { + var dialog = openChannelDialog.createObject(rootItem) + dialog.open() + } + icon.source: '../../icons/lightning.png' + } + + } + + } + + Component { + id: swapDialog + SwapDialog { + onClosed: destroy() + } + } + + Component { + id: swapProgressDialog + SwapProgressDialog { + onClosed: destroy() + } + } + + Component { + id: openChannelDialog + OpenChannelDialog { + onClosed: destroy() + } + } + + Connections { + target: Daemon.currentWallet + function onBalanceChanged() { + piechart.updateSlices() + } + } + + Component.onCompleted: { + piechart.updateSlices() + } + +} diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index c75661bed..c186b7cf1 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -117,60 +117,6 @@ Pane { } } - ButtonContainer { - Layout.fillWidth: true - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Swap'); - visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 - icon.source: Qt.resolvedUrl('../../icons/update.png') - onClicked: { - var swaphelper = app.swaphelper.createObject(app) - swaphelper.swapStarted.connect(function() { - var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) - dialog.open() - }) - var dialog = swapDialog.createObject(root, { swaphelper: swaphelper }) - dialog.open() - } - } - - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Open Channel') - onClicked: { - var dialog = openChannelDialog.createObject(root) - dialog.open() - } - icon.source: '../../icons/lightning.png' - } - - } - - } - - Component { - id: swapDialog - SwapDialog { - onClosed: destroy() - } - } - - Component { - id: swapProgressDialog - SwapProgressDialog { - onClosed: destroy() - } - } - - Component { - id: openChannelDialog - OpenChannelDialog { - onClosed: destroy() - } - } Component { id: importChannelBackupDialog diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 7e5f60c56..aa750fa35 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -135,91 +135,6 @@ Pane { } } - Piechart { - id: piechart - visible: Daemon.currentWallet.totalBalance.satsInt > 0 - Layout.preferredWidth: parent.width - implicitHeight: 220 // TODO: sane value dependent on screen - innerOffset: 6 - function updateSlices() { - var totalB = Daemon.currentWallet.totalBalance.satsInt - var onchainB = Daemon.currentWallet.confirmedBalance.satsInt - var frozenB = Daemon.currentWallet.frozenBalance.satsInt - var lnB = Daemon.currentWallet.lightningBalance.satsInt - piechart.slices = [ - { v: lnB/totalB, color: constants.colorPiechartLightning, text: 'Lightning' }, - { v: (onchainB-frozenB)/totalB, color: constants.colorPiechartOnchain, text: 'On-chain' }, - { v: frozenB/totalB, color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, - ] - } - } - - GridLayout { - Layout.alignment: Qt.AlignHCenter - visible: Daemon.currentWallet - columns: 3 - - Item { - visible: !Daemon.currentWallet.totalBalance.isEmpty - Layout.preferredWidth: 1; Layout.preferredHeight: 1 - } - Label { - visible: !Daemon.currentWallet.totalBalance.isEmpty - text: qsTr('Total') - } - FormattedAmount { - visible: !Daemon.currentWallet.totalBalance.isEmpty - amount: Daemon.currentWallet.totalBalance - } - - Rectangle { - visible: !Daemon.currentWallet.lightningBalance.isEmpty - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - color: constants.colorPiechartLightning - } - Label { - visible: !Daemon.currentWallet.lightningBalance.isEmpty - text: qsTr('Lightning') - - } - FormattedAmount { - amount: Daemon.currentWallet.lightningBalance - visible: !Daemon.currentWallet.lightningBalance.isEmpty - } - - Rectangle { - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - color: constants.colorPiechartOnchain - } - Label { - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty - text: qsTr('On-chain') - - } - FormattedAmount { - amount: Daemon.currentWallet.confirmedBalance - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty - } - - Rectangle { - visible: !Daemon.currentWallet.frozenBalance.isEmpty - Layout.preferredWidth: constants.iconSizeXSmall - Layout.preferredHeight: constants.iconSizeXSmall - color: constants.colorPiechartFrozen - } - Label { - visible: !Daemon.currentWallet.frozenBalance.isEmpty - text: qsTr('Frozen') - } - FormattedAmount { - amount: Daemon.currentWallet.frozenBalance - visible: !Daemon.currentWallet.frozenBalance.isEmpty - } - } - GridLayout { Layout.preferredWidth: parent.width visible: Daemon.currentWallet @@ -591,9 +506,6 @@ Pane { }) dialog.open() } - function onBalanceChanged() { - piechart.updateSlices() - } function onSeedRetrieved() { seedText.visible = true showSeedText.visible = false diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 6c2571cb5..cb0d78b2d 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -23,17 +23,6 @@ Item { } } - state: 'fiat' - - states: [ - State { - name: 'fiat' - }, - State { - name: 'btc' - } - ] - TextHighlightPane { id: balancePane leftPadding: constants.paddingXLarge @@ -63,28 +52,28 @@ Item { } Item { - visible: Daemon.fx.enabled && root.state == 'fiat' + visible: Daemon.fx.enabled // attempt at making fiat state as tall as btc state: Layout.preferredHeight: fontMetrics.lineSpacing * 2 + balanceLayout.rowSpacing + 2 Layout.preferredWidth: 1 } Label { Layout.alignment: Qt.AlignRight - visible: Daemon.fx.enabled && root.state == 'fiat' + visible: Daemon.fx.enabled font.pixelSize: constants.fontSizeLarge font.family: FixedFont color: constants.mutedForeground text: formattedTotalBalanceFiat } Label { - visible: Daemon.fx.enabled && root.state == 'fiat' + visible: Daemon.fx.enabled font.pixelSize: constants.fontSizeLarge color: constants.mutedForeground text: Daemon.fx.fiatCurrency } RowLayout { - visible: Daemon.currentWallet.isLightning && root.state == 'btc' + visible: Daemon.currentWallet.isLightning Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall @@ -97,20 +86,20 @@ Item { } } Label { - visible: Daemon.currentWallet.isLightning && root.state == 'btc' + visible: Daemon.currentWallet.isLightning Layout.alignment: Qt.AlignRight text: formattedLightningBalance font.family: FixedFont } Label { - visible: Daemon.currentWallet.isLightning && root.state == 'btc' + visible: Daemon.currentWallet.isLightning font.pixelSize: constants.fontSizeSmall color: Material.accentColor text: Config.baseUnit } RowLayout { - visible: root.state == 'btc' + visible: Daemon.currentWallet.isLightning Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall @@ -124,13 +113,13 @@ Item { } Label { id: formattedConfirmedBalanceLabel - visible: root.state == 'btc' + visible: Daemon.currentWallet.isLightning Layout.alignment: Qt.AlignRight text: formattedConfirmedBalance font.family: FixedFont } Label { - visible: root.state == 'btc' + visible: Daemon.currentWallet.isLightning font.pixelSize: constants.fontSizeSmall color: Material.accentColor text: Config.baseUnit @@ -157,8 +146,8 @@ Item { MouseArea { anchors.fill: parent - onClicked: { - root.state = root.state == 'fiat' && Daemon.currentWallet.isLightning ? 'btc' : 'fiat' + onPressAndHold: { + app.stack.push(Qt.resolvedUrl('../BalanceDetails.qml')) } } @@ -174,8 +163,6 @@ Item { target: Daemon function onWalletLoaded() { setBalances() - if (!Daemon.currentWallet.isLightning) - root.state = 'fiat' } } From 847c8d4941931a5e0b8406457c79a752f7b02577 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 16:37:49 +0200 Subject: [PATCH 0584/1143] balance details: use onClicked event. Early return if balance is not available --- electrum/gui/qml/components/controls/BalanceSummary.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index cb0d78b2d..cef653dc6 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -146,7 +146,8 @@ Item { MouseArea { anchors.fill: parent - onPressAndHold: { + onClicked: { + if(Daemon.currentWallet.synchronizing || Network.server_status != 'connected') return app.stack.push(Qt.resolvedUrl('../BalanceDetails.qml')) } } From 0e5464ca13ce2f993107b4a293982ea4bfc434b5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 16:00:19 +0000 Subject: [PATCH 0585/1143] android build: enable asserts, and add sanity-check for it Note that 0f541be6f11a372d202c99476e6d051184006bba added a warning log if asserts are disabled. It is intentional that these two things are in separate files: We always want to log that warning, even if someone is using electrum as a library. However, in that latter case, I think it's fine not to sys.exit(), but leave the decision up to the library user. Similar thinking when running from source: let's log the warning but don't sys.exit(). --- contrib/android/Dockerfile | 4 ++-- run_electrum | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index d25466e08..013bdf6df 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,8 +179,8 @@ RUN cd /opt \ && git remote add sombernight https://github.com/SomberNight/python-for-android \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ - # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "3c2750795ba93aa1a3e513a13c2ea2ac5bddba17^{commit}" \ + # commit: from branch sombernight/electrum_20210421d (note: careful with force-pushing! see #8162) + && git checkout "ec82acf894822373ae88247658a233c77e76f879^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars diff --git a/run_electrum b/run_electrum index c587d7bed..7c363d539 100755 --- a/run_electrum +++ b/run_electrum @@ -44,8 +44,9 @@ script_dir = os.path.dirname(os.path.realpath(__file__)) is_pyinstaller = getattr(sys, 'frozen', False) is_android = 'ANDROID_DATA' in os.environ is_appimage = 'APPIMAGE' in os.environ +is_binary_distributable = is_pyinstaller or is_android or is_appimage # is_local: unpacked tar.gz but not pip installed, or git clone -is_local = (not is_pyinstaller and not is_android and not is_appimage +is_local = (not is_binary_distributable and os.path.exists(os.path.join(script_dir, "electrum.desktop"))) is_git_clone = is_local and os.path.exists(os.path.join(script_dir, ".git")) @@ -62,6 +63,19 @@ if is_pyinstaller: # causes ImportErrors and other runtime failures). (see #4072) _file = open(sys.executable, 'rb') +if is_binary_distributable: + # Ensure that asserts are enabled. + # Code *should not rely* on asserts being enabled. In particular, safety and security checks should + # always explicitly raise exceptions. However, this rule is mistakenly broken occasionally... + # In case we are a binary build, we know for a fact that we want the asserts, so enforce them. + # When running from source, defer to the user. (a warning is logged in __init__.py) + try: + assert False + except AssertionError: + pass + else: + sys.exit("Error: Running with asserts disabled, in a binary distributable! Please check build settings.") + def check_imports(): # pure-python dependencies need to be imported here for pyinstaller From d4aeeaf541915402efbc4eef5f49e2ab508b957c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 18:35:46 +0200 Subject: [PATCH 0586/1143] follow-up c98b9e8d7b58002c8d45799293fbbd394ae31de3 (unintended deletion) --- electrum/gui/qml/components/Channels.qml | 54 ++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index c186b7cf1..c75661bed 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -117,6 +117,60 @@ Pane { } } + ButtonContainer { + Layout.fillWidth: true + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Swap'); + visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 + icon.source: Qt.resolvedUrl('../../icons/update.png') + onClicked: { + var swaphelper = app.swaphelper.createObject(app) + swaphelper.swapStarted.connect(function() { + var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + dialog.open() + }) + var dialog = swapDialog.createObject(root, { swaphelper: swaphelper }) + dialog.open() + } + } + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Open Channel') + onClicked: { + var dialog = openChannelDialog.createObject(root) + dialog.open() + } + icon.source: '../../icons/lightning.png' + } + + } + + } + + Component { + id: swapDialog + SwapDialog { + onClosed: destroy() + } + } + + Component { + id: swapProgressDialog + SwapProgressDialog { + onClosed: destroy() + } + } + + Component { + id: openChannelDialog + OpenChannelDialog { + onClosed: destroy() + } + } Component { id: importChannelBackupDialog From 986955a6e8634e9919352bdc76f80afa6f9a0430 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 07:56:10 +0200 Subject: [PATCH 0587/1143] qml: allow user to delete invoices and requests from the list screen also, delete expired requests before loading list --- electrum/gui/qml/components/Invoices.qml | 33 +++++++++++++++++ .../gui/qml/components/ReceiveRequests.qml | 36 ++++++++++++++++--- .../gui/qml/components/WalletMainView.qml | 1 + electrum/gui/qml/qewallet.py | 11 +++--- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 865856ca6..5535837b1 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -9,6 +9,7 @@ import "controls" Pane { id: root + property string selected_key ColumnLayout { anchors.fill: parent @@ -39,7 +40,11 @@ Pane { dialog.invoiceAmountChanged.connect(function () { Daemon.currentWallet.invoiceModel.init_model() }) + selected_key = '' } + onPressAndHold: { + selected_key = model.key + } } } @@ -65,5 +70,33 @@ Pane { ScrollIndicator.vertical: ScrollIndicator { } } } + ButtonContainer { + Layout.fillWidth: true + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Delete') + icon.source: '../../icons/delete.png' + visible: selected_key != '' + onClicked: { + Daemon.currentWallet.delete_invoice(selected_key) + selected_key = '' + } + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('View') + icon.source: '../../icons/tab_receive.png' + visible: selected_key != '' + onClicked: { + var dialog = app.stack.getRoot().openInvoice(selected_key) + dialog.invoiceAmountChanged.connect(function () { + Daemon.currentWallet.invoiceModel.init_model() + }) + selected_key = '' + } + } + } } } diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index 42ac17e72..3de544f5b 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -12,6 +12,7 @@ import "controls" Pane { id: root objectName: 'ReceiveRequests' + property string selected_key ColumnLayout { anchors.fill: parent @@ -38,14 +39,14 @@ Pane { model: Daemon.currentWallet.requestModel delegate: InvoiceDelegate { onClicked: { - // TODO: only open unpaid? - if (model.status == Invoice.Unpaid) { - app.stack.getRoot().openRequest(model.key) - } + app.stack.getRoot().openRequest(model.key) + selected_key = '' } + onPressAndHold: { + selected_key = model.key + } } } - add: Transition { NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 } NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 } @@ -68,5 +69,30 @@ Pane { ScrollIndicator.vertical: ScrollIndicator { } } } + ButtonContainer { + Layout.fillWidth: true + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Delete') + icon.source: '../../icons/delete.png' + visible: selected_key != '' + onClicked: { + Daemon.currentWallet.delete_request(selected_key) + selected_key = '' + } + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('View') + icon.source: '../../icons/tab_receive.png' + visible: selected_key != '' + onClicked: { + app.stack.getRoot().openRequest(selected_key) + selected_key = '' + } + } + } } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index a8ce5687b..c0a1cbfff 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -192,6 +192,7 @@ Item { dialog.open() } onPressAndHold: { + Daemon.currentWallet.delete_expired_requests() app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) } } diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index c9211fec7..5de412f1b 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -608,17 +608,18 @@ def pay_thread(): threading.Thread(target=pay_thread, daemon=True).start() - + @pyqtSlot() + def delete_expired_requests(self): + keys = self.wallet.delete_expired_requests() + for key in keys: + self.requestModel.delete_invoice(key) @pyqtSlot(QEAmount, str, int) @pyqtSlot(QEAmount, str, int, bool) @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning_only: bool = False, reuse_address: bool = False): - # delete expired_requests - keys = self.wallet.delete_expired_requests() - for key in keys: - self.requestModel.delete_invoice(key) + self.delete_expired_requests() try: amount = amount.satsInt addr = self.wallet.get_unused_address() From fc6cbb39ea355fc8562fe4a4df699930eeb060ff Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 31 Mar 2023 22:17:53 +0000 Subject: [PATCH 0588/1143] qml: QEConfig.formatMilliSats to use config.format_amount --- electrum/gui/qml/qeconfig.py | 21 +++++++++++---------- electrum/simple_config.py | 19 ++++++++++++++----- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 007a0e3d0..6dc934221 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -1,5 +1,6 @@ import copy from decimal import Decimal +from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject @@ -11,10 +12,14 @@ from .qetypes import QEAmount from .auth import AuthMixin, auth_protect +if TYPE_CHECKING: + from electrum.simple_config import SimpleConfig + + class QEConfig(AuthMixin, QObject): _logger = get_logger(__name__) - def __init__(self, config, parent=None): + def __init__(self, config: 'SimpleConfig', parent=None): super().__init__(parent) self.config = config @@ -213,15 +218,11 @@ def formatMilliSats(self, amount, with_unit=False): msats = amount.msatsInt else: return '---' - - s = format_satoshis(msats/1000, - decimal_point=self.decimal_point(), - precision=3) - return s - #if with_unit: - #return self.config.format_amount_and_units(msats) - #else: - #return self.config.format_amount(satoshis) + precision = 3 # config.amt_precision_post_satoshi is not exposed in preferences + if with_unit: + return self.config.format_amount_and_units(msats/1000, precision=precision) + else: + return self.config.format_amount(msats/1000, precision=precision) # TODO delegate all this to config.py/util.py def decimal_point(self): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 0ecdfa6b7..d57418b5f 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -677,19 +677,28 @@ def get_netaddress(self, key: str) -> Optional[NetAddress]: except: pass - def format_amount(self, x, is_diff=False, whitespaces=False): + def format_amount( + self, + amount_sat, + *, + is_diff=False, + whitespaces=False, + precision=None, + ) -> str: + if precision is None: + precision = self.amt_precision_post_satoshi return format_satoshis( - x, + amount_sat, num_zeros=self.num_zeros, decimal_point=self.decimal_point, is_diff=is_diff, whitespaces=whitespaces, - precision=self.amt_precision_post_satoshi, + precision=precision, add_thousands_sep=self.amt_add_thousands_sep, ) - def format_amount_and_units(self, amount): - return self.format_amount(amount) + ' '+ self.get_base_unit() + def format_amount_and_units(self, *args, **kwargs) -> str: + return self.format_amount(*args, **kwargs) + ' ' + self.get_base_unit() def format_fee_rate(self, fee_rate): return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + ' sat/byte' From fb47346ed3bcf5c712347c69efa77a41edc880d4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 09:43:52 +0200 Subject: [PATCH 0589/1143] follow-up 2cbb16ae4b. fixes #8290 --- electrum/gui/qml/qewallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 5de412f1b..5e723acff 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -66,7 +66,7 @@ def getInstanceFor(cls, wallet): broadcastSucceeded = pyqtSignal([str], arguments=['txid']) broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason']) saveTxSuccess = pyqtSignal([str], arguments=['txid']) - saveTxError = pyqtSignal([str,str], arguments=['txid', 'code', 'message']) + saveTxError = pyqtSignal([str,str,str], arguments=['txid', 'code', 'message']) importChannelBackupFailed = pyqtSignal([str], arguments=['message']) labelsUpdated = pyqtSignal() otpRequested = pyqtSignal() From 54bb42f82cefe95b0156fc48255592a6f41b5b7c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 10:37:38 +0200 Subject: [PATCH 0590/1143] adb: take locks in get_balance. fixes #8200 --- electrum/address_synchronizer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index c8c261bdd..ea969efbf 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -817,6 +817,8 @@ def get_addr_received(self, address): received, sent = self.get_addr_io(address) return sum([value for height, pos, value, is_cb in received.values()]) + @with_lock + @with_transaction_lock @with_local_height_cached def get_balance(self, domain, *, excluded_addresses: Set[str] = None, excluded_coins: Set[str] = None) -> Tuple[int, int, int]: From f396d151469f22eeb6e23edc62d199428c6751f0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 12:03:45 +0200 Subject: [PATCH 0591/1143] qml: (clarity) use separate slots for sign and sign_and_broadcast. --- electrum/gui/qml/components/TxDetails.qml | 7 +++---- electrum/gui/qml/qetxdetails.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 3e050f24c..d2561e012 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -460,11 +460,10 @@ Pane { wallet: Daemon.currentWallet txid: dialog.txid } - onTxaccepted: { root.rawtx = rbffeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign(true) + txdetails.sign_and_broadcast() // close txdetails? } else { var dialog = app.messageDialog.createObject(app, { @@ -491,7 +490,7 @@ Pane { // replaces parent tx with cpfp tx root.rawtx = cpfpfeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign(true) + txdetails.sign_and_broadcast() // close txdetails? } else { var dialog = app.messageDialog.createObject(app, { @@ -517,7 +516,7 @@ Pane { onTxaccepted: { root.rawtx = txcanceller.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign(true) + txdetails.sign_and_broadcast() // close txdetails? } else { var dialog = app.messageDialog.createObject(app, { diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index a4ba47971..ed032c91b 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -299,8 +299,14 @@ def update_mined_status(self, tx_mined_info: TxMinedInfo): self._short_id = tx_mined_info.short_id() or "" @pyqtSlot() - @pyqtSlot(bool) - def sign(self, broadcast = False): + def sign_and_broadcast(self): + self._sign(broadcast=True) + + @pyqtSlot() + def sign(self): + self._sign(broadcast=False) + + def _sign(self, broadcast): # TODO: connecting/disconnecting signal handlers here is hmm try: self._wallet.transactionSigned.disconnect(self.onSigned) From ad1829887803d571875e615d5bd2637fafc532c3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 12:06:59 +0200 Subject: [PATCH 0592/1143] qml: Give user feedback after bumping the fee. This is better than nothing, but not ideal. This window should have a general purpose 'userinfo' field, like InvoiceDialog, that would also display 'Broadcasting...' while the tx is being broadcast. Note that in order to bump the fee again, the user will have to leave this window and open it again. --- electrum/gui/qml/components/TxDetails.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index d2561e012..a9f8aeded 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -139,6 +139,7 @@ Pane { ? 1 : 2 Label { + id: bumpfeeinfo Layout.fillWidth: true text: qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel ? qsTr('You can increase fees to speed up the transaction, or cancel this transaction') @@ -449,6 +450,10 @@ Pane { var dialog = app.messageDialog.createObject(app, { text: message }) dialog.open() } + function onBroadcastSucceeded() { + bumpfeeinfo.text = qsTr('Transaction was broadcast successfully') + actionButtonsLayout.visible = false + } } Component { @@ -464,7 +469,6 @@ Pane { root.rawtx = rbffeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign_and_broadcast() - // close txdetails? } else { var dialog = app.messageDialog.createObject(app, { text: qsTr('Transaction fee updated.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') @@ -491,7 +495,6 @@ Pane { root.rawtx = cpfpfeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign_and_broadcast() - // close txdetails? } else { var dialog = app.messageDialog.createObject(app, { text: qsTr('CPFP fee bump transaction created.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') @@ -517,7 +520,6 @@ Pane { root.rawtx = txcanceller.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign_and_broadcast() - // close txdetails? } else { var dialog = app.messageDialog.createObject(app, { text: qsTr('Cancel transaction created.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') From d6cbaaa2e9ac8cb2ad8885e432595efe8184758b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 12:26:46 +0200 Subject: [PATCH 0593/1143] qml InvoiceDialog: show invoice type in the title, fallback address in the details --- electrum/gui/qml/components/InvoiceDialog.qml | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 9757c5165..dc1bfa1fc 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -16,7 +16,7 @@ ElDialog { signal doPay signal invoiceAmountChanged - title: qsTr('Invoice') + title: invoice.invoiceType == Invoice.OnchainInvoice ? qsTr('On-chain Invoice') : qsTr('Lightning Invoice') iconSource: Qt.resolvedUrl('../../icons/tab_send.png') padding: 0 @@ -66,33 +66,6 @@ ElDialog { : InfoTextArea.IconStyle.Info } - Label { - text: qsTr('Type') - color: Material.accentColor - } - - RowLayout { - Layout.fillWidth: true - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - source: invoice.invoiceType == Invoice.LightningInvoice - ? "../../icons/lightning.png" - : "../../icons/bitcoin.png" - } - - Label { - text: invoice.invoiceType == Invoice.OnchainInvoice - ? qsTr('On chain') - : invoice.invoiceType == Invoice.LightningInvoice - ? invoice.address - ? qsTr('Lightning with on-chain fallback address') - : qsTr('Lightning') - : '' - Layout.fillWidth: true - } - } - Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall @@ -104,7 +77,6 @@ ElDialog { TextHighlightPane { Layout.columnSpan: 2 Layout.fillWidth: true - visible: invoice.invoiceType == Invoice.OnchainInvoice leftPadding: constants.paddingMedium @@ -388,6 +360,27 @@ ElDialog { } } } + + Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + visible: invoice.invoiceType == Invoice.LightningInvoice && invoice.address + text: qsTr('Fallback address') + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: invoice.invoiceType == Invoice.LightningInvoice && invoice.address + leftPadding: constants.paddingMedium + Label { + width: parent.width + text: invoice.address + font.family: FixedFont + wrapMode: Text.Wrap + } + } } } From 6d876da1c4945ff08e7b285786fd7b7327c84e25 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 12:43:53 +0200 Subject: [PATCH 0594/1143] qml InvoiceDialog: update userinfo messages --- electrum/gui/qml/qeinvoice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 32c09c0db..2b93dab37 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -389,8 +389,8 @@ def update_userinfo(self): self.userinfo = _('Insufficient balance') else: self.userinfo = { - PR_EXPIRED: _('Invoice is expired'), - PR_PAID: _('Invoice is already paid'), + PR_EXPIRED: _('This invoice has expired'), + PR_PAID: _('This invoice was already paid'), PR_INFLIGHT: _('Payment in progress...'), PR_ROUTING: _('Payment in progress'), PR_UNKNOWN: _('Invoice has unknown status'), @@ -401,9 +401,9 @@ def update_userinfo(self): self.userinfo = _('Insufficient balance') else: self.userinfo = { - PR_EXPIRED: _('Invoice is expired'), - PR_PAID: _('Invoice is already paid'), - PR_UNCONFIRMED: _('Invoice is already paid'), + PR_EXPIRED: _('This invoice has expired'), + PR_PAID: _('This invoice was already paid'), + PR_UNCONFIRMED: _('Payment in progress...') + ' (' + _('waiting for confirmation') + ')', PR_UNKNOWN: _('Invoice has unknown status'), }[self.status] From a753f34c0956a60cad956eac57197f847c9b7820 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 12:47:30 +0200 Subject: [PATCH 0595/1143] Qt: rename utxo menu action to 'privacy analysis' --- electrum/gui/qt/utxo_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index e41704eae..aff7e04e7 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -286,7 +286,7 @@ def create_menu(self, position): tx = self.wallet.adb.get_transaction(txid) if tx: label = self.wallet.get_label_for_txid(txid) - menu.addAction(_("Details"), lambda: self.main_window.show_utxo(utxo)) + menu.addAction(_("Privacy analysis"), lambda: self.main_window.show_utxo(utxo)) cc = self.add_copy_menu(menu, idx) cc.addAction(_("Long Output point"), lambda: self.place_text_on_clipboard(utxo.prevout.to_str(), title="Long Output point")) # fully spend From 02f093c2d2fbb2647e27def66e846e93e4aa0660 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 13:46:46 +0200 Subject: [PATCH 0596/1143] qml SwapDialog: move userinfo to the top, make it constant, add padding and labels below the slider --- electrum/gui/qml/components/SwapDialog.qml | 34 +++++++++++++++++----- electrum/gui/qml/qeswaphelper.py | 8 ++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index e5aac63c5..cf2a8aa24 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -23,14 +23,23 @@ ElDialog { ColumnLayout { width: parent.width height: parent.height - spacing: 0 + spacing: constants.paddingLarge + + InfoTextArea { + Layout.leftMargin: constants.paddingXXLarge + Layout.rightMargin: constants.paddingXXLarge + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + visible: swaphelper.userinfo != '' + text: swaphelper.userinfo + } GridLayout { id: layout columns: 2 Layout.preferredWidth: parent.width - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge + Layout.leftMargin: constants.paddingXXLarge + Layout.rightMargin: constants.paddingXXLarge RowLayout { Layout.preferredWidth: 1 @@ -207,13 +216,22 @@ ElDialog { } } - InfoTextArea { + RowLayout { + Layout.fillWidth: true Layout.leftMargin: constants.paddingXXLarge Layout.rightMargin: constants.paddingXXLarge - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - visible: swaphelper.userinfo != '' - text: swaphelper.userinfo + Label { + text: '<-- ' + qsTr('Add receiving capacity') + font.pixelSize: constants.fontSizeXSmall + color: Material.accentColor + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: qsTr('Add sending capacity') + ' -->' + font.pixelSize: constants.fontSizeXSmall + color: Material.accentColor + } } Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 5e09d4952..09107859f 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -33,7 +33,10 @@ def __init__(self, parent=None): self._rangeMax = 0 self._tx = None self._valid = False - self._userinfo = '' + self._userinfo = ' '.join([ + _('Move the slider to set the amount and direction of the swap.'), + _('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'), + ]) self._tosend = QEAmount() self._toreceive = QEAmount() self._serverfeeperc = '' @@ -277,7 +280,6 @@ def swap_slider_moved(self): # pay_amount and receive_amounts are always with fees already included # so they reflect the net balance change after the swap if position < 0: # reverse swap - self.userinfo = _('Adds Lightning receiving capacity.') self.isReverse = True self._send_amount = abs(position) @@ -295,7 +297,6 @@ def swap_slider_moved(self): self.check_valid(self._send_amount, self._receive_amount) else: # forward (normal) swap - self.userinfo = _('Adds Lightning sending capacity.') self.isReverse = False self._send_amount = position self.tosend = QEAmount(amount_sat=self._send_amount) @@ -318,7 +319,6 @@ def check_valid(self, send_amount, receive_amount): self.valid = True else: # add more nuanced error reporting? - self.userinfo = _('Swap below minimal swap size, change the slider.') self.valid = False def fwd_swap_updatetx(self): From ba82813c06e36cb2f0f7b34a4b669ea7f7348002 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 14:05:58 +0200 Subject: [PATCH 0597/1143] qml InvoiceDialog: remove delete button, it only makes sense if you visit the list --- electrum/gui/qml/components/InvoiceDialog.qml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index dc1bfa1fc..0518214f2 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -387,24 +387,12 @@ ElDialog { ButtonContainer { Layout.fillWidth: true - FlatButton { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr('Delete') - icon.source: '../../icons/delete.png' - visible: invoice_key != '' - onClicked: { - invoice.wallet.delete_invoice(invoice_key) - dialog.close() - } - } FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 text: qsTr('Save') icon.source: '../../icons/save.png' - visible: invoice_key == '' - enabled: invoice.canSave + enabled: invoice_key == '' && invoice.canSave onClicked: { app.stack.push(Qt.resolvedUrl('Invoices.qml')) if (invoice.amount.isEmpty) { From 48689ecc89de1bada4906c0ff3d92b288267d5fe Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 15:10:39 +0200 Subject: [PATCH 0598/1143] qml tx details and rbf dialogs: use a single InfoTextArea, to the top of each dialog. Do not display 'cannot bump fee' as the first thing we see when we enter the bump fee dialog; suggest to move the slider instead. --- .../gui/qml/components/RbfBumpFeeDialog.qml | 8 +- .../gui/qml/components/RbfCancelDialog.qml | 8 +- electrum/gui/qml/components/TxDetails.qml | 106 +++++++----------- electrum/gui/qml/qetxfinalizer.py | 13 ++- 4 files changed, 58 insertions(+), 77 deletions(-) diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 9d2c514c1..4761da34c 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -44,12 +44,11 @@ ElDialog { columns: 2 - Label { + InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - text: qsTr('Increase your transaction\'s fee to improve its position in the mempool') - wrapMode: Text.Wrap + text: qsTr('Move the slider to increase your transaction\'s fee. This will improve its position in the mempool') } Label { @@ -186,12 +185,11 @@ ElDialog { } } - InfoTextArea { + Label { Layout.columnSpan: 2 Layout.fillWidth: true visible: rbffeebumper.warning != '' text: rbffeebumper.warning - iconStyle: InfoTextArea.IconStyle.Warn } Label { diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 899c10dc1..3f4aaccb7 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -41,12 +41,11 @@ ElDialog { width: parent.width columns: 2 - Label { + InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - text: qsTr('Cancel an unconfirmed RBF transaction by double-spending its inputs back to your wallet with a higher fee.') - wrapMode: Text.Wrap + text: qsTr('Cancel an unconfirmed transaction by double-spending its inputs back to your wallet with a higher fee.') } Label { @@ -156,12 +155,11 @@ ElDialog { } } - InfoTextArea { + Label { Layout.columnSpan: 2 Layout.fillWidth: true visible: txcanceller.warning != '' text: txcanceller.warning - iconStyle: InfoTextArea.IconStyle.Warn } Label { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index a9f8aeded..e942e83c8 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -47,7 +47,18 @@ Pane { Heading { Layout.columnSpan: 2 - text: qsTr('Transaction Details') + text: qsTr('On-chain Transaction') + } + + InfoTextArea { + id: bumpfeeinfo + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel + text: qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel + ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') + : qsTr('You can bump its fee to speed up its confirmation')) } RowLayout { @@ -126,70 +137,6 @@ Pane { visible: txdetails.mempoolDepth } - TextHighlightPane { - Layout.fillWidth: true - Layout.topMargin: constants.paddingSmall - Layout.columnSpan: 2 - borderColor: constants.colorWarning - visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel - - GridLayout { - width: parent.width - columns: actionButtonsLayout.implicitWidth > parent.width/2 - ? 1 - : 2 - Label { - id: bumpfeeinfo - Layout.fillWidth: true - text: qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel - ? qsTr('You can increase fees to speed up the transaction, or cancel this transaction') - : qsTr('You can increase fees to speed up the transaction')) - wrapMode: Text.Wrap - } - ColumnLayout { - id: actionButtonsLayout - Layout.alignment: Qt.AlignHCenter - Pane { - Layout.alignment: Qt.AlignHCenter - background: Rectangle { color: Material.dialogColor } - padding: 0 - visible: txdetails.canBump || txdetails.canCpfp - FlatButton { - id: feebumpButton - textUnderIcon: false - icon.source: '../../icons/add.png' - text: qsTr('Bump fee') - onClicked: { - if (txdetails.canBump) { - var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) - } else { - var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) - } - dialog.open() - } - } - } - Pane { - Layout.alignment: Qt.AlignHCenter - background: Rectangle { color: Material.dialogColor } - padding: 0 - visible: txdetails.canCancel - FlatButton { - id: cancelButton - textUnderIcon: false - icon.source: '../../icons/closebutton.png' - text: qsTr('Cancel Tx') - onClicked: { - var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) - dialog.open() - } - } - } - } - } - - } - Label { visible: txdetails.isMined text: qsTr('Date') @@ -348,6 +295,34 @@ Pane { ButtonContainer { Layout.fillWidth: true + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + id: feebumpButton + icon.source: '../../icons/add.png' + text: qsTr('Bump fee') + visible: txdetails.canBump || txdetails.canCpfp + onClicked: { + if (txdetails.canBump) { + var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) + } else { + var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + } + dialog.open() + } + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + id: cancelButton + icon.source: '../../icons/closebutton.png' + text: qsTr('Cancel Tx') + visible: txdetails.canCancel + onClicked: { + var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) + dialog.open() + } + } FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -452,7 +427,6 @@ Pane { } function onBroadcastSucceeded() { bumpfeeinfo.text = qsTr('Transaction was broadcast successfully') - actionButtonsLayout.visible = false } } diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 62b0d8493..6c6972a5f 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -527,7 +527,12 @@ def update(self): return new_fee_rate = fee_per_kb / 1000 - + if new_fee_rate <= float(self._oldfee_rate): + self._tx = None + self._valid = False + self.validChanged.emit() + self.warning = _("The new fee rate needs to be higher than the old fee rate.") + return try: self._tx = self._wallet.wallet.bump_fee( tx=self._orig_tx, @@ -630,6 +635,12 @@ def update(self): return new_fee_rate = fee_per_kb / 1000 + if new_fee_rate <= float(self._oldfee_rate): + self._tx = None + self._valid = False + self.validChanged.emit() + self.warning = _("The new fee rate needs to be higher than the old fee rate.") + return try: self._tx = self._wallet.wallet.dscancel( From d0947bc0a649e07f4000acfcc33578e34a24d277 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 15:47:32 +0200 Subject: [PATCH 0599/1143] follow-up 48689ecc89de1bada4906c0ff3d92b288267d5fe --- electrum/gui/qml/qetxfinalizer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 6c6972a5f..593d3bf56 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -528,7 +528,6 @@ def update(self): new_fee_rate = fee_per_kb / 1000 if new_fee_rate <= float(self._oldfee_rate): - self._tx = None self._valid = False self.validChanged.emit() self.warning = _("The new fee rate needs to be higher than the old fee rate.") @@ -636,7 +635,6 @@ def update(self): new_fee_rate = fee_per_kb / 1000 if new_fee_rate <= float(self._oldfee_rate): - self._tx = None self._valid = False self.validChanged.emit() self.warning = _("The new fee rate needs to be higher than the old fee rate.") From 9eb59fc360a527b154175584b86b044912a5d569 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 16:10:49 +0200 Subject: [PATCH 0600/1143] follow-up 56e685f: amount_sat may be None or max --- electrum/invoices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/invoices.py b/electrum/invoices.py index 49819807c..1bd10292b 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -232,7 +232,7 @@ def as_dict(self, status): 'status': status, 'status_str': self.get_status_str(status), 'id': self.get_id(), - 'amount_sat': int(self.get_amount_sat()), + 'amount_sat': self.get_amount_sat(), } if self.is_lightning(): d['amount_msat'] = self.get_amount_msat() From 84cb210e7eed547f28bbd4e948500547fd01213e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 1 Apr 2023 17:07:09 +0200 Subject: [PATCH 0601/1143] qml TxDetails: do not show rbf buttons if the tx is local --- electrum/gui/qml/components/TxDetails.qml | 10 ++++++---- electrum/gui/qml/qetxdetails.py | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index e942e83c8..b8c99bd96 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -55,10 +55,12 @@ Pane { Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel - text: qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel - ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') - : qsTr('You can bump its fee to speed up its confirmation')) + visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel || txdetails.canRemove + text: txdetails.canRemove + ? qsTr('This transaction is local to your wallet. It has not been published yet.') + : qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel + ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') + : qsTr('You can bump its fee to speed up its confirmation')) } RowLayout { diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index ed032c91b..866aea3d9 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -277,10 +277,10 @@ def update(self): self._is_final = self._tx.is_final() self._is_unrelated = txinfo.amount is None and self._lnamount.isEmpty self._is_lightning_funding_tx = txinfo.is_lightning_funding_tx - self._can_bump = txinfo.can_bump - self._can_dscancel = txinfo.can_dscancel self._can_broadcast = txinfo.can_broadcast - self._can_cpfp = txinfo.can_cpfp + self._can_bump = txinfo.can_bump and not txinfo.can_remove + self._can_dscancel = txinfo.can_dscancel and not txinfo.can_remove + self._can_cpfp = txinfo.can_cpfp and not txinfo.can_remove self._can_save_as_local = txinfo.can_save_as_local and not txinfo.can_remove self._can_remove = txinfo.can_remove self._can_sign = not self._is_complete and self._wallet.wallet.can_sign(self._tx) From da802d20adf56f70b431b1251b413aa7976321a4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 2 Apr 2023 09:33:32 +0200 Subject: [PATCH 0602/1143] qml: make zero balance visible in balance details. Disable open channel button if there is no confirmed balance --- .../gui/qml/components/BalanceDetails.qml | 20 +++++++++---------- electrum/gui/qml/components/Channels.qml | 3 ++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index 1500181db..0398436ec 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -92,50 +92,46 @@ Pane { Layout.alignment: Qt.AlignHCenter visible: Daemon.currentWallet columns: 3 - Item { - visible: !Daemon.currentWallet.totalBalance.isEmpty Layout.preferredWidth: 1; Layout.preferredHeight: 1 } Label { - visible: !Daemon.currentWallet.totalBalance.isEmpty text: qsTr('Total') } FormattedAmount { - visible: !Daemon.currentWallet.totalBalance.isEmpty amount: Daemon.currentWallet.totalBalance } Rectangle { - visible: !Daemon.currentWallet.lightningBalance.isEmpty + visible: Daemon.currentWallet.isLightning Layout.preferredWidth: constants.iconSizeXSmall Layout.preferredHeight: constants.iconSizeXSmall color: constants.colorPiechartLightning } Label { - visible: !Daemon.currentWallet.lightningBalance.isEmpty + visible: Daemon.currentWallet.isLightning text: qsTr('Lightning') } FormattedAmount { + visible: Daemon.currentWallet.isLightning amount: Daemon.currentWallet.lightningBalance - visible: !Daemon.currentWallet.lightningBalance.isEmpty } Rectangle { - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty Layout.preferredWidth: constants.iconSizeXSmall Layout.preferredHeight: constants.iconSizeXSmall color: constants.colorPiechartOnchain } Label { - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty + visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty text: qsTr('On-chain') } FormattedAmount { + visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty amount: Daemon.currentWallet.confirmedBalance - visible: !Daemon.currentWallet.lightningBalance.isEmpty || !Daemon.currentWallet.frozenBalance.isEmpty } Rectangle { @@ -163,7 +159,8 @@ Pane { Layout.fillWidth: true Layout.preferredWidth: 1 text: qsTr('Lightning swap'); - visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 + visible: Daemon.currentWallet.isLightning + enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 icon.source: Qt.resolvedUrl('../../icons/update.png') onClicked: { var swaphelper = app.swaphelper.createObject(app) @@ -181,6 +178,7 @@ Pane { Layout.preferredWidth: 1 text: qsTr('Open Channel') visible: Daemon.currentWallet.isLightning + enabled: Daemon.currentWallet.confirmedBalance.satInt > 0 onClicked: { var dialog = openChannelDialog.createObject(rootItem) dialog.open() diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index c75661bed..426c639d1 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -123,7 +123,7 @@ Pane { Layout.fillWidth: true Layout.preferredWidth: 1 text: qsTr('Swap'); - visible: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 + enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 icon.source: Qt.resolvedUrl('../../icons/update.png') onClicked: { var swaphelper = app.swaphelper.createObject(app) @@ -139,6 +139,7 @@ Pane { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 + enabled: Daemon.currentWallet.confirmedBalance.satInt > 0 text: qsTr('Open Channel') onClicked: { var dialog = openChannelDialog.createObject(root) From 545ee24f46deaa8e55a9c3ba0178d609890cc72d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 2 Apr 2023 10:07:34 +0200 Subject: [PATCH 0603/1143] Qt: move new_channel_dialog to main_window and test available amount beforehand --- electrum/gui/qt/channels_list.py | 18 +----------------- electrum/gui/qt/main_window.py | 17 +++++++++++++++++ electrum/gui/qt/new_channel_dialog.py | 4 +--- electrum/gui/qt/send_tab.py | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 0046564ef..2df20b104 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -363,22 +363,11 @@ def create_toolbar(self, config): menu.addAction(_('Submarine swap'), lambda: self.main_window.run_swap_dialog()) menu.addSeparator() menu.addAction(_("Import channel backup"), lambda: self.main_window.do_process_from_text_channel_backup()) - self.new_channel_button = EnterButton(_('New Channel'), self.new_channel_with_warning) + self.new_channel_button = EnterButton(_('New Channel'), self.main_window.new_channel_dialog) self.new_channel_button.setEnabled(self.wallet.has_lightning()) toolbar.insertWidget(2, self.new_channel_button) return toolbar - def new_channel_with_warning(self): - lnworker = self.wallet.lnworker - if not lnworker.channels and not lnworker.channel_backups: - warning = _(messages.MSG_LIGHTNING_WARNING) - answer = self.main_window.question( - _('Do you want to create your first channel?') + '\n\n' + warning) - if answer: - self.new_channel_dialog() - else: - self.new_channel_dialog() - def statistics_dialog(self): channel_db = self.network.channel_db capacity = self.main_window.format_amount(channel_db.capacity()) + ' '+ self.main_window.base_unit() @@ -396,11 +385,6 @@ def statistics_dialog(self): vbox.addLayout(Buttons(OkButton(d))) d.exec_() - def new_channel_dialog(self, *, amount_sat=None, min_amount_sat=None): - from .new_channel_dialog import NewChannelDialog - d = NewChannelDialog(self.main_window, amount_sat, min_amount_sat) - return d.run() - def set_visibility_of_columns(self): def set_visible(col: int, b: bool): self.showColumn(col) if b else self.hideColumn(col) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index aca538c62..4eeb4aebc 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1681,6 +1681,23 @@ def do_search(self, t): if hasattr(tab, 'searchable_list'): tab.searchable_list.filter(t) + def new_channel_dialog(self, *, amount_sat=None, min_amount_sat=None): + from electrum.lnutil import MIN_FUNDING_SAT + from .new_channel_dialog import NewChannelDialog + confirmed, unconfirmed, unmatured, frozen, lightning, f_lightning = self.wallet.get_balances_for_piechart() + min_amount_sat = min_amount_sat or MIN_FUNDING_SAT + if confirmed < min_amount_sat: + msg = _('Not enough funds') + '\n\n' + _('You need at least {} to open a channel.').format(self.format_amount_and_units(min_amount_sat)) + self.show_error(msg) + return + lnworker = self.wallet.lnworker + if not lnworker.channels and not lnworker.channel_backups: + msg = _('Do you want to create your first channel?') + '\n\n' + _(messages.MSG_LIGHTNING_WARNING) + if not self.question(msg): + return + d = NewChannelDialog(self, amount_sat, min_amount_sat) + return d.run() + def new_contact_dialog(self): d = WindowModalDialog(self, _("New Contact")) vbox = QVBoxLayout(d) diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index d48b661a2..6eaf95fbe 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -42,9 +42,7 @@ def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, m ).setEnabled(self.lnworker.can_have_recoverable_channels()) vbox.addLayout(toolbar) msg = _('Choose a remote node and an amount to fund the channel.') - if min_amount_sat: - # only displayed if min_amount_sat is passed as parameter - msg += '\n' + _('You need to put at least') + ': ' + self.window.format_amount_and_units(self.min_amount_sat) + msg += '\n' + _('You need to put at least') + ': ' + self.window.format_amount_and_units(self.min_amount_sat) vbox.addWidget(WWLabel(msg)) if self.network.channel_db: vbox.addWidget(QLabel(_('Enter Remote Node ID or connection string or invoice'))) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 5ff597f6a..cec6b6144 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -713,7 +713,7 @@ def pay_lightning_invoice(self, invoice: Invoice): self.window.rebalance_dialog(chan1, chan2, amount_sat=delta) elif r == 1: amount_sat, min_amount_sat = can_pay_with_new_channel - self.window.channels_list.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat) + self.window.new_channel_dialog(amount_sat=amount_sat, min_amount_sat=min_amount_sat) elif r == 2: chan, swap_recv_amount_sat = can_pay_with_swap self.window.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) From 03fbf6c3d80783210b1a3b5a52d14eac6e6fdc7d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 2 Apr 2023 10:37:35 +0200 Subject: [PATCH 0604/1143] qml: show lightning can send/receive amounts in balance details, rather than in requestDetaildDialog. Also remove junk code copy-pasted from WalletDetails. --- .../gui/qml/components/BalanceDetails.qml | 54 ++++++++----------- .../qml/components/ReceiveDetailsDialog.qml | 28 ---------- 2 files changed, 23 insertions(+), 59 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index 0398436ec..cc4073deb 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -13,36 +13,6 @@ Pane { padding: 0 - property bool _is2fa: Daemon.currentWallet && Daemon.currentWallet.walletType == '2fa' - - function enableLightning() { - var dialog = app.messageDialog.createObject(rootItem, - {'text': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) - dialog.yesClicked.connect(function() { - Daemon.currentWallet.enableLightning() - }) - dialog.open() - } - - function deleteWallet() { - var dialog = app.messageDialog.createObject(rootItem, - {'text': qsTr('Really delete this wallet?'), 'yesno': true}) - dialog.yesClicked.connect(function() { - Daemon.checkThenDeleteWallet(Daemon.currentWallet) - }) - dialog.open() - } - - function changePassword() { - // trigger dialog via wallet (auth then signal) - Daemon.startChangePassword() - } - - function importAddressesKeys() { - var dialog = importAddressesKeysDialog.createObject(rootItem) - dialog.open() - } - ColumnLayout { id: rootLayout anchors.fill: parent @@ -149,6 +119,28 @@ Pane { visible: !Daemon.currentWallet.frozenBalance.isEmpty } } + + Heading { + text: qsTr('Lightning Liquidity') + visible: Daemon.currentWallet.isLightning + } + GridLayout { + Layout.alignment: Qt.AlignHCenter + visible: Daemon.currentWallet && Daemon.currentWallet.isLightning + columns: 2 + Label { + text: qsTr('Can send') + } + FormattedAmount { + amount: Daemon.currentWallet.lightningCanSend + } + Label { + text: qsTr('Can receive') + } + FormattedAmount { + amount: Daemon.currentWallet.lightningCanReceive + } + } } } } @@ -178,7 +170,7 @@ Pane { Layout.preferredWidth: 1 text: qsTr('Open Channel') visible: Daemon.currentWallet.isLightning - enabled: Daemon.currentWallet.confirmedBalance.satInt > 0 + enabled: Daemon.currentWallet.confirmedBalance.satsInt > 0 onClicked: { var dialog = openChannelDialog.createObject(rootItem) dialog.open() diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index c16ab337e..4f45ea3c0 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -33,34 +33,6 @@ ElDialog { columnSpacing: constants.paddingSmall columns: 4 - TextHighlightPane { - Layout.columnSpan: 4 - Layout.fillWidth: true - - visible: !Daemon.currentWallet.lightningCanReceive.isEmpty - - RowLayout { - width: parent.width - spacing: constants.paddingXSmall - Label { - text: qsTr('Max amount over Lightning') - font.pixelSize: constants.fontSizeSmall - color: Material.accentColor - wrapMode: Text.Wrap - // try to fill/wrap in remaining space - Layout.preferredWidth: Math.min(implicitWidth, parent.width - 2*parent.spacing - constants.iconSizeSmall - lnMaxAmount.implicitWidth) - } - Image { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - source: '../../icons/lightning.png' - } - FormattedAmount { - id: lnMaxAmount - amount: Daemon.currentWallet.lightningCanReceive - } - } - } Label { text: qsTr('Message') From 198ca10cd06a33a7fc4d49a999fb9a3a024b0911 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 2 Apr 2023 11:15:28 +0200 Subject: [PATCH 0605/1143] qml: add InfoTextArea about PressAndHold --- electrum/gui/qml/components/Invoices.qml | 7 +++++++ electrum/gui/qml/components/ReceiveRequests.qml | 7 +++++++ electrum/gui/qml/components/WalletMainView.qml | 2 ++ electrum/gui/qml/qewallet.py | 12 ++++++++++-- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 5535837b1..596427976 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -14,6 +14,13 @@ Pane { ColumnLayout { anchors.fill: parent + InfoTextArea { + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: !Daemon.currentWallet.userKnowsPressAndHold + text: qsTr('To access this list from the main screen, press and hold the Send button') + } + Heading { text: qsTr('Saved Invoices') } diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index 3de544f5b..f33ab8dea 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -17,6 +17,13 @@ Pane { ColumnLayout { anchors.fill: parent + InfoTextArea { + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: !Daemon.currentWallet.userKnowsPressAndHold + text: qsTr('To access this list from the main screen, press and hold the Receive button') + } + Heading { text: qsTr('Pending requests') } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index c0a1cbfff..7d89e80a3 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -192,6 +192,7 @@ Item { dialog.open() } onPressAndHold: { + Daemon.currentWallet.userKnowsPressAndHold = true Daemon.currentWallet.delete_expired_requests() app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) } @@ -204,6 +205,7 @@ Item { text: qsTr('Send') onClicked: openSendDialog() onPressAndHold: { + Daemon.currentWallet.userKnowsPressAndHold = true app.stack.push(Qt.resolvedUrl('Invoices.qml')) } } diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 5e723acff..605f9acc2 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -125,9 +125,18 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) - + self._user_knows_press_and_hold = False # maybe save in config? self.synchronizing = not wallet.is_up_to_date() + userKnowsPressAndHoldChanged = pyqtSignal() + @pyqtProperty(bool, notify=userKnowsPressAndHoldChanged) + def userKnowsPressAndHold(self): + return self._user_knows_press_and_hold + + @userKnowsPressAndHold.setter + def userKnowsPressAndHold(self, userKnowsPressAndHold): + self._user_knows_press_and_hold = userKnowsPressAndHold + synchronizingChanged = pyqtSignal() @pyqtProperty(bool, notify=synchronizingChanged) def synchronizing(self): @@ -629,7 +638,6 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, lightni elif lightning_only: addr = None else: - has_lightning = self.wallet.has_lightning() msg = [ _('No address available.'), _('All your addresses are used in pending requests.'), From 4c87773174f1d8da1eaecbea00c7d1019a1e0b58 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 10:26:03 +0200 Subject: [PATCH 0606/1143] qml: move user_knowns_press_and_hold to config --- electrum/gui/qml/components/Invoices.qml | 2 +- electrum/gui/qml/components/ReceiveRequests.qml | 2 +- electrum/gui/qml/components/WalletMainView.qml | 2 +- electrum/gui/qml/qeconfig.py | 12 ++++++++++++ electrum/gui/qml/qewallet.py | 10 ---------- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 596427976..81a6eeee3 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -17,7 +17,7 @@ Pane { InfoTextArea { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: !Daemon.currentWallet.userKnowsPressAndHold + visible: !Config.userKnowsPressAndHold text: qsTr('To access this list from the main screen, press and hold the Send button') } diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index f33ab8dea..456544728 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -20,7 +20,7 @@ Pane { InfoTextArea { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: !Daemon.currentWallet.userKnowsPressAndHold + visible: !Config.userKnowsPressAndHold text: qsTr('To access this list from the main screen, press and hold the Receive button') } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 7d89e80a3..5641d98b3 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -192,7 +192,7 @@ Item { dialog.open() } onPressAndHold: { - Daemon.currentWallet.userKnowsPressAndHold = true + Config.userKnowsPressAndHold = true Daemon.currentWallet.delete_expired_requests() app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) } diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 6dc934221..7a4c1720b 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -199,6 +199,18 @@ def preferredRequestType(self, preferred_request_type): self.config.set_key('preferred_request_type', preferred_request_type) self.preferredRequestTypeChanged.emit() + userKnowsPressAndHoldChanged = pyqtSignal() + @pyqtProperty(bool, notify=userKnowsPressAndHoldChanged) + def userKnowsPressAndHold(self): + return self.config.get('user_knows_press_and_hold', False) + + @userKnowsPressAndHold.setter + def userKnowsPressAndHold(self, userKnowsPressAndHold): + if userKnowsPressAndHold != self.config.get('user_knows_press_and_hold', False): + self.config.set_key('user_knows_press_and_hold', userKnowsPressAndHold) + self.userKnowsPressAndHoldChanged.emit() + + @pyqtSlot('qint64', result=str) @pyqtSlot('qint64', bool, result=str) @pyqtSlot(QEAmount, result=str) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 605f9acc2..850bbf60e 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -125,18 +125,8 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) - self._user_knows_press_and_hold = False # maybe save in config? self.synchronizing = not wallet.is_up_to_date() - userKnowsPressAndHoldChanged = pyqtSignal() - @pyqtProperty(bool, notify=userKnowsPressAndHoldChanged) - def userKnowsPressAndHold(self): - return self._user_knows_press_and_hold - - @userKnowsPressAndHold.setter - def userKnowsPressAndHold(self, userKnowsPressAndHold): - self._user_knows_press_and_hold = userKnowsPressAndHold - synchronizingChanged = pyqtSignal() @pyqtProperty(bool, notify=synchronizingChanged) def synchronizing(self): From ffac79c324dfe1a698531e89bc00daa128b8d0a8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 10:27:33 +0200 Subject: [PATCH 0607/1143] qml: follupup prev --- electrum/gui/qml/components/WalletMainView.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 5641d98b3..dee814555 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -205,7 +205,7 @@ Item { text: qsTr('Send') onClicked: openSendDialog() onPressAndHold: { - Daemon.currentWallet.userKnowsPressAndHold = true + Config.userKnowsPressAndHold = true app.stack.push(Qt.resolvedUrl('Invoices.qml')) } } From 0ce3559d62076e3f11d3ba1d7262c804ef1e9089 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 10:59:50 +0200 Subject: [PATCH 0608/1143] qml: trustedcoin icon in 2fas wizard disclaimer --- electrum/plugins/trustedcoin/qml/Disclaimer.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/electrum/plugins/trustedcoin/qml/Disclaimer.qml b/electrum/plugins/trustedcoin/qml/Disclaimer.qml index 53f5b555e..bf8520074 100644 --- a/electrum/plugins/trustedcoin/qml/Disclaimer.qml +++ b/electrum/plugins/trustedcoin/qml/Disclaimer.qml @@ -14,6 +14,12 @@ WizardComponent { ColumnLayout { width: parent.width + Image { + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: constants.paddingLarge + source: '../../../gui/icons/trustedcoin-wizard.png' + } + Label { Layout.fillWidth: true text: plugin ? plugin.disclaimer : '' From 86711a6a9960d7220b3375b8c66ce74242ddc8de Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 12:07:47 +0200 Subject: [PATCH 0609/1143] qml: icon --- electrum/gui/qml/components/ReceiveDetailsDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 4f45ea3c0..4950f1e34 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -12,6 +12,7 @@ ElDialog { id: dialog title: qsTr('Receive payment') + iconSource: Qt.resolvedUrl('../../icons/tab_receive.png') property alias amount: amountBtc.text property alias description: message.text From ba2faa8c9f01f4ba545eab91e494cc0c6b5537c3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 12:11:32 +0200 Subject: [PATCH 0610/1143] qml: avoid adding duplicate request/invoice to listmodel --- electrum/gui/qml/qeinvoicelistmodel.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index a983708ab..7d5ea115c 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -72,6 +72,12 @@ def init_model(self): self.set_status_timer() def add_invoice(self, invoice: Invoice): + # skip if already in list + key = invoice.get_id() + for invoice in self.invoices: + if invoice['key'] == key: + return + item = self.invoice_to_model(invoice) self._logger.debug(str(item)) From 01c31edae233643ddaa4b43f7b0a94a370fbd8d1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 13:31:20 +0200 Subject: [PATCH 0611/1143] qml: force same auto-capitalizing behavior on PasswordField regardless of echoMode --- electrum/gui/qml/components/controls/PasswordField.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/PasswordField.qml b/electrum/gui/qml/components/controls/PasswordField.qml index fed6be43e..8aedfa76d 100644 --- a/electrum/gui/qml/components/controls/PasswordField.qml +++ b/electrum/gui/qml/components/controls/PasswordField.qml @@ -14,7 +14,7 @@ RowLayout { TextField { id: password_tf echoMode: TextInput.Password - inputMethodHints: Qt.ImhSensitiveData + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoAutoUppercase | Qt.ImhPreferLowercase Layout.fillWidth: true Layout.minimumWidth: fontMetrics.advanceWidth('X') * 16 onAccepted: root.accepted() From e362ac52fad6782106ed0e479734675359995307 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 14:01:59 +0200 Subject: [PATCH 0612/1143] qml: styling --- electrum/gui/qml/components/NetworkOverview.qml | 2 +- electrum/gui/qml/components/SwapDialog.qml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index d655d81bf..bf84be72b 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -135,7 +135,7 @@ Pane { RowLayout { Layout.fillWidth: true Label { - text: '> ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) + text: '< ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) font.pixelSize: constants.fontSizeXSmall color: Material.accentColor } diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index cf2a8aa24..0eeb8bb3d 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -218,8 +218,9 @@ ElDialog { RowLayout { Layout.fillWidth: true - Layout.leftMargin: constants.paddingXXLarge - Layout.rightMargin: constants.paddingXXLarge + Layout.topMargin: -constants.paddingXXLarge + Layout.leftMargin: constants.paddingXXLarge + swapslider.leftPadding + Layout.rightMargin: constants.paddingXXLarge + swapslider.rightPadding Label { text: '<-- ' + qsTr('Add receiving capacity') font.pixelSize: constants.fontSizeXSmall From 8bb2464acd41e899fce5cd4ab02729fcae8f75de Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Apr 2023 16:07:15 +0200 Subject: [PATCH 0613/1143] qml: add haptic feedback for android. Also preload most used classes to reduce lag on first use. --- .../Styles/Electrum/ElectrumKeyPanel.qml | 16 ++++++ .../VirtualKeyboard/Styles/Electrum/style.qml | 20 ++++---- electrum/gui/qml/qeapp.py | 49 +++++++++++-------- 3 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml new file mode 100644 index 000000000..c21f91f50 --- /dev/null +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml @@ -0,0 +1,16 @@ +import QtQuick 2.7 +import QtQuick.VirtualKeyboard 2.1 +import QtQuick.VirtualKeyboard.Styles 2.1 + +import org.electrum 1.0 + +KeyPanel { + id: keyPanel + Connections { + target: keyPanel.control + function onPressedChanged() { + if (keyPanel.control.pressed) + AppController.haptic() + } + } +} diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml index 547affc09..a016f25a4 100644 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml @@ -65,7 +65,7 @@ KeyboardStyle { color: constants.colorAlpha(Material.accentColor, 0.5) //mutedForeground //'red' //"black" } - keyPanel: KeyPanel { + keyPanel: ElectrumKeyPanel { id: keyPanel Rectangle { id: keyBackground @@ -135,7 +135,7 @@ KeyboardStyle { ] } - backspaceKeyPanel: KeyPanel { + backspaceKeyPanel: ElectrumKeyPanel { id: backspaceKeyPanel Rectangle { id: backspaceKeyBackground @@ -180,7 +180,7 @@ KeyboardStyle { ] } - languageKeyPanel: KeyPanel { + languageKeyPanel: ElectrumKeyPanel { id: languageKeyPanel Rectangle { id: languageKeyBackground @@ -225,7 +225,7 @@ KeyboardStyle { ] } - enterKeyPanel: KeyPanel { + enterKeyPanel: ElectrumKeyPanel { id: enterKeyPanel Rectangle { id: enterKeyBackground @@ -322,7 +322,7 @@ KeyboardStyle { ] } - hideKeyPanel: KeyPanel { + hideKeyPanel: ElectrumKeyPanel { id: hideKeyPanel Rectangle { id: hideKeyBackground @@ -367,7 +367,7 @@ KeyboardStyle { ] } - shiftKeyPanel: KeyPanel { + shiftKeyPanel: ElectrumKeyPanel { id: shiftKeyPanel Rectangle { id: shiftKeyBackground @@ -434,7 +434,7 @@ KeyboardStyle { ] } - spaceKeyPanel: KeyPanel { + spaceKeyPanel: ElectrumKeyPanel { id: spaceKeyPanel Rectangle { id: spaceKeyBackground @@ -475,7 +475,7 @@ KeyboardStyle { ] } - symbolKeyPanel: KeyPanel { + symbolKeyPanel: ElectrumKeyPanel { id: symbolKeyPanel Rectangle { id: symbolKeyBackground @@ -527,7 +527,7 @@ KeyboardStyle { ] } - modeKeyPanel: KeyPanel { + modeKeyPanel: ElectrumKeyPanel { id: modeKeyPanel Rectangle { id: modeKeyBackground @@ -592,7 +592,7 @@ KeyboardStyle { ] } - handwritingKeyPanel: KeyPanel { + handwritingKeyPanel: ElectrumKeyPanel { id: handwritingKeyPanel Rectangle { id: hwrKeyBackground diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 3dbbc36ea..e6a8a49a5 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -47,6 +47,16 @@ from electrum.daemon import Daemon from electrum.plugin import Plugins +if 'ANDROID_DATA' in os.environ: + from jnius import autoclass, cast + from android import activity + + jpythonActivity = autoclass('org.kivy.android.PythonActivity').mActivity + jHfc = autoclass('android.view.HapticFeedbackConstants') + jString = autoclass('java.lang.String') + jIntent = autoclass('android.content.Intent') + jview = jpythonActivity.getWindow().getDecorView() + notification = None class QEAppController(BaseCrashReporter, QObject): @@ -81,9 +91,10 @@ def __init__(self, qedaemon: 'QEDaemon', plugins: 'Plugins'): self._qedaemon.walletLoaded.connect(self.on_wallet_loaded) - self.userNotify.connect(self.notifyAndroid) + self.userNotify.connect(self.doNotify) - self.bindIntent() + if self.isAndroid(): + self.bindIntent() def on_wallet_loaded(self): qewallet = self._qedaemon.currentWallet @@ -125,7 +136,7 @@ def on_notification_timer(self): except queue.Empty: pass - def notifyAndroid(self, wallet_name, message): + def doNotify(self, wallet_name, message): try: # TODO: lazy load not in UI thread please global notification @@ -143,11 +154,7 @@ def bindIntent(self): if not self.isAndroid(): return try: - from android import activity - from jnius import autoclass - PythonActivity = autoclass('org.kivy.android.PythonActivity') - mactivity = PythonActivity.mActivity - self.on_new_intent(mactivity.getIntent()) + self.on_new_intent(jpythonActivity.getIntent()) activity.bind(on_new_intent=self.on_new_intent) except Exception as e: self.logger.error(f'unable to bind intent: {repr(e)}') @@ -170,22 +177,15 @@ def startupFinished(self): @pyqtSlot(str, str) def doShare(self, data, title): - try: - from jnius import autoclass, cast - except ImportError: - self.logger.error('Share: needs jnius. Platform not Android?') + if not self.isAndroid(): return - JS = autoclass('java.lang.String') - Intent = autoclass('android.content.Intent') - sendIntent = Intent() - sendIntent.setAction(Intent.ACTION_SEND) + sendIntent = jIntent() + sendIntent.setAction(jIntent.ACTION_SEND) sendIntent.setType("text/plain") - sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data)) - pythonActivity = autoclass('org.kivy.android.PythonActivity') - currentActivity = cast('android.app.Activity', pythonActivity.mActivity) - it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title))) - currentActivity.startActivity(it) + sendIntent.putExtra(jIntent.EXTRA_TEXT, jString(data)) + it = jIntent.createChooser(sendIntent, cast('java.lang.CharSequence', jString(title))) + jpythonActivity.startActivity(it) @pyqtSlot('QString') def textToClipboard(self, text): @@ -289,6 +289,13 @@ def get_wallet_type(self): wallet_types = Exception_Hook._INSTANCE.wallet_types_seen return ",".join(wallet_types) + @pyqtSlot() + def haptic(self): + if not self.isAndroid(): + return + jview.performHapticFeedback(jHfc.CONFIRM) + + class ElectrumQmlApplication(QGuiApplication): _valid = True From 157954d4ff2f72ad1ca67fed8d4ce8edbb59102c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 3 Apr 2023 17:10:52 +0200 Subject: [PATCH 0614/1143] qml: do not block access to BalanceSummary if we are not connected. Display warning instead --- electrum/gui/qml/components/BalanceDetails.qml | 10 ++++++++++ .../gui/qml/components/controls/BalanceSummary.qml | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index cc4073deb..db15329f8 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -35,6 +35,16 @@ Pane { width: parent.width spacing: constants.paddingLarge + InfoTextArea { + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: Daemon.currentWallet.synchronizing || Network.server_status != 'connected' + text: Daemon.currentWallet.synchronizing + ? qsTr('Your wallet is not synchronized. The displayed balance may be inaccurate.') + : qsTr('Your wallet is not connected to an Electrum server. The displayed balance may be outdated.') + iconStyle: InfoTextArea.IconStyle.Warn + } + Heading { text: qsTr('Wallet balance') } diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index cef653dc6..f1437f870 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -147,7 +147,6 @@ Item { MouseArea { anchors.fill: parent onClicked: { - if(Daemon.currentWallet.synchronizing || Network.server_status != 'connected') return app.stack.push(Qt.resolvedUrl('../BalanceDetails.qml')) } } From 83ee260ab73f67b4d7a40261fe9b526c1a4ec79f Mon Sep 17 00:00:00 2001 From: 3rd Iteration Date: Tue, 4 Apr 2023 03:27:38 -0400 Subject: [PATCH 0615/1143] Add Device IDs for DIY Jade on M5StickC-Plus (#8291) --- electrum/plugins/jade/jade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 2a68e0ad4..1493de8c2 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -349,7 +349,7 @@ def show_address_multi(self, multisig_name, paths): class JadePlugin(HW_PluginBase): keystore_class = Jade_KeyStore minimum_library = (0, 0, 1) - DEVICE_IDS = [(0x10c4, 0xea60), (0x1a86, 0x55d4)] + DEVICE_IDS = [(0x10c4, 0xea60), (0x1a86, 0x55d4), (0x0403, 0x6001)] SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') MIN_SUPPORTED_FW_VERSION = (0, 1, 32) From ebdebd18b4f0556bccbe0cb22466459739936ef5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Apr 2023 10:21:56 +0200 Subject: [PATCH 0616/1143] qml: fix type hints in qeinvoicelistmodel --- electrum/gui/qml/qeinvoicelistmodel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 7d5ea115c..2ab962b83 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -5,7 +5,7 @@ from electrum.logging import get_logger from electrum.util import Satoshis, format_time -from electrum.invoices import Invoice, PR_EXPIRED, LN_EXPIRY_NEVER +from electrum.invoices import BaseInvoice, PR_EXPIRED, LN_EXPIRY_NEVER from .util import QtEventListener, qt_event_listener, status_update_timer_interval from .qetypes import QEAmount @@ -71,7 +71,7 @@ def init_model(self): self.set_status_timer() - def add_invoice(self, invoice: Invoice): + def add_invoice(self, invoice: BaseInvoice): # skip if already in list key = invoice.get_id() for invoice in self.invoices: @@ -122,7 +122,7 @@ def updateInvoice(self, key, status): return i = i + 1 - def invoice_to_model(self, invoice: Invoice): + def invoice_to_model(self, invoice: BaseInvoice): item = self.get_invoice_as_dict(invoice) item['key'] = invoice.get_id() item['is_lightning'] = invoice.is_lightning() @@ -169,7 +169,7 @@ def get_invoice_list(self): raise Exception('provide impl') @abstractmethod - def get_invoice_as_dict(self, invoice: Invoice): + def get_invoice_as_dict(self, invoice: BaseInvoice): raise Exception('provide impl') @@ -190,7 +190,7 @@ def on_event_invoice_status(self, wallet, key, status): self._logger.debug(f'invoice status update for key {key} to {status}') self.updateInvoice(key, status) - def invoice_to_model(self, invoice: Invoice): + def invoice_to_model(self, invoice: BaseInvoice): item = super().invoice_to_model(invoice) item['type'] = 'invoice' @@ -202,7 +202,7 @@ def get_invoice_list(self): def get_invoice_for_key(self, key: str): return self.wallet.get_invoice(key) - def get_invoice_as_dict(self, invoice: Invoice): + def get_invoice_as_dict(self, invoice: BaseInvoice): return self.wallet.export_invoice(invoice) class QERequestListModel(QEAbstractInvoiceListModel, QtEventListener): @@ -222,7 +222,7 @@ def on_event_request_status(self, wallet, key, status): self._logger.debug(f'request status update for key {key} to {status}') self.updateRequest(key, status) - def invoice_to_model(self, invoice: Invoice): + def invoice_to_model(self, invoice: BaseInvoice): item = super().invoice_to_model(invoice) item['type'] = 'request' @@ -234,7 +234,7 @@ def get_invoice_list(self): def get_invoice_for_key(self, key: str): return self.wallet.get_request(key) - def get_invoice_as_dict(self, invoice: Invoice): + def get_invoice_as_dict(self, invoice: BaseInvoice): return self.wallet.export_request(invoice) @pyqtSlot(str, int) From 479f952c9d50eb651f1b9962ce93b8b7e6648b59 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Apr 2023 10:23:30 +0200 Subject: [PATCH 0617/1143] follow-up ba2faa8. fixes #8294 --- electrum/gui/qml/qeinvoicelistmodel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 2ab962b83..943273b94 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -74,8 +74,8 @@ def init_model(self): def add_invoice(self, invoice: BaseInvoice): # skip if already in list key = invoice.get_id() - for invoice in self.invoices: - if invoice['key'] == key: + for x in self.invoices: + if x['key'] == key: return item = self.invoice_to_model(invoice) From 8e3a3cefcf544aac0ffacadf773d1c850a9be5dc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Apr 2023 10:36:43 +0200 Subject: [PATCH 0618/1143] qt: do not mutate already saved invoice after editing amount. Also show empty an string for invoices that do not have an amount, similar to the requests list. --- electrum/gui/qt/invoice_list.py | 5 +++-- electrum/gui/qt/send_tab.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 785a73f9d..3e641e2fe 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -119,11 +119,12 @@ def update(self): icon_name = 'seal.png' status = self.wallet.get_invoice_status(item) amount = item.get_amount_sat() + amount_str = self.main_window.format_amount(amount, whitespaces=True) if amount else "" timestamp = item.time or 0 labels = [""] * len(self.Columns) labels[self.Columns.DATE] = format_time(timestamp) if timestamp else _('Unknown') labels[self.Columns.DESCRIPTION] = item.message - labels[self.Columns.AMOUNT] = self.main_window.format_amount(amount, whitespaces=True) + labels[self.Columns.AMOUNT] = amount_str labels[self.Columns.STATUS] = item.get_status_str(status) items = [QStandardItem(e) for e in labels] self.set_editability(items) @@ -182,7 +183,7 @@ def create_menu(self, position): if bool(invoice.get_amount_sat()): menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_pay_invoice(invoice)) else: - menu.addAction(_("Edit amount") + "...", lambda: self.send_tab.do_edit_invoice(invoice)) + menu.addAction(_("Pay") + "...", lambda: self.send_tab.do_edit_invoice(invoice)) if status == PR_FAILED: menu.addAction(_("Retry"), lambda: self.send_tab.do_pay_invoice(invoice)) if self.wallet.lnworker: diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index cec6b6144..825c9aa69 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -585,6 +585,8 @@ def do_edit_invoice(self, invoice: 'Invoice'): text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address() self.payto_e._on_input_btn(text) self.amount_e.setFocus() + # disable save button, because it would create a new invoice + self.save_button.setEnabled(False) def do_pay_invoice(self, invoice: 'Invoice'): if not bool(invoice.get_amount_sat()): From 2f9ecf3311d2e1cf5681786f49634a8d11086763 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 4 Apr 2023 13:22:58 +0200 Subject: [PATCH 0619/1143] qml: combine Connections on same target --- electrum/gui/qml/components/WalletMainView.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index dee814555..776264432 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -281,9 +281,6 @@ Item { var dialog = app.messageDialog.createObject(app, {text: error}) dialog.open() } - } - Connections { - target: Daemon.currentWallet function onOtpRequested() { console.log('OTP requested') var dialog = otpDialog.createObject(mainView) From 446879ade06d67490e6e6eea437bf7073c1b5802 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 13:37:10 +0000 Subject: [PATCH 0620/1143] lnwatcher.maybe_redeem: wanted_height should always be absolute previously, if prev_height.height was <= 0, lnwatcher was calling adb.set_future_tx() with weird wanted_height values (with ~sweep_info.csv_delay) --- electrum/address_synchronizer.py | 6 ++++-- electrum/lnwatcher.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index ea969efbf..e54b176c7 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -79,7 +79,7 @@ def __init__(self, db: 'WalletDB', config: 'SimpleConfig', *, name: str = None): # locks: if you need to take multiple ones, acquire them in the order they are defined here! self.lock = threading.RLock() self.transaction_lock = threading.RLock() - self.future_tx = {} # type: Dict[str, int] # txid -> wanted height + self.future_tx = {} # type: Dict[str, int] # txid -> wanted (abs) height # Txs the server claims are mined but still pending verification: self.unverified_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock. # Txs the server claims are in the mempool: @@ -655,7 +655,9 @@ def get_local_height(self) -> int: return cached_local_height return self.network.get_local_height() if self.network else self.db.get('stored_height', 0) - def set_future_tx(self, txid:str, wanted_height: int): + def set_future_tx(self, txid: str, *, wanted_height: int): + # note: wanted_height is always an absolute height, even in case of CSV-locked txs. + # In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess. with self.lock: self.future_tx[txid] = wanted_height diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index 51b741b42..deeb46492 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -514,13 +514,18 @@ async def maybe_redeem(self, spenders, prevout, sweep_info: 'SweepInfo', name: s wanted_height = sweep_info.cltv_expiry if wanted_height - local_height > 0: can_broadcast = False - reason = 'waiting for {}: CLTV ({} > {})'.format(name, local_height, sweep_info.cltv_expiry) + # self.logger.debug(f"pending redeem for {prevout}. waiting for {name}: CLTV ({local_height=}, {wanted_height=})") if sweep_info.csv_delay: prev_height = self.adb.get_tx_height(prev_txid) - wanted_height = sweep_info.csv_delay + prev_height.height - 1 - if prev_height.height <= 0 or wanted_height - local_height > 0: + if prev_height.height > 0: + wanted_height = prev_height.height + sweep_info.csv_delay - 1 + else: + wanted_height = local_height + sweep_info.csv_delay + if wanted_height - local_height > 0: can_broadcast = False - reason = 'waiting for {}: CSV ({} >= {})'.format(name, prev_height.conf, sweep_info.csv_delay) + # self.logger.debug( + # f"pending redeem for {prevout}. waiting for {name}: CSV " + # f"({local_height=}, {wanted_height=}, {prev_height.height=}, {sweep_info.csv_delay=})") if can_broadcast: self.logger.info(f'we can broadcast: {name}') tx_was_added = await self.network.try_broadcasting(new_tx, name) @@ -536,8 +541,9 @@ async def maybe_redeem(self, spenders, prevout, sweep_info: 'SweepInfo', name: s self.logger.info(f'added redeem tx: {name}. prevout: {prevout}') else: tx_was_added = False - # set future tx regardless of tx_was_added, because it is not persisted - self.adb.set_future_tx(new_tx.txid(), wanted_height) + # set future tx regardless of tx_was_added, because it is not persisted + # (and wanted_height can change if input of CSV was not mined before) + self.adb.set_future_tx(new_tx.txid(), wanted_height=wanted_height) if tx_was_added: self.lnworker.wallet.set_label(new_tx.txid(), name) if old_tx and old_tx.txid() != new_tx.txid(): From 9097d5e43d266325dacd14c8a9aefacb5bebadc4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 13:55:12 +0000 Subject: [PATCH 0621/1143] addr_sync.set_future_tx: clarify wanted_height off-by-one semantics --- electrum/address_synchronizer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index e54b176c7..810621e7a 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -656,8 +656,11 @@ def get_local_height(self) -> int: return self.network.get_local_height() if self.network else self.db.get('stored_height', 0) def set_future_tx(self, txid: str, *, wanted_height: int): - # note: wanted_height is always an absolute height, even in case of CSV-locked txs. - # In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess. + """Mark a local tx as "future" (encumbered by a timelock). + wanted_height is the min (abs) block height at which the tx can get into the mempool (be broadcast). + note: tx becomes consensus-valid to be mined in a block at height wanted_height+1 + In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess. + """ with self.lock: self.future_tx[txid] = wanted_height From 5d0d07c2b30c952d71fbeec87946442a4abd3d64 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 14:02:54 +0000 Subject: [PATCH 0622/1143] qml: QEWallet.broadcast: bring error msgs in line with qt --- electrum/gui/qml/qewallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 850bbf60e..408a9d665 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -554,10 +554,10 @@ def broadcast_thread(): self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx)) except TxBroadcastError as e: self._logger.error(repr(e)) - self.broadcastFailed.emit(tx.txid(),'',str(e)) + self.broadcastFailed.emit(tx.txid(), '', e.get_message_for_gui()) except BestEffortRequestFailed as e: self._logger.error(repr(e)) - self.broadcastFailed.emit(tx.txid(),'',str(e)) + self.broadcastFailed.emit(tx.txid(), '', repr(e)) else: self._logger.info('broadcast success') self.broadcastSucceeded.emit(tx.txid()) From 6c65161d27100f074b7654371447d051f2a0d0de Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 4 Apr 2023 16:13:00 +0200 Subject: [PATCH 0623/1143] qml: refactor qeinvoice.py QEInvoice/QEInvoiceParser now properly split for mapping to Invoice type (QEInvoice) and parsing/resolving of payment identifiers (QEInvoiceParser). additionally, old, unused QEUserEnteredPayment was removed. invoices are now never saved with user-entered amount if the original invoice did not specify an amount (e.g. address-only, no-amount bip21 uri, or no-amount lightning invoice). Furthermore, QEInvoice now adds an isSaved property so the UI doesn't need to infer that from the existence of the invoice key. Payments of lightning invoices are now triggered through QEInvoice.pay_lightning_invoice(), using the internally kept Invoice instance. This replaces the old call path of QEWallet.pay_lightning_invoice(invoice_key) which required the invoice to be saved in the backend wallet before payment. The LNURLpay flow arriving on InvoiceDialog implicitly triggered payment, this is now indicated by InvoiceDialog.payImmediately property instead of inferrred from the QEInvoiceParser isLnurlPay property. --- electrum/gui/qml/components/InvoiceDialog.qml | 27 +- .../gui/qml/components/WalletMainView.qml | 28 +- electrum/gui/qml/qeapp.py | 3 +- electrum/gui/qml/qeinvoice.py | 454 +++++++----------- electrum/gui/qml/qewallet.py | 10 +- 5 files changed, 210 insertions(+), 312 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index 0518214f2..cb86a2c8d 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -11,7 +11,7 @@ ElDialog { id: dialog property Invoice invoice - property string invoice_key + property bool payImmediately: false signal doPay signal invoiceAmountChanged @@ -392,13 +392,13 @@ ElDialog { Layout.preferredWidth: 1 text: qsTr('Save') icon.source: '../../icons/save.png' - enabled: invoice_key == '' && invoice.canSave + enabled: !invoice.isSaved && invoice.canSave onClicked: { - app.stack.push(Qt.resolvedUrl('Invoices.qml')) if (invoice.amount.isEmpty) { - invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) + invoice.amountOverride = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) } invoice.save_invoice() + app.stack.push(Qt.resolvedUrl('Invoices.qml')) dialog.close() } } @@ -410,15 +410,10 @@ ElDialog { enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay onClicked: { if (invoice.amount.isEmpty) { - invoice.amount = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) - if (invoice_key != '') { - // delete the existing invoice because this affects get_id() - invoice.wallet.delete_invoice(invoice_key) - invoice_key = '' - } + invoice.amountOverride = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) } - if (invoice_key == '') { - // save invoice if new or modified + if (!invoice.isSaved) { + // save invoice if newly parsed invoice.save_invoice() } doPay() // only signal here @@ -429,18 +424,14 @@ ElDialog { } Component.onCompleted: { - if (invoice_key != '') { - invoice.initFromKey(invoice_key) - } if (invoice.amount.isEmpty && !invoice.status == Invoice.Expired) { amountContainer.editmode = true } else if (invoice.amount.isMax) { amountMax.checked = true } - if (invoice.isLnurlPay) { - // we arrive from a lnurl-pay confirm dialog where the user already indicated the intent to pay. + if (payImmediately) { if (invoice.canPay) { - if (invoice_key == '') { + if (!invoice.isSaved) { invoice.save_invoice() } doPay() diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 776264432..30d56158f 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -21,7 +21,8 @@ Item { property string _request_expiry function openInvoice(key) { - var dialog = invoiceDialog.createObject(app, { invoice: invoiceParser, invoice_key: key }) + invoice.key = key + var dialog = invoiceDialog.createObject(app, { invoice: invoice }) dialog.open() return dialog } @@ -195,6 +196,7 @@ Item { Config.userKnowsPressAndHold = true Daemon.currentWallet.delete_expired_requests() app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) + AppController.haptic() } } FlatButton { @@ -207,11 +209,17 @@ Item { onPressAndHold: { Config.userKnowsPressAndHold = true app.stack.push(Qt.resolvedUrl('Invoices.qml')) + AppController.haptic() } } } } + Invoice { + id: invoice + wallet: Daemon.currentWallet + } + InvoiceParser { id: invoiceParser wallet: Daemon.currentWallet @@ -232,7 +240,7 @@ Item { } onValidationSuccess: { closeSendDialog() - var dialog = invoiceDialog.createObject(app, { invoice: invoiceParser }) + var dialog = invoiceDialog.createObject(app, { invoice: invoiceParser, payImmediately: invoiceParser.isLnurlPay }) dialog.open() } onInvoiceCreateError: console.log(code + ' ' + message) @@ -307,10 +315,16 @@ Item { height: parent.height onDoPay: { - if (invoice.invoiceType == Invoice.OnchainInvoice || (invoice.invoiceType == Invoice.LightningInvoice && invoice.amount.satsInt > Daemon.currentWallet.lightningCanSend ) ) { + if (invoice.invoiceType == Invoice.OnchainInvoice + || (invoice.invoiceType == Invoice.LightningInvoice + && invoice.amountOverride.isEmpty + ? invoice.amount.satsInt > Daemon.currentWallet.lightningCanSend + : invoice.amountOverride.satsInt > Daemon.currentWallet.lightningCanSend + )) + { var dialog = confirmPaymentDialog.createObject(mainView, { address: invoice.address, - satoshis: invoice.amount, + satoshis: invoice.amountOverride.isEmpty ? invoice.amount : invoice.amountOverride, message: invoice.message }) var canComplete = !Daemon.currentWallet.isWatchOnly && Daemon.currentWallet.canSignWithoutCosigner @@ -328,11 +342,7 @@ Item { dialog.open() } else if (invoice.invoiceType == Invoice.LightningInvoice) { console.log('About to pay lightning invoice') - if (invoice.key == '') { - console.log('No invoice key, aborting') - return - } - Daemon.currentWallet.pay_lightning_invoice(invoice.key) + invoice.pay_lightning_invoice() } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index e6a8a49a5..31f62b4d1 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -29,7 +29,7 @@ from .qebitcoin import QEBitcoin from .qefx import QEFX from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller -from .qeinvoice import QEInvoice, QEInvoiceParser, QEUserEnteredPayment +from .qeinvoice import QEInvoice, QEInvoiceParser from .qerequestdetails import QERequestDetails from .qetypes import QEAmount from .qeaddressdetails import QEAddressDetails @@ -315,7 +315,6 @@ def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: ' qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer') qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice') qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser') - qmlRegisterType(QEUserEnteredPayment, 'org.electrum', 1, 0, 'UserEnteredPayment') qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails') qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails') qmlRegisterType(QEChannelOpener, 'org.electrum', 1, 0, 'ChannelOpener') diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 2b93dab37..917a1b6b2 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -26,7 +26,7 @@ from .util import status_update_timer_interval, QtEventListener, event_listener -class QEInvoice(QObject): +class QEInvoice(QObject, QtEventListener): class Type: Invalid = -1 OnchainInvoice = 0 @@ -48,118 +48,31 @@ class Status: _logger = get_logger(__name__) - def __init__(self, parent=None): - super().__init__(parent) - - self._wallet = None # type: Optional[QEWallet] - self._canSave = False - self._canPay = False - self._key = None - - walletChanged = pyqtSignal() - @pyqtProperty(QEWallet, notify=walletChanged) - def wallet(self): - return self._wallet - - @wallet.setter - def wallet(self, wallet: QEWallet): - if self._wallet != wallet: - self._wallet = wallet - self.walletChanged.emit() - - canSaveChanged = pyqtSignal() - @pyqtProperty(bool, notify=canSaveChanged) - def canSave(self): - return self._canSave - - @canSave.setter - def canSave(self, canSave): - if self._canSave != canSave: - self._canSave = canSave - self.canSaveChanged.emit() - - canPayChanged = pyqtSignal() - @pyqtProperty(bool, notify=canPayChanged) - def canPay(self): - return self._canPay - - @canPay.setter - def canPay(self, canPay): - if self._canPay != canPay: - self._canPay = canPay - self.canPayChanged.emit() - - keyChanged = pyqtSignal() - @pyqtProperty(str, notify=keyChanged) - def key(self): - return self._key - - @key.setter - def key(self, key): - if self._key != key: - self._key = key - self.keyChanged.emit() - - userinfoChanged = pyqtSignal() - @pyqtProperty(str, notify=userinfoChanged) - def userinfo(self): - return self._userinfo - - @userinfo.setter - def userinfo(self, userinfo): - if self._userinfo != userinfo: - self._userinfo = userinfo - self.userinfoChanged.emit() - - def get_max_spendable_onchain(self): - spendable = self._wallet.confirmedBalance.satsInt - if not self._wallet.wallet.config.get('confirmed_only', False): - spendable += self._wallet.unconfirmedBalance.satsInt - return spendable - - def get_max_spendable_lightning(self): - return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0 - -class QEInvoiceParser(QEInvoice, QtEventListener): - _logger = get_logger(__name__) - invoiceChanged = pyqtSignal() invoiceSaved = pyqtSignal([str], arguments=['key']) - - validationSuccess = pyqtSignal() - validationWarning = pyqtSignal([str,str], arguments=['code', 'message']) - validationError = pyqtSignal([str,str], arguments=['code', 'message']) - - invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) - - lnurlRetrieved = pyqtSignal() - lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) - amountOverrideChanged = pyqtSignal() - _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['pr']) - def __init__(self, parent=None): super().__init__(parent) + self._wallet = None # type: Optional[QEWallet] + self._isSaved = False + self._canSave = False + self._canPay = False + self._key = None self._invoiceType = QEInvoice.Type.Invalid - self._recipient = '' self._effectiveInvoice = None - self._amount = QEAmount() self._userinfo = '' self._lnprops = {} + self._amount = QEAmount() + self._amountOverride = QEAmount() self._timer = QTimer(self) self._timer.setSingleShot(True) self._timer.timeout.connect(self.updateStatusString) - self._amountOverride = QEAmount() self._amountOverride.valueChanged.connect(self._on_amountoverride_value_changed) - self._bip70PrResolvedSignal.connect(self._bip70_payment_request_resolved) - - self.clear() - self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) @@ -188,39 +101,40 @@ def on_event_invoice_status(self, wallet, key, status): self.determine_can_pay() self.userinfo = _('In progress...') + walletChanged = pyqtSignal() + @pyqtProperty(QEWallet, notify=walletChanged) + def wallet(self): + return self._wallet + + @wallet.setter + def wallet(self, wallet: QEWallet): + if self._wallet != wallet: + self._wallet = wallet + self.walletChanged.emit() + @pyqtProperty(int, notify=invoiceChanged) def invoiceType(self): return self._invoiceType # not a qt setter, don't let outside set state - def setInvoiceType(self, invoiceType: QEInvoice.Type): + def setInvoiceType(self, invoiceType: Type): self._invoiceType = invoiceType - recipientChanged = pyqtSignal() - @pyqtProperty(str, notify=recipientChanged) - def recipient(self): - return self._recipient - - @recipient.setter - def recipient(self, recipient: str): - self.canPay = False - self._recipient = recipient - self.amountOverride = QEAmount() - if recipient: - self.validateRecipient(recipient) - self.recipientChanged.emit() + @pyqtProperty(str, notify=invoiceChanged) + def message(self): + return self._effectiveInvoice.message if self._effectiveInvoice else '' - @pyqtProperty('QVariantMap', notify=lnurlRetrieved) - def lnurlData(self): - return self._lnurlData + @pyqtProperty('quint64', notify=invoiceChanged) + def time(self): + return self._effectiveInvoice.time if self._effectiveInvoice else 0 - @pyqtProperty(bool, notify=lnurlRetrieved) - def isLnurlPay(self): - return self._lnurlData is not None + @pyqtProperty('quint64', notify=invoiceChanged) + def expiration(self): + return self._effectiveInvoice.exp if self._effectiveInvoice else 0 @pyqtProperty(str, notify=invoiceChanged) - def message(self): - return self._effectiveInvoice.message if self._effectiveInvoice else '' + def address(self): + return self._effectiveInvoice.get_address() if self._effectiveInvoice else '' @pyqtProperty(QEAmount, notify=invoiceChanged) def amount(self): @@ -230,16 +144,6 @@ def amount(self): self._amount.copyFrom(QEAmount(from_invoice=self._effectiveInvoice)) return self._amount - @amount.setter - def amount(self, new_amount): - self._logger.debug(f'set new amount {repr(new_amount)}') - if self._effectiveInvoice: - self._effectiveInvoice.amount_msat = '!' if new_amount.isMax else int(new_amount.satsInt * 1000) - - self.update_userinfo() - self.determine_can_pay() - self.invoiceChanged.emit() - @pyqtProperty(QEAmount, notify=amountOverrideChanged) def amountOverride(self): return self._amountOverride @@ -255,14 +159,6 @@ def _on_amountoverride_value_changed(self): self.update_userinfo() self.determine_can_pay() - @pyqtProperty('quint64', notify=invoiceChanged) - def time(self): - return self._effectiveInvoice.time if self._effectiveInvoice else 0 - - @pyqtProperty('quint64', notify=invoiceChanged) - def expiration(self): - return self._effectiveInvoice.exp if self._effectiveInvoice else 0 - statusChanged = pyqtSignal() @pyqtProperty(int, notify=statusChanged) def status(self): @@ -277,9 +173,59 @@ def status_str(self): status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice) return self._effectiveInvoice.get_status_str(status) - @pyqtProperty(str, notify=invoiceChanged) - def address(self): - return self._effectiveInvoice.get_address() if self._effectiveInvoice else '' + isSavedChanged = pyqtSignal() + @pyqtProperty(bool, notify=isSavedChanged) + def isSaved(self): + return self._isSaved + + canSaveChanged = pyqtSignal() + @pyqtProperty(bool, notify=canSaveChanged) + def canSave(self): + return self._canSave + + @canSave.setter + def canSave(self, canSave): + if self._canSave != canSave: + self._canSave = canSave + self.canSaveChanged.emit() + + canPayChanged = pyqtSignal() + @pyqtProperty(bool, notify=canPayChanged) + def canPay(self): + return self._canPay + + @canPay.setter + def canPay(self, canPay): + if self._canPay != canPay: + self._canPay = canPay + self.canPayChanged.emit() + + keyChanged = pyqtSignal() + @pyqtProperty(str, notify=keyChanged) + def key(self): + return self._key + + @key.setter + def key(self, key): + if self._key != key: + self._key = key + if self._effectiveInvoice and self._effectiveInvoice.get_id() == key: + return + invoice = self._wallet.wallet.get_invoice(key) + self._logger.debug(f'invoice from key {key}: {repr(invoice)}') + self.set_effective_invoice(invoice) + self.keyChanged.emit() + + userinfoChanged = pyqtSignal() + @pyqtProperty(str, notify=userinfoChanged) + def userinfo(self): + return self._userinfo + + @userinfo.setter + def userinfo(self, userinfo): + if self._userinfo != userinfo: + self._userinfo = userinfo + self.userinfoChanged.emit() @pyqtProperty('QVariantMap', notify=invoiceChanged) def lnprops(self): @@ -304,38 +250,19 @@ def set_lnprops(self): } def name_for_node_id(self, node_id): - node_alias = self._wallet.wallet.lnworker.get_node_alias(node_id) or node_id.hex() - return node_alias - - @pyqtSlot() - def clear(self): - self.recipient = '' - self.setInvoiceType(QEInvoice.Type.Invalid) - self._bip21 = None - self._lnurlData = None - self.canSave = False - self.canPay = False - self.userinfo = '' - self.invoiceChanged.emit() - - # don't parse the recipient string, but init qeinvoice from an invoice key - # this should not emit validation signals - @pyqtSlot(str) - def initFromKey(self, key): - self.clear() - invoice = self._wallet.wallet.get_invoice(key) - self._logger.debug(repr(invoice)) - if invoice: - self.set_effective_invoice(invoice) - self.key = key + return self._wallet.wallet.lnworker.get_node_alias(node_id) or node_id.hex() def set_effective_invoice(self, invoice: Invoice): self._effectiveInvoice = invoice - if invoice.is_lightning(): - self.setInvoiceType(QEInvoice.Type.LightningInvoice) + if invoice is None: + self.setInvoiceType(QEInvoice.Type.Invalid) else: - self.setInvoiceType(QEInvoice.Type.OnchainInvoice) + if invoice.is_lightning(): + self.setInvoiceType(QEInvoice.Type.LightningInvoice) + else: + self.setInvoiceType(QEInvoice.Type.OnchainInvoice) + self._isSaved = self._wallet.wallet.get_invoice(invoice.get_id()) is not None self.set_lnprops() @@ -344,6 +271,7 @@ def set_effective_invoice(self, invoice: Invoice): self.invoiceChanged.emit() self.statusChanged.emit() + self.isSavedChanged.emit() self.set_status_timer() @@ -440,6 +368,86 @@ def determine_can_pay(self): # TODO: subtract fee? self.canPay = True + @pyqtSlot() + def pay_lightning_invoice(self): + if not self.canPay: + raise Exception('can not pay invoice, canPay is false') + + if self.invoiceType != QEInvoice.Type.LightningInvoice: + raise Exception('pay_lightning_invoice can only pay lightning invoices') + + if self.amount.isEmpty: + if self.amountOverride.isEmpty: + raise Exception('can not pay 0 amount') + # TODO: is update amount_msat for overrideAmount sufficient? + self._effectiveInvoice.amount_msat = self.amountOverride.satsInt * 1000 + + self._wallet.pay_lightning_invoice(self._effectiveInvoice) + + def get_max_spendable_onchain(self): + spendable = self._wallet.confirmedBalance.satsInt + if not self._wallet.wallet.config.get('confirmed_only', False): + spendable += self._wallet.unconfirmedBalance.satsInt + return spendable + + def get_max_spendable_lightning(self): + return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0 + +class QEInvoiceParser(QEInvoice): + _logger = get_logger(__name__) + + validationSuccess = pyqtSignal() + validationWarning = pyqtSignal([str,str], arguments=['code', 'message']) + validationError = pyqtSignal([str,str], arguments=['code', 'message']) + + invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) + + lnurlRetrieved = pyqtSignal() + lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) + + _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['pr']) + + def __init__(self, parent=None): + super().__init__(parent) + + self._recipient = '' + self._bip70PrResolvedSignal.connect(self._bip70_payment_request_resolved) + + self.clear() + + recipientChanged = pyqtSignal() + @pyqtProperty(str, notify=recipientChanged) + def recipient(self): + return self._recipient + + @recipient.setter + def recipient(self, recipient: str): + self.canPay = False + self._recipient = recipient + self.amountOverride = QEAmount() + if recipient: + self.validateRecipient(recipient) + self.recipientChanged.emit() + + @pyqtProperty('QVariantMap', notify=lnurlRetrieved) + def lnurlData(self): + return self._lnurlData + + @pyqtProperty(bool, notify=lnurlRetrieved) + def isLnurlPay(self): + return self._lnurlData is not None + + @pyqtSlot() + def clear(self): + self.recipient = '' + self.setInvoiceType(QEInvoice.Type.Invalid) + self._bip21 = None + self._lnurlData = None + self.canSave = False + self.canPay = False + self.userinfo = '' + self.invoiceChanged.emit() + def setValidOnchainInvoice(self, invoice: Invoice): self._logger.debug('setValidOnchainInvoice') if invoice.is_lightning(): @@ -641,118 +649,14 @@ def on_lnurl_invoice(self, orig_amount, invoice): @pyqtSlot() def save_invoice(self): - self.canSave = False if not self._effectiveInvoice: return - - self.key = self._effectiveInvoice.get_id() - if self._wallet.wallet.get_invoice(self.key): - self._logger.info(f'invoice {self.key} already exists') - else: - self._wallet.wallet.save_invoice(self._effectiveInvoice) - self._wallet.invoiceModel.addInvoice(self.key) - self.invoiceSaved.emit(self.key) - - -class QEUserEnteredPayment(QEInvoice): - _logger = get_logger(__name__) - - validationError = pyqtSignal([str,str], arguments=['code','message']) - invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) - invoiceSaved = pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent) - - self._amount = QEAmount() - self.clear() - - recipientChanged = pyqtSignal() - @pyqtProperty(str, notify=recipientChanged) - def recipient(self): - return self._recipient - - @recipient.setter - def recipient(self, recipient: str): - if self._recipient != recipient: - self._recipient = recipient - self.validate() - self.recipientChanged.emit() - - messageChanged = pyqtSignal() - @pyqtProperty(str, notify=messageChanged) - def message(self): - return self._message - - @message.setter - def message(self, message): - if self._message != message: - self._message = message - self.messageChanged.emit() - - amountChanged = pyqtSignal() - @pyqtProperty(QEAmount, notify=amountChanged) - def amount(self): - return self._amount - - @amount.setter - def amount(self, amount): - if self._amount != amount: - self._amount.copyFrom(amount) - self.validate() - self.amountChanged.emit() - - - def validate(self): - self.canPay = False - self.canSave = False - self._logger.debug('validate') - - if not self._recipient: - self.validationError.emit('recipient', _('Recipient not specified.')) - return - - if not bitcoin.is_address(self._recipient): - self.validationError.emit('recipient', _('Invalid Bitcoin address')) - return - - self.canSave = True - - if self._amount.isEmpty: - self.validationError.emit('amount', _('Invalid amount')) + if self.isSaved: return - if self._amount.isMax: - self.canPay = True - else: - if self.get_max_spendable_onchain() >= self._amount.satsInt: - self.canPay = True - - @pyqtSlot() - def save_invoice(self): - assert self.canSave - assert not self._amount.isMax - - self._logger.debug('saving invoice to %s, amount=%s, message=%s' % (self._recipient, repr(self._amount), self._message)) - - inv_amt = self._amount.satsInt - - try: - outputs = [PartialTxOutput.from_address_and_value(self._recipient, inv_amt)] - self._logger.debug(repr(outputs)) - invoice = self._wallet.wallet.create_invoice(outputs=outputs, message=self._message, pr=None, URI=None) - except InvoiceError as e: - self.invoiceCreateError.emit('fatal', _('Error creating payment') + ':\n' + str(e)) - return - - self.key = invoice.get_id() - self._wallet.wallet.save_invoice(invoice) - self.invoiceSaved.emit() - - @pyqtSlot() - def clear(self): - self._recipient = None - self._amount.clear() - self._message = None self.canSave = False - self.canPay = False + + self.key = self._effectiveInvoice.get_id() + self._wallet.wallet.save_invoice(self._effectiveInvoice) + self._wallet.invoiceModel.addInvoice(self.key) + self.invoiceSaved.emit(self.key) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 408a9d665..bc2701bc5 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -28,7 +28,7 @@ if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet - + from .qeinvoice import QEInvoice class QEWallet(AuthMixin, QObject, QtEventListener): __instances = [] @@ -587,14 +587,8 @@ def save_tx(self, tx: 'PartialTransaction'): def ln_auth_rejected(self): self.paymentAuthRejected.emit() - @pyqtSlot(str) @auth_protect(reject='ln_auth_rejected') - def pay_lightning_invoice(self, invoice_key): - self._logger.debug('about to pay LN') - invoice = self.wallet.get_invoice(invoice_key) - assert(invoice) - assert(invoice.lightning_invoice) - + def pay_lightning_invoice(self, invoice: 'QEInvoice'): amount_msat = invoice.get_amount_msat() def pay_thread(): From 793cbd1c6e110c5e3fed0b90bade4d6f097b35de Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 4 Apr 2023 17:47:37 +0200 Subject: [PATCH 0624/1143] qml: save with user entered amount --- electrum/gui/qml/qeinvoice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 917a1b6b2..f08650440 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -654,9 +654,12 @@ def save_invoice(self): if self.isSaved: return + if not self._effectiveInvoice.amount_msat and not self.amountOverride.isEmpty: + self._effectiveInvoice.amount_msat = self.amountOverride.satsInt * 1000 + self.canSave = False - self.key = self._effectiveInvoice.get_id() self._wallet.wallet.save_invoice(self._effectiveInvoice) + self.key = self._effectiveInvoice.get_id() self._wallet.invoiceModel.addInvoice(self.key) self.invoiceSaved.emit(self.key) From cf3613b7d54fed5b14bbe083fe8ea93522996843 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 4 Apr 2023 17:59:40 +0200 Subject: [PATCH 0625/1143] qml: handle max too --- electrum/gui/qml/qeinvoice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index f08650440..af6a7d524 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -655,7 +655,10 @@ def save_invoice(self): return if not self._effectiveInvoice.amount_msat and not self.amountOverride.isEmpty: - self._effectiveInvoice.amount_msat = self.amountOverride.satsInt * 1000 + if self.invoiceType == QEInvoice.Type.OnchainInvoice and self.amountOverride.isMax: + self._effectiveInvoice.amount_msat = '!' + else: + self._effectiveInvoice.amount_msat = self.amountOverride.satsInt * 1000 self.canSave = False From 159646fe54720dc76aa77413e94f64d868d6666d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Apr 2023 16:55:06 +0200 Subject: [PATCH 0626/1143] Set status of onchain invoices to PR_INFLIGHT while tx is being broadcast --- electrum/gui/qml/qeinvoice.py | 6 ++--- electrum/gui/qml/qewallet.py | 2 ++ electrum/gui/qt/send_tab.py | 3 +++ electrum/invoices.py | 1 + electrum/wallet.py | 44 ++++++++++++++++++++++++++--------- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index af6a7d524..38cdc7e90 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -96,10 +96,9 @@ def on_event_payment_failed(self, wallet, key, reason): @event_listener def on_event_invoice_status(self, wallet, key, status): if wallet == self._wallet.wallet and key == self.key: + self.update_userinfo() + self.determine_can_pay() self.statusChanged.emit() - if status in [PR_INFLIGHT, PR_ROUTING]: - self.determine_can_pay() - self.userinfo = _('In progress...') walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) @@ -331,6 +330,7 @@ def update_userinfo(self): self.userinfo = { PR_EXPIRED: _('This invoice has expired'), PR_PAID: _('This invoice was already paid'), + PR_INFLIGHT: _('Payment in progress...') + ' (' + _('broadcasting') + ')', PR_UNCONFIRMED: _('Payment in progress...') + ' (' + _('waiting for confirmation') + ')', PR_UNKNOWN: _('Invoice has unknown status'), }[self.status] diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index bc2701bc5..2fa19e674 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -549,6 +549,7 @@ def broadcast(self, tx): assert tx.is_complete() def broadcast_thread(): + self.wallet.set_broadcasting(tx, True) try: self._logger.info('running broadcast in thread') self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx)) @@ -562,6 +563,7 @@ def broadcast_thread(): self._logger.info('broadcast success') self.broadcastSucceeded.emit(tx.txid()) self.historyModel.requestRefresh.emit() # via qt thread + self.wallet.set_broadcasting(tx, False) threading.Thread(target=broadcast_thread, daemon=True).start() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 825c9aa69..7e3c23966 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -761,7 +761,10 @@ def broadcast_thread(): # Capture current TL window; override might be removed on return parent = self.window.top_level_window(lambda win: isinstance(win, MessageBoxMixin)) + self.wallet.set_broadcasting(tx, True) + def broadcast_done(result): + self.wallet.set_broadcasting(tx, False) # GUI thread if result: success, msg = result diff --git a/electrum/invoices.py b/electrum/invoices.py index 1bd10292b..47ea6aa0a 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -243,6 +243,7 @@ def as_dict(self, status): class Invoice(BaseInvoice): lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] __lnaddr = None + _is_broadcasting = False def is_lightning(self): return self.lightning_invoice is not None diff --git a/electrum/wallet.py b/electrum/wallet.py index d246c4caa..6723898e1 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -75,7 +75,7 @@ from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) from .invoices import BaseInvoice, Invoice, Request -from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED +from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT from .contacts import Contacts from .interface import NetworkException from .mnemonic import Mnemonic @@ -2405,6 +2405,8 @@ def check_expired_status(self, r: BaseInvoice, status): def get_invoice_status(self, invoice: BaseInvoice): """Returns status of (incoming) request or (outgoing) invoice.""" + if isinstance(invoice, Invoice) and invoice._is_broadcasting: + return PR_INFLIGHT # lightning invoices can be paid onchain if invoice.is_lightning() and self.lnworker: status = self.lnworker.get_invoice_status(invoice) @@ -2496,6 +2498,18 @@ def export_invoice(self, x: Invoice) -> Dict[str, Any]: d['bip70'] = x.bip70 return d + def get_invoices_and_requests_touched_by_tx(self, tx): + request_keys = set() + invoice_keys = set() + with self.lock, self.transaction_lock: + for txo in tx.outputs(): + addr = txo.address + if request:=self.get_request_by_addr(addr): + request_keys.add(request.get_id()) + for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()): + invoice_keys.add(invoice_key) + return request_keys, invoice_keys + def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None: # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called. # For a given receive request, if tx1 touches it but tx2 does not, then @@ -2503,16 +2517,24 @@ def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None: tx = self.db.get_transaction(tx_hash) if tx is None: return - relevant_invoice_keys = set() - with self.lock, self.transaction_lock: - for txo in tx.outputs(): - addr = txo.address - if request:=self.get_request_by_addr(addr): - status = self.get_invoice_status(request) - util.trigger_callback('request_status', self, request.get_id(), status) - for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()): - relevant_invoice_keys.add(invoice_key) - self._update_onchain_invoice_paid_detection(relevant_invoice_keys) + request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx) + for key in request_keys: + request = self.get_request(key) + if not request: + continue + status = self.get_invoice_status(request) + util.trigger_callback('request_status', self, request.get_id(), status) + self._update_onchain_invoice_paid_detection(invoice_keys) + + def set_broadcasting(self, tx: Transaction, b: bool): + request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx) + for key in invoice_keys: + invoice = self._invoices.get(key) + if not invoice: + continue + invoice._is_broadcasting = b + status = self.get_invoice_status(invoice) + util.trigger_callback('invoice_status', self, key, status) def get_bolt11_invoice(self, req: Request) -> str: if not self.lnworker: From f04e2e2e6ffaade73d543c7e6e7b848db50eed98 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 4 Apr 2023 19:31:19 +0200 Subject: [PATCH 0627/1143] Add an extra state for invoices where our tx has been broadcast successfully, but it is not in our history yet. (follow-up 159646fe54720dc76aa77413e94f64d868d6666d) --- electrum/gui/qml/qeinvoice.py | 7 ++++--- electrum/gui/qml/qewallet.py | 8 +++++--- electrum/gui/qt/send_tab.py | 7 ++++--- electrum/gui/qt/util.py | 4 +++- electrum/invoices.py | 14 +++++++++++--- electrum/wallet.py | 10 +++++++--- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 38cdc7e90..a17ed9d05 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -10,7 +10,7 @@ from electrum.i18n import _ from electrum.invoices import Invoice from electrum.invoices import (PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, - PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, LN_EXPIRY_NEVER) + PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER) from electrum.lnaddr import LnInvoiceException from electrum.logging import get_logger from electrum.transaction import PartialTxOutput @@ -95,7 +95,7 @@ def on_event_payment_failed(self, wallet, key, reason): @event_listener def on_event_invoice_status(self, wallet, key, status): - if wallet == self._wallet.wallet and key == self.key: + if self._wallet and wallet == self._wallet.wallet and key == self.key: self.update_userinfo() self.determine_can_pay() self.statusChanged.emit() @@ -330,7 +330,8 @@ def update_userinfo(self): self.userinfo = { PR_EXPIRED: _('This invoice has expired'), PR_PAID: _('This invoice was already paid'), - PR_INFLIGHT: _('Payment in progress...') + ' (' + _('broadcasting') + ')', + PR_BROADCASTING: _('Payment in progress...') + ' (' + _('broadcasting') + ')', + PR_BROADCAST: _('Payment in progress...') + ' (' + _('broadcast successfully') + ')', PR_UNCONFIRMED: _('Payment in progress...') + ' (' + _('waiting for confirmation') + ')', PR_UNKNOWN: _('Invoice has unknown status'), }[self.status] diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 2fa19e674..72e45b437 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -9,7 +9,7 @@ from electrum import bitcoin from electrum.i18n import _ -from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID +from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_BROADCASTING, PR_BROADCAST from electrum.logging import get_logger from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.transaction import PartialTxOutput, PartialTransaction @@ -549,21 +549,23 @@ def broadcast(self, tx): assert tx.is_complete() def broadcast_thread(): - self.wallet.set_broadcasting(tx, True) + self.wallet.set_broadcasting(tx, PR_BROADCASTING) try: self._logger.info('running broadcast in thread') self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx)) except TxBroadcastError as e: self._logger.error(repr(e)) self.broadcastFailed.emit(tx.txid(), '', e.get_message_for_gui()) + self.wallet.set_broadcasting(tx, None) except BestEffortRequestFailed as e: self._logger.error(repr(e)) self.broadcastFailed.emit(tx.txid(), '', repr(e)) + self.wallet.set_broadcasting(tx, None) else: self._logger.info('broadcast success') self.broadcastSucceeded.emit(tx.txid()) self.historyModel.requestRefresh.emit() # via qt thread - self.wallet.set_broadcasting(tx, False) + self.wallet.set_broadcasting(tx, PR_BROADCAST) threading.Thread(target=broadcast_thread, daemon=True).start() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 7e3c23966..3412a2f1c 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -18,7 +18,7 @@ from electrum.util import (get_asyncio_loop, FailedToParsePaymentIdentifier, InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, NotEnoughFunds, NoDynamicFeeEstimates, InvoiceError, parse_max_spend) -from electrum.invoices import PR_PAID, Invoice +from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.transaction import Transaction, PartialTxInput, PartialTransaction, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.logging import Logger @@ -761,19 +761,20 @@ def broadcast_thread(): # Capture current TL window; override might be removed on return parent = self.window.top_level_window(lambda win: isinstance(win, MessageBoxMixin)) - self.wallet.set_broadcasting(tx, True) + self.wallet.set_broadcasting(tx, PR_BROADCASTING) def broadcast_done(result): - self.wallet.set_broadcasting(tx, False) # GUI thread if result: success, msg = result if success: parent.show_message(_('Payment sent.') + '\n' + msg) self.invoice_list.update() + self.wallet.set_broadcasting(tx, PR_BROADCAST) else: msg = msg or '' parent.show_error(msg) + self.wallet.set_broadcasting(tx, None) WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.window.on_error) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 5dd43e195..2e400e158 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -31,7 +31,7 @@ from electrum.i18n import _, languages from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path from electrum.util import EventListener, event_listener -from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED +from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST from electrum.logging import Logger from electrum.qrreader import MissingQrDetectionLib @@ -60,6 +60,8 @@ PR_FAILED:"warning.png", PR_ROUTING:"unconfirmed.png", PR_UNCONFIRMED:"unconfirmed.png", + PR_BROADCASTING:"unconfirmed.png", + PR_BROADCAST:"unconfirmed.png", } diff --git a/electrum/invoices.py b/electrum/invoices.py index 47ea6aa0a..c90ad75a6 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -29,7 +29,8 @@ PR_FAILED = 5 # only for LN. we attempted to pay it, but all attempts failed PR_ROUTING = 6 # only for LN. *unused* atm. PR_UNCONFIRMED = 7 # only onchain. invoice is satisfied but tx is not mined yet. - +PR_BROADCASTING = 8 # onchain, tx is being broadcast +PR_BROADCAST = 9 # onchain, tx was broadcast, is not yet in our history pr_color = { PR_UNPAID: (.7, .7, .7, 1), @@ -38,7 +39,9 @@ PR_EXPIRED: (.9, .2, .2, 1), PR_INFLIGHT: (.9, .6, .3, 1), PR_FAILED: (.9, .2, .2, 1), - PR_ROUTING: (.9, .6, .3, 1), + PR_ROUTING: (.9, .6, .3, 1), + PR_BROADCASTING: (.9, .6, .3, 1), + PR_BROADCAST: (.9, .6, .3, 1), PR_UNCONFIRMED: (.9, .6, .3, 1), } @@ -48,6 +51,8 @@ PR_UNKNOWN:_('Unknown'), PR_EXPIRED:_('Expired'), PR_INFLIGHT:_('In progress'), + PR_BROADCASTING:_('Broadcasting'), + PR_BROADCAST:_('Broadcast successfully'), PR_FAILED:_('Failed'), PR_ROUTING: _('Computing route...'), PR_UNCONFIRMED: _('Unconfirmed'), @@ -243,11 +248,14 @@ def as_dict(self, status): class Invoice(BaseInvoice): lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] __lnaddr = None - _is_broadcasting = False + _broadcasting_status = None # can be None or PR_BROADCASTING or PR_BROADCAST def is_lightning(self): return self.lightning_invoice is not None + def get_broadcasting_status(self): + return self._broadcasting_status + def get_address(self) -> Optional[str]: address = None if self.outputs: diff --git a/electrum/wallet.py b/electrum/wallet.py index 6723898e1..d53e2dddc 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1157,6 +1157,9 @@ def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> self._invoices_from_txid_map[txid].add(invoice_key) for txout in invoice.get_outputs(): self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key) + # update invoice status + status = self.get_invoice_status(invoice) + util.trigger_callback('invoice_status', self, invoice_key, status) def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]: """Returns whether on-chain invoice/request is satisfied, num confs required txs have, @@ -2405,8 +2408,6 @@ def check_expired_status(self, r: BaseInvoice, status): def get_invoice_status(self, invoice: BaseInvoice): """Returns status of (incoming) request or (outgoing) invoice.""" - if isinstance(invoice, Invoice) and invoice._is_broadcasting: - return PR_INFLIGHT # lightning invoices can be paid onchain if invoice.is_lightning() and self.lnworker: status = self.lnworker.get_invoice_status(invoice) @@ -2414,6 +2415,9 @@ def get_invoice_status(self, invoice: BaseInvoice): return self.check_expired_status(invoice, status) paid, conf = self.is_onchain_invoice_paid(invoice) if not paid: + if isinstance(invoice, Invoice): + if status:=invoice.get_broadcasting_status(): + return status status = PR_UNPAID elif conf == 0: status = PR_UNCONFIRMED @@ -2532,7 +2536,7 @@ def set_broadcasting(self, tx: Transaction, b: bool): invoice = self._invoices.get(key) if not invoice: continue - invoice._is_broadcasting = b + invoice._broadcasting_status = b status = self.get_invoice_status(invoice) util.trigger_callback('invoice_status', self, key, status) From 76f795bc9a7429090d1714867e1f069ab3a465d4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 15:52:18 +0000 Subject: [PATCH 0628/1143] kivy: history screen: fix "future" txs ``` 3.96 | E | gui.kivy.uix.dialogs.crash_reporter.ExceptionHook | exception caught by crash reporter Traceback (most recent call last): File "kivy/_clock.pyx", line 649, in kivy._clock.CyClockBase._process_events File "kivy/_clock.pyx", line 218, in kivy._clock.ClockEvent.tick File "/home/user/wspace/electrum/electrum/gui/kivy/main_window.py", line 1045, in update_wallet self.update_tabs() File "/home/user/wspace/electrum/electrum/util.py", line 462, in return lambda *args, **kw_args: do_profile(args, kw_args) File "/home/user/wspace/electrum/electrum/util.py", line 458, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/gui/kivy/main_window.py", line 506, in update_tabs self.update_tab(name) File "/home/user/wspace/electrum/electrum/gui/kivy/main_window.py", line 501, in update_tab s.update() File "/home/user/wspace/electrum/electrum/gui/kivy/uix/screens.py", line 161, in update history_card.data = [self.get_card(item) for item in history] File "/home/user/wspace/electrum/electrum/gui/kivy/uix/screens.py", line 161, in history_card.data = [self.get_card(item) for item in history] File "/home/user/wspace/electrum/electrum/gui/kivy/uix/screens.py", line 132, in get_card status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_info) File "/home/user/wspace/electrum/electrum/wallet.py", line 1491, in get_tx_status num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height() TypeError: unsupported operand type(s) for -: 'NoneType' and 'int' ``` --- electrum/gui/kivy/uix/screens.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 6476e2e1a..1d6115138 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -126,9 +126,12 @@ def get_card(self, tx_item): #tx_hash, tx_mined_status, value, balance): fee_text = '' if fee is None else 'fee: %d sat'%fee else: tx_hash = tx_item['txid'] - tx_mined_info = TxMinedInfo(height=tx_item['height'], - conf=tx_item['confirmations'], - timestamp=tx_item['timestamp']) + tx_mined_info = TxMinedInfo( + height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp'], + wanted_height=tx_item.get('wanted_height', None), + ) status, status_str = self.app.wallet.get_tx_status(tx_hash, tx_mined_info) icon = f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/' + TX_ICONS[status] message = tx_item['label'] or tx_hash From f8f0af4a2fd00db547a90d26f2c98a4ea6f45bb0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 15:58:49 +0000 Subject: [PATCH 0629/1143] qml: history: add some support for future txs - they are now distinguished from local by the status text "in %d blocks" - this status text needs updating occasionally: on new blocks, and some lnwatcher interactions, hence new event: "adb_set_future_tx" --- electrum/address_synchronizer.py | 3 ++ electrum/gui/qml/qetransactionlistmodel.py | 52 ++++++++++++++++++---- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 810621e7a..88bce48fe 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -662,7 +662,10 @@ def set_future_tx(self, txid: str, *, wanted_height: int): In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess. """ with self.lock: + old_height = self.future_tx.get(txid) or None self.future_tx[txid] = wanted_height + if old_height != wanted_height: + util.trigger_callback('adb_set_future_tx', self, txid) def get_tx_height(self, tx_hash: str) -> TxMinedInfo: if tx_hash is None: # ugly backwards compat... diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 2f98fbd1d..9b88c6e61 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -6,6 +6,7 @@ from electrum.logging import get_logger from electrum.util import Satoshis, TxMinedInfo +from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL from .qetypes import QEAmount from .util import QtEventListener, qt_event_listener @@ -49,6 +50,18 @@ def on_event_verified(self, wallet, txid, info): self._logger.debug('verified event for txid %s' % txid) self.on_tx_verified(txid, info) + @qt_event_listener + def on_event_adb_set_future_tx(self, adb, txid): + if adb != self.wallet.adb: + return + self._logger.debug(f'adb_set_future_tx event for txid {txid}') + i = 0 + for item in self.tx_history: + if 'txid' in item and item['txid'] == txid: + self._update_future_txitem(i) + return + i = i + 1 + def rowCount(self, index): return len(self.tx_history) @@ -111,10 +124,13 @@ def tx_to_model(self, tx): item['complete'] = tx.is_complete() # newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp - if not item['timestamp']: + # FIXME just use wallet.get_tx_status, and change that as needed + if not item['timestamp']: # onchain: local or mempool or unverified txs txinfo = self.wallet.get_tx_info(tx) item['section'] = 'mempool' if item['complete'] and not txinfo.can_broadcast else 'local' - else: + status, status_str = self.wallet.get_tx_status(item['txid'], txinfo.tx_mined_status) + item['date'] = status_str + else: # lightning or already mined (and SPV-ed) onchain txs item['section'] = self.get_section_by_timestamp(item['timestamp']) item['date'] = self.format_date_by_section(item['section'], datetime.fromtimestamp(item['timestamp'])) @@ -191,6 +207,23 @@ def on_tx_verified(self, txid, info): return i = i + 1 + def _update_future_txitem(self, tx_item_idx: int): + tx_item = self.tx_history[tx_item_idx] + # note: local txs can transition to future, as "future" state is not persisted + if tx_item.get('height') not in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL): + return + txid = tx_item['txid'] + tx = self.wallet.db.get_transaction(txid) + if tx is None: + return + txinfo = self.wallet.get_tx_info(tx) + status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status) + tx_item['date'] = status_str + tx_item['height'] = self.wallet.adb.get_tx_height(txid).height + index = self.index(tx_item_idx, 0) + roles = [self._ROLE_RMAP[x] for x in ['height', 'date']] + self.dataChanged.emit(index, index, roles) + @pyqtSlot(str, str) def update_tx_label(self, key, label): i = 0 @@ -206,10 +239,13 @@ def update_tx_label(self, key, label): def updateBlockchainHeight(self, height): self._logger.debug('updating height to %d' % height) i = 0 - for tx in self.tx_history: - if 'height' in tx and tx['height'] > 0: - tx['confirmations'] = height - tx['height'] + 1 - index = self.index(i,0) - roles = [self._ROLE_RMAP['confirmations']] - self.dataChanged.emit(index, index, roles) + for tx_item in self.tx_history: + if 'height' in tx_item: + if tx_item['height'] > 0: + tx_item['confirmations'] = height - tx_item['height'] + 1 + index = self.index(i,0) + roles = [self._ROLE_RMAP['confirmations']] + self.dataChanged.emit(index, index, roles) + elif tx_item['height'] in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL): + self._update_future_txitem(i) i = i + 1 From f0e89b3ef600102e78849a4f5392b3d7add4e689 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 17:49:46 +0000 Subject: [PATCH 0630/1143] addr_sync: migrate usages of get_txpos to get_tx_height the return value of get_txpos is fine-tuned for sorting... other uses are highly questionable. --- electrum/address_synchronizer.py | 22 ++++++++++++---------- electrum/gui/qt/transaction_dialog.py | 4 +++- electrum/gui/qt/utxo_dialog.py | 6 ++++-- electrum/transaction.py | 4 ++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 88bce48fe..cc7e31b11 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -244,12 +244,11 @@ def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=Fa def get_transaction(self, txid: str) -> Optional[Transaction]: tx = self.db.get_transaction(txid) if tx: - # add verified tx info tx.deserialize() for txin in tx._inputs: - tx_height, tx_pos = self.get_txpos(txin.prevout.txid.hex()) - txin.block_height = tx_height - txin.block_txpos = tx_pos + tx_mined_info = self.get_tx_height(txin.prevout.txid.hex()) + txin.block_height = tx_mined_info.height # not SPV-ed + txin.block_txpos = tx_mined_info.txpos return tx def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool: @@ -477,8 +476,10 @@ def clear_history(self): self._history_local.clear() self._get_balance_cache.clear() # invalidate cache - def get_txpos(self, tx_hash: str) -> Tuple[int, int]: - """Returns (height, txpos) tuple, even if the tx is unverified.""" + def _get_txpos(self, tx_hash: str) -> Tuple[int, int]: + """Returns (height, txpos) tuple, even if the tx is unverified. + If txpos is -1, height should only be used for sorting purposes. + """ with self.lock: verified_tx_mined_info = self.db.get_verified_tx(tx_hash) if verified_tx_mined_info: @@ -529,7 +530,7 @@ def get_history(self, domain) -> Sequence[HistoryItem]: tx_mined_status = self.get_tx_height(tx_hash) fee = self.get_tx_fee(tx_hash) history.append((tx_hash, tx_mined_status, delta, fee)) - history.sort(key = lambda x: self.get_txpos(x[0])) + history.sort(key = lambda x: self._get_txpos(x[0])) # 3. add balance h2 = [] balance = 0 @@ -783,13 +784,14 @@ def get_addr_io(self, address): received = {} sent = {} for tx_hash, height in h: - hh, pos = self.get_txpos(tx_hash) + tx_mined_info = self.get_tx_height(tx_hash) + txpos = tx_mined_info.txpos if tx_mined_info.txpos is not None else -1 d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): - received[tx_hash + ':%d'%n] = (height, pos, v, is_cb) + received[tx_hash + ':%d'%n] = (height, txpos, v, is_cb) l = self.db.get_txi_addr(tx_hash, address) for txi, v in l: - sent[txi] = tx_hash, height, pos + sent[txi] = tx_hash, height, txpos return received, sent def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index de648ee38..e860e425c 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -246,7 +246,9 @@ def insert_tx_io( tx_height, tx_pos = None, None tx_hash = self.tx.txid() if tx_hash: - tx_height, tx_pos = self.wallet.adb.get_txpos(tx_hash) + tx_mined_info = self.wallet.adb.get_tx_height(tx_hash) + tx_height = tx_mined_info.height + tx_pos = tx_mined_info.txpos cursor = o_text.textCursor() for txout_idx, o in enumerate(self.tx.outputs()): if tx_height is not None and tx_pos is not None and tx_pos >= 0: diff --git a/electrum/gui/qt/utxo_dialog.py b/electrum/gui/qt/utxo_dialog.py index c714e41fc..e94eae3db 100644 --- a/electrum/gui/qt/utxo_dialog.py +++ b/electrum/gui/qt/utxo_dialog.py @@ -105,8 +105,10 @@ def update(self): def print_ascii_tree(_txid, prefix, is_last, is_uncle): if _txid not in parents: return - tx_height, tx_pos = self.wallet.adb.get_txpos(_txid) - key = "%dx%d"%(tx_height, tx_pos) if tx_pos >= 0 else _txid[0:8] + tx_mined_info = self.wallet.adb.get_tx_height(_txid) + tx_height = tx_mined_info.height + tx_pos = tx_mined_info.txpos + key = "%dx%d"%(tx_height, tx_pos) if tx_pos is not None else _txid[0:8] label = self.wallet.get_label_for_txid(_txid) or "" if _txid not in parents_copy: label = '[duplicate]' diff --git a/electrum/transaction.py b/electrum/transaction.py index aa9e264dc..0012468d8 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -260,8 +260,8 @@ def __init__(self, *, self.witness = witness self._is_coinbase_output = is_coinbase_output # blockchain fields - self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown - self.block_txpos = None + self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown. not SPV-ed. + self.block_txpos = None # type: Optional[int] # position of tx in block, if TXO is mined; otherwise None or -1 self.spent_height = None # type: Optional[int] # height at which the TXO got spent self.spent_txid = None # type: Optional[str] # txid of the spender self._utxo = None # type: Optional[Transaction] From db4943ff86c3e50c35846fc819a6e953e3b4aa24 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 4 Apr 2023 18:10:30 +0000 Subject: [PATCH 0631/1143] wallet.get_full_history: more consistent sort order before: ``` >>> [print(wallet.get_tx_status(txid, wallet.adb.get_tx_height(txid))) for txid in list(wallet.get_full_history())[-10:]] (7, '2023-04-04 16:13') (7, '2023-04-04 16:13') (7, '2023-04-04 16:13') (7, '2023-04-04 16:13') (0, 'Unconfirmed [20. sat/b, 0.00 MB]') (2, 'in 2 blocks') (3, 'Local [180.4 sat/b]') (3, 'Local [180.2 sat/b]') (2, 'in 2016 blocks') (0, 'Unconfirmed [180. sat/b, 0.00 MB]') ``` after: ``` >>> [print(wallet.get_tx_status(txid, wallet.adb.get_tx_height(txid))) for txid in list(wallet.get_full_history())[-10:]] (7, '2023-04-04 16:13') (7, '2023-04-04 16:13') (7, '2023-04-04 16:13') (7, '2023-04-04 16:13') (0, 'Unconfirmed [20. sat/b, 0.00 MB]') (0, 'Unconfirmed [180. sat/b, 0.00 MB]') (2, 'in 2016 blocks') (2, 'in 2 blocks') (3, 'Local [180.4 sat/b]') (3, 'Local [180.2 sat/b]') ``` --- electrum/address_synchronizer.py | 48 ++++++++++++---------- electrum/gui/qml/qetransactionlistmodel.py | 1 + electrum/lnworker.py | 4 +- electrum/wallet.py | 12 ++++-- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index cc7e31b11..e110f4c3e 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -50,6 +50,9 @@ TX_HEIGHT_UNCONF_PARENT = -1 TX_HEIGHT_UNCONFIRMED = 0 +TX_TIMESTAMP_INF = 999_999_999_999 +TX_HEIGHT_INF = 10 ** 9 + class HistoryItem(NamedTuple): txid: str @@ -476,28 +479,29 @@ def clear_history(self): self._history_local.clear() self._get_balance_cache.clear() # invalidate cache - def _get_txpos(self, tx_hash: str) -> Tuple[int, int]: - """Returns (height, txpos) tuple, even if the tx is unverified. - If txpos is -1, height should only be used for sorting purposes. - """ + def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]: + """Returns a key to be used for sorting txs.""" with self.lock: - verified_tx_mined_info = self.db.get_verified_tx(tx_hash) - if verified_tx_mined_info: - height = verified_tx_mined_info.height - txpos = verified_tx_mined_info.txpos - assert height > 0, height - assert txpos is not None - return height, txpos - elif tx_hash in self.unverified_tx: - height = self.unverified_tx[tx_hash] - assert height > 0, height - return height, -1 - elif tx_hash in self.unconfirmed_tx: - height = self.unconfirmed_tx[tx_hash] - assert height <= 0, height - return (10**9 - height), -1 - else: - return (10**9 + 1), -1 + tx_mined_info = self.get_tx_height(tx_hash) + height = self.tx_height_to_sort_height(tx_mined_info.height) + txpos = tx_mined_info.txpos or -1 + return height, txpos + + @classmethod + def tx_height_to_sort_height(cls, height: int = None): + """Return a height-like value to be used for sorting txs.""" + if height is not None: + if height > 0: + return height + if height == TX_HEIGHT_UNCONFIRMED: + return TX_HEIGHT_INF + if height == TX_HEIGHT_UNCONF_PARENT: + return TX_HEIGHT_INF + 1 + if height == TX_HEIGHT_FUTURE: + return TX_HEIGHT_INF + 2 + if height == TX_HEIGHT_LOCAL: + return TX_HEIGHT_INF + 3 + return TX_HEIGHT_INF + 100 def with_local_height_cached(func): # get local height only once, as it's relatively expensive. @@ -530,7 +534,7 @@ def get_history(self, domain) -> Sequence[HistoryItem]: tx_mined_status = self.get_tx_height(tx_hash) fee = self.get_tx_fee(tx_hash) history.append((tx_hash, tx_mined_status, delta, fee)) - history.sort(key = lambda x: self._get_txpos(x[0])) + history.sort(key = lambda x: self._get_tx_sort_key(x[0])) # 3. add balance h2 = [] balance = 0 diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 9b88c6e61..957548f18 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -219,6 +219,7 @@ def _update_future_txitem(self, tx_item_idx: int): txinfo = self.wallet.get_tx_info(tx) status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status) tx_item['date'] = status_str + # note: if the height changes, that might affect the history order, but we won't re-sort now. tx_item['height'] = self.wallet.adb.get_tx_height(txid).height index = self.index(tx_item_idx, 0) roles = [self._ROLE_RMAP[x] for x in ['height', 'date']] diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 57652bdc5..37aa6c98a 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -73,7 +73,7 @@ from .i18n import _ from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use, NoChannelPolicy, LNPathInconsistent) -from .address_synchronizer import TX_HEIGHT_LOCAL +from .address_synchronizer import TX_HEIGHT_LOCAL, TX_TIMESTAMP_INF from . import lnsweep from .lnwatcher import LNWalletWatcher from .crypto import pw_encode_with_version_and_mac, pw_decode_with_version_and_mac @@ -900,6 +900,7 @@ def get_onchain_history(self): 'amount_msat': chan.balance(LOCAL, ctn=0), 'direction': PaymentDirection.RECEIVED, 'timestamp': tx_height.timestamp, + 'monotonic_timestamp': tx_height.timestamp or TX_TIMESTAMP_INF, 'date': timestamp_to_datetime(tx_height.timestamp), 'fee_sat': None, 'fee_msat': None, @@ -922,6 +923,7 @@ def get_onchain_history(self): 'amount_msat': -chan.balance_minus_outgoing_htlcs(LOCAL), 'direction': PaymentDirection.SENT, 'timestamp': tx_height.timestamp, + 'monotonic_timestamp': tx_height.timestamp or TX_TIMESTAMP_INF, 'date': timestamp_to_datetime(tx_height.timestamp), 'fee_sat': None, 'fee_msat': None, diff --git a/electrum/wallet.py b/electrum/wallet.py index d53e2dddc..7c3fc5d75 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -73,7 +73,7 @@ PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, - TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) + TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF) from .invoices import BaseInvoice, Invoice, Request from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT from .contacts import Contacts @@ -1011,7 +1011,7 @@ def get_onchain_history(self, *, domain=None): domain = self.get_addresses() monotonic_timestamp = 0 for hist_item in self.adb.get_history(domain=domain): - monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or 999_999_999_999)) + monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)) d = { 'txid': hist_item.txid, 'fee_sat': hist_item.fee, @@ -1241,9 +1241,13 @@ def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=Tr transactions_tmp[key] = tx_item # sort on-chain and LN stuff into new dict, by timestamp # (we rely on this being a *stable* sort) + def sort_key(x): + txid, tx_item = x + ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf') + height = self.adb.tx_height_to_sort_height(tx_item.get('height')) + return ts, height transactions = OrderedDictWithIndex() - for k, v in sorted(list(transactions_tmp.items()), - key=lambda x: x[1].get('monotonic_timestamp') or x[1].get('timestamp') or float('inf')): + for k, v in sorted(list(transactions_tmp.items()), key=sort_key): transactions[k] = v now = time.time() balance = 0 From 3e4737d6e9a2b0f1e12683b44176101244dfd1cd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 5 Apr 2023 11:38:38 +0200 Subject: [PATCH 0632/1143] qml: haptic override global setting --- electrum/gui/qml/qeapp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 31f62b4d1..6de90d133 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -293,7 +293,8 @@ def get_wallet_type(self): def haptic(self): if not self.isAndroid(): return - jview.performHapticFeedback(jHfc.CONFIRM) + # TODO: deprecated from API 33 + jview.performHapticFeedback(jHfc.VIRTUAL_KEY, jHfc.FLAG_IGNORE_GLOBAL_SETTING) class ElectrumQmlApplication(QGuiApplication): From 80a16e137756afa099563a3d1f9f2a26c1f8a647 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 5 Apr 2023 11:50:54 +0200 Subject: [PATCH 0633/1143] fix typo (the error was silent) --- electrum/gui/qml/components/Channels.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 426c639d1..13f3cef92 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -139,7 +139,7 @@ Pane { FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 - enabled: Daemon.currentWallet.confirmedBalance.satInt > 0 + enabled: Daemon.currentWallet.confirmedBalance.satsInt > 0 text: qsTr('Open Channel') onClicked: { var dialog = openChannelDialog.createObject(root) From 8f3f282b62a68ef5d7dec1d5cf7bf3d397eee6cf Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 5 Apr 2023 12:08:28 +0200 Subject: [PATCH 0634/1143] Revert "qml: haptic override global setting" This reverts commit 3e4737d6e9a2b0f1e12683b44176101244dfd1cd. --- electrum/gui/qml/qeapp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 6de90d133..31f62b4d1 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -293,8 +293,7 @@ def get_wallet_type(self): def haptic(self): if not self.isAndroid(): return - # TODO: deprecated from API 33 - jview.performHapticFeedback(jHfc.VIRTUAL_KEY, jHfc.FLAG_IGNORE_GLOBAL_SETTING) + jview.performHapticFeedback(jHfc.CONFIRM) class ElectrumQmlApplication(QGuiApplication): From 0c83f363eb543beb1729d0095360fe84dbf32ecd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 5 Apr 2023 12:28:56 +0200 Subject: [PATCH 0635/1143] qml: haptic use constant compatible with older android --- electrum/gui/qml/qeapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 31f62b4d1..05e2a2c4f 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -293,7 +293,7 @@ def get_wallet_type(self): def haptic(self): if not self.isAndroid(): return - jview.performHapticFeedback(jHfc.CONFIRM) + jview.performHapticFeedback(jHfc.VIRTUAL_KEY) class ElectrumQmlApplication(QGuiApplication): From d4c386a62ca836afcedb390517323ae90e8dffd7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 5 Apr 2023 12:26:32 +0200 Subject: [PATCH 0636/1143] qml: use daemon threads everywhere the network is involved The app hangs indefinitely if we try to quit it while one of these threads is active, because once asyncio has shut down, futures never return. This was already fixed for lightning payments in c5dc133, but there are many other cases. --- electrum/gui/qml/qeapp.py | 2 +- electrum/gui/qml/qechanneldetails.py | 2 +- electrum/gui/qml/qechannelopener.py | 2 +- electrum/gui/qml/qeinvoice.py | 4 ++-- electrum/gui/qml/qeswaphelper.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 05e2a2c4f..b04ddfda2 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -265,7 +265,7 @@ def report_task(): self.sendingBugreportSuccess.emit(text) self.sendingBugreport.emit() - threading.Thread(target=report_task).start() + threading.Thread(target=report_task, daemon=True).start() @pyqtSlot() def showNever(self): diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 12afb7794..e219e3394 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -194,7 +194,7 @@ def do_close(): self._logger.exception("Could not close channel: " + repr(e)) self.channelCloseFailed.emit(_('Could not close channel: ') + repr(e)) - threading.Thread(target=do_close).start() + threading.Thread(target=do_close, daemon=True).start() @pyqtSlot() def deleteChannel(self): diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 9b2947e7a..a4bae2eb0 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -216,7 +216,7 @@ def open_thread(): self._logger.debug('starting open thread') self.channelOpening.emit(conn_str) - threading.Thread(target=open_thread).start() + threading.Thread(target=open_thread, daemon=True).start() # TODO: it would be nice to show this before broadcasting #if chan.has_onchain_backup(): diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index a17ed9d05..40ed6e13a 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -587,7 +587,7 @@ def resolve_task(): except Exception as e: self.validationError.emit('lnurl', repr(e)) - threading.Thread(target=resolve_task).start() + threading.Thread(target=resolve_task, daemon=True).start() def on_lnurl(self, lnurldata): self._logger.debug('on_lnurl') @@ -635,7 +635,7 @@ def fetch_invoice_task(): self._logger.error(repr(e)) self.lnurlError.emit('lnurl', str(e)) - threading.Thread(target=fetch_invoice_task).start() + threading.Thread(target=fetch_invoice_task, daemon=True).start() def on_lnurl_invoice(self, orig_amount, invoice): self._logger.debug('on_lnurl_invoice') diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 09107859f..eb6745add 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -350,7 +350,7 @@ def swap_task(): self._logger.error(str(e)) self.swapFailed.emit(str(e)) - threading.Thread(target=swap_task).start() + threading.Thread(target=swap_task, daemon=True).start() def do_reverse_swap(self, lightning_amount, onchain_amount): if lightning_amount is None or onchain_amount is None: @@ -375,7 +375,7 @@ def swap_task(): self._logger.error(str(e)) self.swapFailed.emit(str(e)) - threading.Thread(target=swap_task).start() + threading.Thread(target=swap_task, daemon=True).start(d) @pyqtSlot() @pyqtSlot(bool) From ca386181a038e1677993d8c3f7901dec9a318c0c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 5 Apr 2023 12:35:49 +0200 Subject: [PATCH 0637/1143] fix typo (follow-up prev) --- electrum/gui/qml/qeswaphelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index eb6745add..10702f1f5 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -375,7 +375,7 @@ def swap_task(): self._logger.error(str(e)) self.swapFailed.emit(str(e)) - threading.Thread(target=swap_task, daemon=True).start(d) + threading.Thread(target=swap_task, daemon=True).start() @pyqtSlot() @pyqtSlot(bool) From 7ac3afdcda092949bdd3e581ff95dce06cc72bb3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 5 Apr 2023 13:13:50 +0200 Subject: [PATCH 0638/1143] qml: fixes --- electrum/gui/qml/components/NotificationPopup.qml | 2 +- electrum/gui/qml/components/SwapDialog.qml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/NotificationPopup.qml b/electrum/gui/qml/components/NotificationPopup.qml index 3700d77be..543268042 100644 --- a/electrum/gui/qml/components/NotificationPopup.qml +++ b/electrum/gui/qml/components/NotificationPopup.qml @@ -54,7 +54,7 @@ Item { RowLayout { Layout.margins: constants.paddingLarge - spacing: constants.paddingSizeSmall + spacing: constants.paddingSmall Image { source: '../../icons/info.png' diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 0eeb8bb3d..77b8694fc 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -12,8 +12,8 @@ ElDialog { required property QtObject swaphelper - width: parent.width - height: parent.height + implicitHeight: parent.height + implicitWidth: parent.width title: qsTr('Lightning Swap') iconSource: Qt.resolvedUrl('../../icons/update.png') @@ -170,7 +170,7 @@ ElDialog { width: swapslider.availableWidth height: implicitHeight radius: 2 - color: Color.transparent(Material.accentColor, 0.33) + color: Material.accentColor // full width somehow misaligns with handle, define rangeWidth property int rangeWidth: width - swapslider.leftPadding From dd1a83e1c059a3008887a934280bd64a5741e311 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 5 Apr 2023 13:18:50 +0200 Subject: [PATCH 0639/1143] qml: fixes --- electrum/gui/qml/components/WalletDetails.qml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index aa750fa35..1c3e977ac 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -521,8 +521,4 @@ Pane { } } - Component.onCompleted: { - piechart.updateSlices() - } - } From 8ea63f9bde50214acccb4e3adfc38e29150ee6a0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Apr 2023 11:21:31 +0000 Subject: [PATCH 0640/1143] qml network overview: show server height, if lagging to see how many blocks it is behind --- electrum/gui/qml/components/NetworkOverview.qml | 9 +++++++++ electrum/gui/qml/qenetwork.py | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index bf84be72b..416bb5efc 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -66,6 +66,15 @@ Pane { Label { text: Network.height } + Label { + text: qsTr('Server Height:'); + color: Material.accentColor + visible: Network.server_height != Network.height + } + Label { + text: Network.server_height + " (lagging)" + visible: Network.server_height != Network.height + } Heading { Layout.columnSpan: 2 text: qsTr('Mempool fees') diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 6abe6d683..71a416bce 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -20,7 +20,8 @@ class QENetwork(QObject, QtEventListener): networkUpdated = pyqtSignal() blockchainUpdated = pyqtSignal() - heightChanged = pyqtSignal([int], arguments=['height']) + heightChanged = pyqtSignal([int], arguments=['height']) # local blockchain height + serverHeightChanged = pyqtSignal([int], arguments=['height']) proxySet = pyqtSignal() proxyChanged = pyqtSignal() statusChanged = pyqtSignal() @@ -51,7 +52,8 @@ def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): self.network = network self._qeconfig = qeconfig self._serverListModel = None - self._height = network.get_local_height() # init here, update event can take a while + self._height = network.get_local_height() # init here, update event can take a while + self._server_height = network.get_server_height() # init here, update event can take a while self.register_callbacks() self._qeconfig.useGossipChanged.connect(self.on_gossip_setting_changed) @@ -94,6 +96,11 @@ def _update_status(self): self._logger.debug('server_status updated: %s' % server_status) self._server_status = server_status self.statusChanged.emit() + server_height = self.network.get_server_height() + if self._server_height != server_height: + self._logger.debug(f'server_height updated: {server_height}') + self._server_height = server_height + self.serverHeightChanged.emit(server_height) chains = len(self.network.get_blockchains()) if chains != self._chaintips: self._logger.debug('chain tips # changed: %d', chains) @@ -173,9 +180,13 @@ def on_gossip_setting_changed(self): self.network.run_from_another_thread(self.network.stop_gossip()) @pyqtProperty(int, notify=heightChanged) - def height(self): + def height(self): # local blockchain height return self._height + @pyqtProperty(int, notify=serverHeightChanged) + def server_height(self): + return self._server_height + @pyqtProperty(str, notify=statusChanged) def server(self): return self._server From 88209617b45670a4c79e691d5d8234c5b6d7a815 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 5 Apr 2023 14:11:32 +0200 Subject: [PATCH 0641/1143] qml: in auth wrapper, use own logger and log func name --- electrum/gui/qml/auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py index a7e141204..c8dd3d825 100644 --- a/electrum/gui/qml/auth.py +++ b/electrum/gui/qml/auth.py @@ -10,9 +10,10 @@ def auth_protect(func=None, reject=None, method='pin'): @wraps(func) def wrapper(self, *args, **kwargs): - self._logger.debug(str(self)) + _logger = get_logger(__name__) + _logger.debug(f'{str(self)}.{func.__name__}') if hasattr(self, '__auth_fcall'): - self._logger.debug('object already has a pending authed function call') + _logger.debug('object already has a pending authed function call') raise Exception('object already has a pending authed function call') setattr(self, '__auth_fcall', (func,args,kwargs,reject)) getattr(self, 'authRequired').emit(method) From 03f0d632af9e1d98a0adf0c04b8fdbcefab338f2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 5 Apr 2023 14:57:52 +0200 Subject: [PATCH 0642/1143] wallet.sign_transaction: return tx if signed by swap manager This fixes bumping swap fee in the qml GUI, because it expects the value returned by this method to be None if the transaction could not be signed. --- electrum/wallet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 7c3fc5d75..b545ac54f 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2318,6 +2318,7 @@ def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = Fal self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: + """ returns tx if successful else None """ if self.is_watching_only(): return if not isinstance(tx, PartialTransaction): @@ -2326,7 +2327,7 @@ def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransac swap = self.get_swap_by_claim_tx(tx) if swap: self.lnworker.swap_manager.sign_tx(tx, swap) - return + return tx # add info to a temporary tx copy; including xpubs # and full derivation paths as hw keystores might want them tmp_tx = copy.deepcopy(tx) From b9c81b0fcb3ab36dd6294fd1eb16080c310fa3c7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 5 Apr 2023 15:08:01 +0200 Subject: [PATCH 0643/1143] qml rbf dialogs: use 'new fee', 'new fee rate' instead of 'mining fee' --- electrum/gui/qml/components/RbfBumpFeeDialog.qml | 4 ++-- electrum/gui/qml/components/RbfCancelDialog.qml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 4761da34c..0d05b118d 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -117,7 +117,7 @@ ElDialog { } Label { - text: qsTr('Mining fee') + text: qsTr('New fee') color: Material.accentColor } @@ -127,7 +127,7 @@ ElDialog { } Label { - text: qsTr('Fee rate') + text: qsTr('New fee rate') color: Material.accentColor } diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 3f4aaccb7..9238592e4 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -76,7 +76,7 @@ ElDialog { } Label { - text: qsTr('Mining fee') + text: qsTr('New fee') color: Material.accentColor } @@ -86,7 +86,7 @@ ElDialog { } Label { - text: qsTr('Fee rate') + text: qsTr('New fee rate') color: Material.accentColor } From e748345be0f0e6c1ba1079a26f3dd2704c55caea Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Apr 2023 12:52:06 +0000 Subject: [PATCH 0644/1143] addr_sync: change return type of get_address_history to dict from list --- electrum/address_synchronizer.py | 20 ++++++++++---------- electrum/wallet.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index e110f4c3e..bb20df2de 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -119,21 +119,21 @@ def is_mine(self, address: Optional[str]) -> bool: def get_addresses(self): return sorted(self.db.get_history()) - def get_address_history(self, addr: str) -> Sequence[Tuple[str, int]]: - """Returns the history for the address, in the format that would be returned by a server. + def get_address_history(self, addr: str) -> Dict[str, int]: + """Returns the history for the address, as a txid->height dict. + In addition to what we have from the server, this includes local and future txns. - Note: The difference between db.get_addr_history and this method is that - db.get_addr_history stores the response from a server, so it only includes txns - a server sees, i.e. that does not contain local and future txns. + Also see related method db.get_addr_history, which stores the response from the server, + so that only includes txns the server sees. """ - h = [] + h = {} # we need self.transaction_lock but get_tx_height will take self.lock # so we need to take that too here, to enforce order of locks with self.lock, self.transaction_lock: related_txns = self._history_local.get(addr, set()) for tx_hash in related_txns: tx_height = self.get_tx_height(tx_hash).height - h.append((tx_hash, tx_height)) + h[tx_hash] = tx_height return h def get_address_history_len(self, addr: str) -> int: @@ -421,7 +421,7 @@ def receive_tx_callback(self, tx_hash: str, tx: Transaction, tx_height: int) -> def receive_history_callback(self, addr: str, hist, tx_fees: Dict[str, int]): with self.lock: old_hist = self.get_address_history(addr) - for tx_hash, height in old_hist: + for tx_hash, height in old_hist.items(): if (tx_hash, height) not in hist: # make tx local self.unverified_tx.pop(tx_hash, None) @@ -524,7 +524,7 @@ def get_history(self, domain) -> Sequence[HistoryItem]: # delta of a tx as the sum of its deltas on domain addresses tx_deltas = defaultdict(int) # type: Dict[str, int] for addr in domain: - h = self.get_address_history(addr) + h = self.get_address_history(addr).items() for tx_hash, height in h: tx_deltas[tx_hash] += self.get_tx_delta(tx_hash, addr) # 2. create sorted history @@ -784,7 +784,7 @@ def get_tx_fee(self, txid: str) -> Optional[int]: def get_addr_io(self, address): with self.lock, self.transaction_lock: - h = self.get_address_history(address) + h = self.get_address_history(address).items() received = {} sent = {} for tx_hash, height in h: diff --git a/electrum/wallet.py b/electrum/wallet.py index b545ac54f..94d9fe8fa 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -3079,7 +3079,7 @@ def delete_address(self, address: str) -> None: transactions_new = set() # txs that are not only referred to by address with self.lock: for addr in self.db.get_history(): - details = self.adb.get_address_history(addr) + details = self.adb.get_address_history(addr).items() if addr == address: for tx_hash, height in details: transactions_to_remove.add(tx_hash) From b81508cfc048bbb1cb5e18f1750c37f9578a6108 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Apr 2023 13:07:22 +0000 Subject: [PATCH 0645/1143] qml: fix refresh bug in history, for local->unconfirmed tx transition Previously if a local tx got broadcast, it was still displayed as local in the history until it got mined (or some other action forced a full refresh). --- electrum/address_synchronizer.py | 4 ++++ electrum/gui/qml/qewallet.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index bb20df2de..cea1ed0f3 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -439,6 +439,10 @@ def receive_history_callback(self, addr: str, hist, tx_fees: Dict[str, int]): if tx is None: continue self.add_transaction(tx, allow_unrelated=True, is_new=False) + # if we already had this tx, see if its height changed (e.g. local->unconfirmed) + old_height = old_hist.get(tx_hash, None) + if old_height is not None and old_height != tx_height: + util.trigger_callback('adb_tx_height_changed', self, tx_hash, old_height, tx_height) # Store fees for tx_hash, fee_sat in tx_fees.items(): diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 72e45b437..23b3011df 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -181,9 +181,15 @@ def on_event_new_transaction(self, wallet, tx): self._logger.info(f'new transaction {tx.txid()}') self.add_tx_notification(tx) self.addressModel.setDirty() - self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after + self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after self.balanceChanged.emit() + @qt_event_listener + def on_event_adb_tx_height_changed(self, adb, txid, old_height, new_height): + if adb == self.wallet.adb: + self._logger.info(f'tx_height_changed {txid}. {old_height} -> {new_height}') + self.historyModel.setDirty() # assuming wallet.is_up_to_date triggers after + @qt_event_listener def on_event_removed_transaction(self, wallet, tx): if wallet == self.wallet: From e47059c96bfc0c4268e3be315dfbbcc054a36250 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Apr 2023 14:01:45 +0000 Subject: [PATCH 0646/1143] qml: addresses list should not depend on wallet.use_change wallet.use_change is a weird preference using which a user can disable sending new change to change addresses. However the setting can be toggled at ~any time; and the user might have pre-existing balance on change addresses, which we should not hide. --- electrum/gui/qml/qeaddresslistmodel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py index 2dd1c357b..ef12a8f94 100644 --- a/electrum/gui/qml/qeaddresslistmodel.py +++ b/electrum/gui/qml/qeaddresslistmodel.py @@ -73,7 +73,7 @@ def init_model(self): r_addresses = self.wallet.get_receiving_addresses() c_addresses = self.wallet.get_change_addresses() - n_addresses = len(r_addresses) + (len(c_addresses) if self.wallet.use_change else 0) + n_addresses = len(r_addresses) + len(c_addresses) def insert_row(atype, alist, address, iaddr): item = self.addr_to_model(address) @@ -88,7 +88,7 @@ def insert_row(atype, alist, address, iaddr): insert_row('receive', self.receive_addresses, address, i) i = i + 1 i = 0 - for address in c_addresses if self.wallet.use_change else []: + for address in c_addresses: insert_row('change', self.change_addresses, address, i) i = i + 1 self.endInsertRows() From fcff4b7274d0de7e7911cc45cc3507f071a3833f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Apr 2023 14:13:40 +0000 Subject: [PATCH 0647/1143] qml: begone, you C-style for loops how am I going to continue in the middle of the loop body if the i+=1 is at the end? :P --- electrum/gui/qml/qeaddresslistmodel.py | 18 ++++-------------- electrum/gui/qml/qechannellistmodel.py | 8 ++------ electrum/gui/qml/qedaemon.py | 1 + electrum/gui/qml/qeinvoicelistmodel.py | 12 +++--------- electrum/gui/qml/qetransactionlistmodel.py | 16 ++++------------ 5 files changed, 14 insertions(+), 41 deletions(-) diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py index ef12a8f94..ba17ae5a9 100644 --- a/electrum/gui/qml/qeaddresslistmodel.py +++ b/electrum/gui/qml/qeaddresslistmodel.py @@ -1,3 +1,4 @@ +import itertools from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject @@ -83,31 +84,20 @@ def insert_row(atype, alist, address, iaddr): self.clear() self.beginInsertRows(QModelIndex(), 0, n_addresses - 1) - i = 0 - for address in r_addresses: + for i, address in enumerate(r_addresses): insert_row('receive', self.receive_addresses, address, i) - i = i + 1 - i = 0 - for address in c_addresses: + for i, address in enumerate(c_addresses): insert_row('change', self.change_addresses, address, i) - i = i + 1 self.endInsertRows() self._dirty = False @pyqtSlot(str) def update_address(self, address): - i = 0 - for a in self.receive_addresses: + for i, a in enumerate(itertools.chain(self.receive_addresses, self.change_addresses)): if a['address'] == address: self.do_update(i,a) return - i = i + 1 - for a in self.change_addresses: - if a['address'] == address: - self.do_update(i,a) - return - i = i + 1 def do_update(self, modelindex, modelitem): mi = self.createIndex(modelindex, 0) diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 9a94c8c92..52582bf81 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -134,12 +134,10 @@ def chan_sort_score(c): self.countChanged.emit() def on_channel_updated(self, channel): - i = 0 - for c in self.channels: + for i, c in enumerate(self.channels): if c['cid'] == channel.channel_id.hex(): self.do_update(i,channel) break - i = i + 1 def do_update(self, modelindex, channel): self._logger.debug(f'updating our channel {channel.short_id_for_GUI()}') @@ -167,8 +165,7 @@ def new_channel(self, cid): @pyqtSlot(str) def remove_channel(self, cid): self._logger.debug('remove channel with cid %s' % cid) - i = 0 - for channel in self.channels: + for i, channel in enumerate(self.channels): if cid == channel['cid']: self._logger.debug(cid) self.beginRemoveRows(QModelIndex(), i, i) @@ -176,7 +173,6 @@ def remove_channel(self, cid): self.endRemoveRows() self.countChanged.emit() return - i = i + 1 def filterModel(self, role, match): _filterModel = QEFilterProxyModel(self, self) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index de959e931..db36a6ec6 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -302,6 +302,7 @@ def singlePassword(self): @pyqtSlot(result=str) def suggestWalletName(self): + # FIXME why not use util.get_new_wallet_name ? i = 1 while self.availableWallets.wallet_name_exists(f'wallet_{i}'): i = i + 1 diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 943273b94..605481adf 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -92,14 +92,12 @@ def addInvoice(self, key): self.add_invoice(self.get_invoice_for_key(key)) def delete_invoice(self, key: str): - i = 0 - for invoice in self.invoices: + for i, invoice in enumerate(self.invoices): if invoice['key'] == key: self.beginRemoveRows(QModelIndex(), i, i) self.invoices.pop(i) self.endRemoveRows() break - i = i + 1 self.set_status_timer() def get_model_invoice(self, key: str): @@ -111,8 +109,7 @@ def get_model_invoice(self, key: str): @pyqtSlot(str, int) def updateInvoice(self, key, status): self._logger.debug('updating invoice for %s to %d' % (key,status)) - i = 0 - for item in self.invoices: + for i, item in enumerate(self.invoices): if item['key'] == key: invoice = self.get_invoice_for_key(key) item['status'] = status @@ -120,7 +117,6 @@ def updateInvoice(self, key, status): index = self.index(i,0) self.dataChanged.emit(index, index, [self._ROLE_RMAP['status'], self._ROLE_RMAP['status_str']]) return - i = i + 1 def invoice_to_model(self, invoice: BaseInvoice): item = self.get_invoice_as_dict(invoice) @@ -149,14 +145,12 @@ def set_status_timer(self): @pyqtSlot() def updateStatusStrings(self): - i = 0 - for item in self.invoices: + for i, item in enumerate(self.invoices): invoice = self.get_invoice_for_key(item['key']) item['status'] = self.wallet.get_invoice_status(invoice) item['status_str'] = invoice.get_status_str(item['status']) index = self.index(i,0) self.dataChanged.emit(index, index, [self._ROLE_RMAP['status'], self._ROLE_RMAP['status_str']]) - i = i + 1 self.set_status_timer() diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 957548f18..9bfee225f 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -55,12 +55,10 @@ def on_event_adb_set_future_tx(self, adb, txid): if adb != self.wallet.adb: return self._logger.debug(f'adb_set_future_tx event for txid {txid}') - i = 0 - for item in self.tx_history: + for i, item in enumerate(self.tx_history): if 'txid' in item and item['txid'] == txid: self._update_future_txitem(i) return - i = i + 1 def rowCount(self, index): return len(self.tx_history) @@ -193,8 +191,7 @@ def init_model(self, force: bool = False): self._dirty = False def on_tx_verified(self, txid, info): - i = 0 - for tx in self.tx_history: + for i, tx in enumerate(self.tx_history): if 'txid' in tx and tx['txid'] == txid: tx['height'] = info.height tx['confirmations'] = info.conf @@ -205,7 +202,6 @@ def on_tx_verified(self, txid, info): roles = [self._ROLE_RMAP[x] for x in ['section','height','confirmations','timestamp','date']] self.dataChanged.emit(index, index, roles) return - i = i + 1 def _update_future_txitem(self, tx_item_idx: int): tx_item = self.tx_history[tx_item_idx] @@ -227,20 +223,17 @@ def _update_future_txitem(self, tx_item_idx: int): @pyqtSlot(str, str) def update_tx_label(self, key, label): - i = 0 - for tx in self.tx_history: + for i, tx in enumerate(self.tx_history): if tx['key'] == key: tx['label'] = label index = self.index(i,0) self.dataChanged.emit(index, index, [self._ROLE_RMAP['label']]) return - i = i + 1 @pyqtSlot(int) def updateBlockchainHeight(self, height): self._logger.debug('updating height to %d' % height) - i = 0 - for tx_item in self.tx_history: + for i, tx_item in enumerate(self.tx_history): if 'height' in tx_item: if tx_item['height'] > 0: tx_item['confirmations'] = height - tx_item['height'] + 1 @@ -249,4 +242,3 @@ def updateBlockchainHeight(self, height): self.dataChanged.emit(index, index, roles) elif tx_item['height'] in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL): self._update_future_txitem(i) - i = i + 1 From 2de9ca24a213a60d50f4c0b8be2337d1f02fd3d9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 5 Apr 2023 14:32:11 +0000 Subject: [PATCH 0648/1143] qml history: update mempool depth for unconf txs on new histogram --- electrum/gui/qml/qenetwork.py | 2 +- electrum/gui/qml/qetransactionlistmodel.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 71a416bce..11ed92cb1 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -118,7 +118,7 @@ def on_event_status(self, *args): @event_listener def on_event_fee_histogram(self, histogram): - self._logger.debug(f'fee histogram updated: {repr(histogram)}') + self._logger.debug(f'fee histogram updated') self.update_histogram(histogram) def update_histogram(self, histogram): diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 9bfee225f..e9ab92526 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -242,3 +242,21 @@ def updateBlockchainHeight(self, height): self.dataChanged.emit(index, index, roles) elif tx_item['height'] in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL): self._update_future_txitem(i) + + @qt_event_listener + def on_event_fee_histogram(self, histogram): + self._logger.debug(f'fee histogram updated') + for i, tx_item in enumerate(self.tx_history): + if 'height' not in tx_item: # filter to on-chain + continue + if tx_item['confirmations'] > 0: # filter out already mined + continue + txid = tx_item['txid'] + tx = self.wallet.db.get_transaction(txid) + assert tx is not None + txinfo = self.wallet.get_tx_info(tx) + status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status) + tx_item['date'] = status_str + index = self.index(i, 0) + roles = [self._ROLE_RMAP['date']] + self.dataChanged.emit(index, index, roles) From bcbcf18c4d505eeecfb3652dbb123bc7fa3e2e86 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 09:15:46 +0200 Subject: [PATCH 0649/1143] qml: arrows consistency. replace with unicode arrows once we can assure these glyphs are included on device --- electrum/gui/qml/components/NetworkOverview.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 416bb5efc..f1339dc1f 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -144,14 +144,14 @@ Pane { RowLayout { Layout.fillWidth: true Label { - text: '< ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) + text: '<-- ' + qsTr('%1 sat/vB').arg(Math.ceil(Network.feeHistogram.max_fee)) font.pixelSize: constants.fontSizeXSmall color: Material.accentColor } Label { Layout.fillWidth: true horizontalAlignment: Text.AlignRight - text: qsTr('%1 sat/vB').arg(Math.floor(Network.feeHistogram.min_fee)) + ' >' + text: qsTr('%1 sat/vB').arg(Math.floor(Network.feeHistogram.min_fee)) + ' -->' font.pixelSize: constants.fontSizeXSmall color: Material.accentColor } From e02ca6b2d896181f7b39535c80041b95226d334d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 09:26:52 +0200 Subject: [PATCH 0650/1143] qml: add logging of instance around SwapDialog create/destroy and swap trigger --- electrum/gui/qml/components/Channels.qml | 5 ++++- electrum/gui/qml/components/SwapDialog.qml | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 13f3cef92..f1f1370dc 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -155,7 +155,10 @@ Pane { Component { id: swapDialog SwapDialog { - onClosed: destroy() + onClosed: { + console.log('Destroying SwapDialog ' + this) + destroy() + } } } diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 77b8694fc..ba24db9cc 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -243,7 +243,10 @@ ElDialog { text: qsTr('Ok') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') enabled: swaphelper.valid - onClicked: swaphelper.executeSwap() + onClicked: { + console.log('Swap triggered from dialog ' + this + ' using swaphelper ' + swaphelper) + swaphelper.executeSwap() + } } } @@ -258,6 +261,7 @@ ElDialog { } Component.onCompleted: { + console.log('Created SwapDialog ' + this) swapslider.value = swaphelper.sliderPos } From 5d350184a4762f343b33b328906090d6b9788053 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 10:11:31 +0200 Subject: [PATCH 0651/1143] qml: add busy property and guards around swaphelper --- electrum/gui/qml/components/SwapDialog.qml | 2 +- electrum/gui/qml/qeswaphelper.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index ba24db9cc..19c94d3ef 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -242,7 +242,7 @@ ElDialog { Layout.fillWidth: true text: qsTr('Ok') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') - enabled: swaphelper.valid + enabled: swaphelper.valid && !swaphelper.busy onClicked: { console.log('Swap triggered from dialog ' + this + ' using swaphelper ' + swaphelper) swaphelper.executeSwap() diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 10702f1f5..393067cad 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -33,6 +33,7 @@ def __init__(self, parent=None): self._rangeMax = 0 self._tx = None self._valid = False + self._busy = False self._userinfo = ' '.join([ _('Move the slider to set the amount and direction of the swap.'), _('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'), @@ -201,6 +202,17 @@ def isReverse(self, isReverse): self._isReverse = isReverse self.isReverseChanged.emit() + busyChanged = pyqtSignal() + @pyqtProperty(bool, notify=busyChanged) + def busy(self): + return self._busy + + @busy.setter + def busy(self, busy): + if self._busy != busy: + self._busy = busy + self.busyChanged.emit() + def init_swap_slider_range(self): lnworker = self._wallet.wallet.lnworker @@ -349,6 +361,8 @@ def swap_task(): except Exception as e: self._logger.error(str(e)) self.swapFailed.emit(str(e)) + finally: + self.busy = False threading.Thread(target=swap_task, daemon=True).start() @@ -374,6 +388,8 @@ def swap_task(): except Exception as e: self._logger.error(str(e)) self.swapFailed.emit(str(e)) + finally: + self.busy = False threading.Thread(target=swap_task, daemon=True).start() @@ -383,6 +399,11 @@ def executeSwap(self, confirm=False): if not self._wallet.wallet.network: self.error.emit(_("You are offline.")) return + + if self._busy: + self._logger.error('swap already in progress for this swaphelper') + return + if confirm: self._do_execute_swap() return @@ -396,6 +417,7 @@ def executeSwap(self, confirm=False): @auth_protect def _do_execute_swap(self): + self.busy = True if self.isReverse: lightning_amount = self._send_amount onchain_amount = self._receive_amount From 42cb3a1377948ea817ae1c8c902b1137a1a55cc6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 10:36:09 +0200 Subject: [PATCH 0652/1143] qml: use same main-server icon as in desktop client --- .../gui/qml/components/controls/ServerDelegate.qml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/controls/ServerDelegate.qml b/electrum/gui/qml/components/controls/ServerDelegate.qml index 40b1650ac..21eb5639e 100644 --- a/electrum/gui/qml/components/controls/ServerDelegate.qml +++ b/electrum/gui/qml/components/controls/ServerDelegate.qml @@ -29,11 +29,12 @@ ItemDelegate { } Item { - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - Label { - text: '❤' - anchors.centerIn: parent + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium + Image { + source: '../../../icons/chevron-right.png' + width: constants.iconSizeMedium + height: constants.iconSizeMedium visible: model.is_primary } } From b40794014df5d8fee5074623512081e4d2b7b4ec Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 11:42:06 +0200 Subject: [PATCH 0653/1143] android: exclude more unneeded files in qml resource bundle generation --- contrib/android/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 013bdf6df..a3d6040bd 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -179,8 +179,8 @@ RUN cd /opt \ && git remote add sombernight https://github.com/SomberNight/python-for-android \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ - # commit: from branch sombernight/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "ec82acf894822373ae88247658a233c77e76f879^{commit}" \ + # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) + && git checkout "8d73dc4f2b74b187c4f1ff59b55873ba1e357b05^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars From 24cc80b724d4ada9795e693d841346c4adec694d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Apr 2023 11:45:23 +0000 Subject: [PATCH 0654/1143] kivy: fix ln chan open follow-up e1dc7d1e6fb2fc5b88195b62cbe1613b252db388 --- electrum/gui/kivy/uix/dialogs/lightning_open_channel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py index 76a956fbd..3123b1ff6 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py @@ -199,7 +199,7 @@ def _open_channel(self, x, conn_str, amount): lnworker = self.app.wallet.lnworker coins = self.app.wallet.get_spendable_coins(None, nonlocal_only=True) node_id, rest = extract_nodeid(conn_str) - make_tx = lambda rbf: lnworker.mktx_for_open_channel( + make_tx = lambda: lnworker.mktx_for_open_channel( coins=coins, funding_sat=amount, node_id=node_id, @@ -210,7 +210,7 @@ def _open_channel(self, x, conn_str, amount): amount = amount, make_tx=make_tx, on_pay=on_pay, - show_final=False) + ) d.open() def do_open_channel(self, funding_tx, conn_str, password): From a45d2ce8315cbe44a8638c8c42f18ca011c9a948 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 15:21:16 +0200 Subject: [PATCH 0655/1143] qml: highlight selected invoice, styling Invoices buttons --- electrum/gui/qml/components/Invoices.qml | 117 +++++++++--------- .../components/controls/InvoiceDelegate.qml | 9 +- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 81a6eeee3..8e243d154 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -9,74 +9,81 @@ import "controls" Pane { id: root - property string selected_key + + padding: 0 ColumnLayout { anchors.fill: parent + spacing: 0 - InfoTextArea { + ColumnLayout { Layout.fillWidth: true - Layout.bottomMargin: constants.paddingLarge - visible: !Config.userKnowsPressAndHold - text: qsTr('To access this list from the main screen, press and hold the Send button') - } + Layout.margins: constants.paddingLarge - Heading { - text: qsTr('Saved Invoices') - } + InfoTextArea { + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: !Config.userKnowsPressAndHold + text: qsTr('To access this list from the main screen, press and hold the Send button') + } - Frame { - background: PaneInsetBackground {} + Heading { + text: qsTr('Saved Invoices') + } - verticalPadding: 0 - horizontalPadding: 0 - Layout.fillHeight: true - Layout.fillWidth: true + Frame { + background: PaneInsetBackground {} - ListView { - id: listview - anchors.fill: parent - clip: true + verticalPadding: 0 + horizontalPadding: 0 + Layout.fillHeight: true + Layout.fillWidth: true - model: DelegateModel { - id: delegateModel - model: Daemon.currentWallet.invoiceModel - delegate: InvoiceDelegate { - onClicked: { - var dialog = app.stack.getRoot().openInvoice(model.key) - dialog.invoiceAmountChanged.connect(function () { - Daemon.currentWallet.invoiceModel.init_model() - }) - selected_key = '' + ListView { + id: listview + anchors.fill: parent + clip: true + currentIndex: -1 + model: DelegateModel { + id: delegateModel + model: Daemon.currentWallet.invoiceModel + delegate: InvoiceDelegate { + onClicked: { + var dialog = app.stack.getRoot().openInvoice(model.key) + dialog.invoiceAmountChanged.connect(function () { + Daemon.currentWallet.invoiceModel.init_model() + }) + listview.currentIndex = -1 + } + onPressAndHold: listview.currentIndex = index } - onPressAndHold: { - selected_key = model.key - } } - } - add: Transition { - NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 } - NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 } - } - addDisplaced: Transition { - SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 } - } + add: Transition { + NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 } + NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 } + } + addDisplaced: Transition { + SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 } + } - remove: Transition { - NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } - NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } - } - removeDisplaced: Transition { - SequentialAnimation { - PauseAnimation { duration: 200 } - SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } + remove: Transition { + NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } + NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: 200 } + SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } + } } - } - ScrollIndicator.vertical: ScrollIndicator { } + ScrollIndicator.vertical: ScrollIndicator { } + } } + } + ButtonContainer { Layout.fillWidth: true FlatButton { @@ -84,10 +91,9 @@ Pane { Layout.preferredWidth: 1 text: qsTr('Delete') icon.source: '../../icons/delete.png' - visible: selected_key != '' + visible: listview.currentIndex >= 0 onClicked: { - Daemon.currentWallet.delete_invoice(selected_key) - selected_key = '' + Daemon.currentWallet.delete_invoice(listview.currentItem.getKey()) } } FlatButton { @@ -95,13 +101,12 @@ Pane { Layout.preferredWidth: 1 text: qsTr('View') icon.source: '../../icons/tab_receive.png' - visible: selected_key != '' + visible: listview.currentIndex >= 0 onClicked: { - var dialog = app.stack.getRoot().openInvoice(selected_key) + var dialog = app.stack.getRoot().openInvoice(listview.currentItem.getKey()) dialog.invoiceAmountChanged.connect(function () { Daemon.currentWallet.invoiceModel.init_model() }) - selected_key = '' } } } diff --git a/electrum/gui/qml/components/controls/InvoiceDelegate.qml b/electrum/gui/qml/components/controls/InvoiceDelegate.qml index 2147b2964..18b3055d1 100644 --- a/electrum/gui/qml/components/controls/InvoiceDelegate.qml +++ b/electrum/gui/qml/components/controls/InvoiceDelegate.qml @@ -5,11 +5,17 @@ import QtQuick.Controls.Material 2.0 ItemDelegate { id: root + height: item.height width: ListView.view.width - font.pixelSize: constants.fontSizeSmall // set default font size for child controls + highlighted: ListView.isCurrentItem + + function getKey() { + return model.key + } + GridLayout { id: item @@ -130,6 +136,7 @@ ItemDelegate { Layout.preferredHeight: constants.paddingTiny color: 'transparent' } + } Connections { From f0d7983a46d1524e00c7e9e29890e137213d3f35 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 16:05:41 +0200 Subject: [PATCH 0656/1143] qml: piechart from wallet.get_balances_for_piechart --- .../gui/qml/components/BalanceDetails.qml | 23 ++++++++++++------- electrum/gui/qml/components/Constants.qml | 6 ++++- electrum/gui/qml/qewallet.py | 13 +++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index db15329f8..87a1b819f 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -56,14 +56,21 @@ Pane { implicitHeight: 220 // TODO: sane value dependent on screen innerOffset: 6 function updateSlices() { - var totalB = Daemon.currentWallet.totalBalance.satsInt - var onchainB = Daemon.currentWallet.confirmedBalance.satsInt - var frozenB = Daemon.currentWallet.frozenBalance.satsInt - var lnB = Daemon.currentWallet.lightningBalance.satsInt + var p = Daemon.currentWallet.getBalancesForPiechart() + var total = p['total'] piechart.slices = [ - { v: lnB/totalB, color: constants.colorPiechartLightning, text: 'Lightning' }, - { v: (onchainB-frozenB)/totalB, color: constants.colorPiechartOnchain, text: 'On-chain' }, - { v: frozenB/totalB, color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, + { v: p['lightning']/total, + color: constants.colorPiechartLightning, text: qsTr('Lightning') }, + { v: p['confirmed']/total, + color: constants.colorPiechartOnchain, text: 'On-chain' }, + { v: p['frozen']/total, + color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, + { v: p['unconfirmed']/total, + color: constants.colorPiechartFrozen, text: 'Unconfirmed' }, + { v: p['unmatured']/total, + color: constants.colorPiechartFrozen, text: 'Unmatured' }, + { v: p['f_lightning']/total, + color: constants.colorPiechartLightningFrozen, text: 'Frozen Lightning' }, ] } } @@ -154,7 +161,7 @@ Pane { } } } - + ButtonContainer { Layout.fillWidth: true FlatButton { diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 87c3c6fd2..0beec55cb 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -48,7 +48,11 @@ Item { property color colorPiechartOnchain: Qt.darker(Material.accentColor, 1.50) property color colorPiechartFrozen: 'gray' - property color colorPiechartLightning: 'orange' //Qt.darker(Material.accentColor, 1.20) + property color colorPiechartLightning: 'orange' + property color colorPiechartLightningFrozen: Qt.darker('orange', 1.20) + property color colorPiechartUnconfirmed: Qt.darker(Material.accentColor, 2.00) + property color colorPiechartUnmatured: 'magenta' + property color colorPiechartParticipant: 'gray' property color colorPiechartSignature: 'yellow' diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 23b3011df..0a365f665 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -745,3 +745,16 @@ def getSerializedTx(self, txid): tx = self.wallet.db.get_transaction(txid) txqr = tx.to_qr_data() return [str(tx), txqr[0], txqr[1]] + + @pyqtSlot(result='QVariantMap') + def getBalancesForPiechart(self): + confirmed, unconfirmed, unmatured, frozen, lightning, f_lightning = balances = self.wallet.get_balances_for_piechart() + return { + 'confirmed': confirmed, + 'unconfirmed': unconfirmed, + 'unmatured': unmatured, + 'frozen': frozen, + 'lightning': int(lightning), + 'f_lightning': int(f_lightning), + 'total': sum([int(x) for x in list(balances)]) + } From 2e15899fdab8f4e2de29fb203998b67d4e9a5c52 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 16:13:37 +0200 Subject: [PATCH 0657/1143] followup prev --- electrum/gui/qml/components/BalanceDetails.qml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index 87a1b819f..0a558fdcf 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -62,15 +62,15 @@ Pane { { v: p['lightning']/total, color: constants.colorPiechartLightning, text: qsTr('Lightning') }, { v: p['confirmed']/total, - color: constants.colorPiechartOnchain, text: 'On-chain' }, + color: constants.colorPiechartOnchain, text: qsTr('On-chain') }, { v: p['frozen']/total, - color: constants.colorPiechartFrozen, text: 'On-chain (frozen)' }, + color: constants.colorPiechartFrozen, text: qsTr('On-chain (frozen)') }, { v: p['unconfirmed']/total, - color: constants.colorPiechartFrozen, text: 'Unconfirmed' }, + color: constants.colorPiechartUnconfirmed, text: qsTr('Unconfirmed') }, { v: p['unmatured']/total, - color: constants.colorPiechartFrozen, text: 'Unmatured' }, + color: constants.colorPiechartUnmatured, text: qsTr('Unmatured') }, { v: p['f_lightning']/total, - color: constants.colorPiechartLightningFrozen, text: 'Frozen Lightning' }, + color: constants.colorPiechartLightningFrozen, text: qsTr('Frozen Lightning') }, ] } } From 6cd1f553e4e62d717fe51ec7c637066ffd70c67e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 16:15:57 +0200 Subject: [PATCH 0658/1143] qml: BalanceSummary remove height hint for fiat amount --- electrum/gui/qml/components/controls/BalanceSummary.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index f1437f870..94c31ea60 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -53,8 +53,6 @@ Item { Item { visible: Daemon.fx.enabled - // attempt at making fiat state as tall as btc state: - Layout.preferredHeight: fontMetrics.lineSpacing * 2 + balanceLayout.rowSpacing + 2 Layout.preferredWidth: 1 } Label { From 2dd9b0796a4cf140e34aac6c30aff6eb28e5af1a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 6 Apr 2023 16:17:38 +0200 Subject: [PATCH 0659/1143] qml: right-align balance labels --- electrum/gui/qml/components/controls/BalanceSummary.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 94c31ea60..0559b9a5d 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -71,6 +71,7 @@ Item { } RowLayout { + Layout.alignment: Qt.AlignRight visible: Daemon.currentWallet.isLightning Image { Layout.preferredWidth: constants.iconSizeSmall @@ -97,6 +98,7 @@ Item { } RowLayout { + Layout.alignment: Qt.AlignRight visible: Daemon.currentWallet.isLightning Image { Layout.preferredWidth: constants.iconSizeSmall From 72da9c1a6a44651c8a6da037e1de0dff38db6b57 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Apr 2023 13:53:40 +0000 Subject: [PATCH 0660/1143] sanitise untrusted error bytes before logging it full-blown paranoia kicking in --- electrum/base_crash_reporter.py | 7 ++++--- electrum/lnpeer.py | 27 ++++++++++++++------------- electrum/network.py | 12 +++++++----- electrum/paymentrequest.py | 18 +++++------------- electrum/tests/test_util.py | 23 +++++++++++++++++++++++ electrum/util.py | 23 +++++++++++++++++++++++ 6 files changed, 76 insertions(+), 34 deletions(-) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index b2c95be80..ed75a1ac6 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -30,7 +30,7 @@ from .version import ELECTRUM_VERSION from . import constants from .i18n import _ -from .util import make_aiohttp_session +from .util import make_aiohttp_session, error_text_str_to_safe_str from .logging import describe_os_version, Logger, get_git_version @@ -80,7 +80,8 @@ def send_report(self, asyncio_loop, proxy, *, timeout=None) -> CrashReportRespon report = json.dumps(report) coro = self.do_post(proxy, BaseCrashReporter.report_server + "/crash.json", data=report) response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout) - self.logger.info(f"Crash report sent. Got response [DO NOT TRUST THIS MESSAGE]: {response!r}") + self.logger.info( + f"Crash report sent. Got response [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(response)}") response = json.loads(response) assert isinstance(response, dict), type(response) # sanitize URL @@ -98,7 +99,7 @@ def send_report(self, asyncio_loop, proxy, *, timeout=None) -> CrashReportRespon ) return ret - async def do_post(self, proxy, url, data): + async def do_post(self, proxy, url, data) -> str: async with make_aiohttp_session(proxy) as session: async with session.post(url, data=data, raise_for_status=True) as resp: return await resp.text() diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 1110e1726..c4f2a3827 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -22,7 +22,7 @@ from .ecc import sig_string_from_r_and_s, der_sig_from_sig_string from . import constants from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup, - UnrelatedTransactionException) + UnrelatedTransactionException, error_text_bytes_to_safe_str) from . import transaction from .bitcoin import make_op_return from .transaction import PartialTxOutput, match_script_against_template, Sighash @@ -241,13 +241,14 @@ def process_message(self, message: bytes): def on_warning(self, payload): chan_id = payload.get("channel_id") + err_bytes = payload['data'] self.logger.info(f"remote peer sent warning [DO NOT TRUST THIS MESSAGE]: " - f"{payload['data'].decode('ascii')}. chan_id={chan_id.hex()}") + f"{error_text_bytes_to_safe_str(err_bytes)}. chan_id={chan_id.hex()}") if chan_id in self.channels: - self.ordered_message_queues[chan_id].put_nowait((None, {'warning': payload['data']})) + self.ordered_message_queues[chan_id].put_nowait((None, {'warning': err_bytes})) elif chan_id in self.temp_id_to_id: chan_id = self.temp_id_to_id[chan_id] or chan_id - self.ordered_message_queues[chan_id].put_nowait((None, {'warning': payload['data']})) + self.ordered_message_queues[chan_id].put_nowait((None, {'warning': err_bytes})) else: # if no existing channel is referred to by channel_id: # - MUST ignore the message. @@ -256,20 +257,21 @@ def on_warning(self, payload): def on_error(self, payload): chan_id = payload.get("channel_id") + err_bytes = payload['data'] self.logger.info(f"remote peer sent error [DO NOT TRUST THIS MESSAGE]: " - f"{payload['data'].decode('ascii')}. chan_id={chan_id.hex()}") + f"{error_text_bytes_to_safe_str(err_bytes)}. chan_id={chan_id.hex()}") if chan_id in self.channels: self.schedule_force_closing(chan_id) - self.ordered_message_queues[chan_id].put_nowait((None, {'error': payload['data']})) + self.ordered_message_queues[chan_id].put_nowait((None, {'error': err_bytes})) elif chan_id in self.temp_id_to_id: chan_id = self.temp_id_to_id[chan_id] or chan_id - self.ordered_message_queues[chan_id].put_nowait((None, {'error': payload['data']})) + self.ordered_message_queues[chan_id].put_nowait((None, {'error': err_bytes})) elif chan_id == bytes(32): # if channel_id is all zero: # - MUST fail all channels with the sending node. for cid in self.channels: self.schedule_force_closing(cid) - self.ordered_message_queues[cid].put_nowait((None, {'error': payload['data']})) + self.ordered_message_queues[cid].put_nowait((None, {'error': err_bytes})) else: # if no existing channel is referred to by channel_id: # - MUST ignore the message. @@ -337,12 +339,11 @@ async def wait_for_message(self, expected_name, channel_id): q = self.ordered_message_queues[channel_id] name, payload = await asyncio.wait_for(q.get(), LN_P2P_NETWORK_TIMEOUT) # raise exceptions for errors/warnings, so that the caller sees them - if payload.get('error'): + if (err_bytes := (payload.get("error") or payload.get("warning"))) is not None: + err_type = "error" if payload.get("error") else "warning" + err_text = error_text_bytes_to_safe_str(err_bytes) raise GracefulDisconnect( - f"remote peer sent error [DO NOT TRUST THIS MESSAGE]: {payload['error'].decode('ascii')}") - elif payload.get('warning'): - raise GracefulDisconnect( - f"remote peer sent warning [DO NOT TRUST THIS MESSAGE]: {payload['warning'].decode('ascii')}") + f"remote peer sent {err_type} [DO NOT TRUST THIS MESSAGE]: {err_text}") if name != expected_name: raise Exception(f"Received unexpected '{name}'") return payload diff --git a/electrum/network.py b/electrum/network.py index 71467d048..1a3ffebc1 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -46,7 +46,7 @@ from .util import (log_exceptions, ignore_exceptions, OldTaskGroup, bfh, make_aiohttp_session, send_exception_to_crash_reporter, is_hash256_str, is_non_negative_integer, MyEncoder, NetworkRetryManager, - nullcontext) + nullcontext, error_text_str_to_safe_str) from .bitcoin import COIN from . import constants from . import blockchain @@ -235,8 +235,9 @@ def __str__(self): return _("The server returned an error.") def __repr__(self): + e = self.original_exception return (f"") + f"[DO NOT TRUST THIS MESSAGE] original_exception: {error_text_str_to_safe_str(repr(e))}>") _INSTANCE = None @@ -924,14 +925,15 @@ async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> Non except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError): raise # pass-through except aiorpcx.jsonrpc.CodeMessageError as e: - self.logger.info(f"broadcast_transaction error [DO NOT TRUST THIS MESSAGE]: {repr(e)}") + self.logger.info(f"broadcast_transaction error [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}") raise TxBroadcastServerReturnedError(self.sanitize_tx_broadcast_response(e.message)) from e except BaseException as e: # intentional BaseException for sanity! - self.logger.info(f"broadcast_transaction error2 [DO NOT TRUST THIS MESSAGE]: {repr(e)}") + self.logger.info(f"broadcast_transaction error2 [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}") send_exception_to_crash_reporter(e) raise TxBroadcastUnknownError() from e if out != tx.txid(): - self.logger.info(f"unexpected txid for broadcast_transaction [DO NOT TRUST THIS MESSAGE]: {out} != {tx.txid()}") + self.logger.info(f"unexpected txid for broadcast_transaction [DO NOT TRUST THIS MESSAGE]: " + f"{error_text_str_to_safe_str(out)} != {tx.txid()}") raise TxBroadcastHashMismatch(_("Server returned unexpected transaction ID.")) async def try_broadcasting(self, tx, name) -> bool: diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index afc876e20..8d46909e2 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -39,7 +39,7 @@ sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'contrib/generate_payreqpb2.sh'") from . import bitcoin, constants, ecc, util, transaction, x509, rsakey -from .util import bfh, make_aiohttp_session +from .util import bfh, make_aiohttp_session, error_text_bytes_to_safe_str from .invoices import Invoice, get_id_from_onchain_outputs from .crypto import sha256 from .bitcoin import address_to_script @@ -94,12 +94,8 @@ async def get_payment_request(url: str) -> 'PaymentRequest': if isinstance(e, aiohttp.ClientResponseError): error += f"\nGot HTTP status code {e.status}." if resp_content: - try: - error_text_received = resp_content.decode("utf8") - except UnicodeDecodeError: - error_text_received = "(failed to decode error)" - else: - error_text_received = error_text_received[:400] + error_text_received = error_text_bytes_to_safe_str(resp_content) + error_text_received = error_text_received[:400] error_oneline = ' -- '.join(error.split('\n')) _logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] " f"{repr(e)} text: {error_text_received}") @@ -306,12 +302,8 @@ async def send_payment_and_receive_paymentack(self, raw_tx, refund_addr): if isinstance(e, aiohttp.ClientResponseError): error += f"\nGot HTTP status code {e.status}." if resp_content: - try: - error_text_received = resp_content.decode("utf8") - except UnicodeDecodeError: - error_text_received = "(failed to decode error)" - else: - error_text_received = error_text_received[:400] + error_text_received = error_text_bytes_to_safe_str(resp_content) + error_text_received = error_text_received[:400] error_oneline = ' -- '.join(error.split('\n')) _logger.info(f"{error_oneline} -- [DO NOT TRUST THIS MESSAGE] " f"{repr(e)} text: {error_text_received}") diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index 60659d237..9668aa2e1 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -344,3 +344,26 @@ def test_is_subpath(self): self.assertFalse(util.is_subpath("/a/b/c", "c")) self.assertFalse(util.is_subpath("a", "/a/b/c")) self.assertFalse(util.is_subpath("c", "/a/b/c")) + + def test_error_text_bytes_to_safe_str(self): + # ascii + self.assertEqual("'test'", util.error_text_bytes_to_safe_str(b"test")) + self.assertEqual('"test123 \'QWE"', util.error_text_bytes_to_safe_str(b"test123 'QWE")) + self.assertEqual("'prefix: \\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08malicious_stuff'", + util.error_text_bytes_to_safe_str(b"prefix: " + 8 * b"\x08" + b"malicious_stuff")) + # unicode + self.assertEqual("'here is some unicode: \\\\xe2\\\\x82\\\\xbf \\\\xf0\\\\x9f\\\\x98\\\\x80 \\\\xf0\\\\x9f\\\\x98\\\\x88'", + util.error_text_bytes_to_safe_str(b'here is some unicode: \xe2\x82\xbf \xf0\x9f\x98\x80 \xf0\x9f\x98\x88')) + # not even unicode + self.assertEqual("""\'\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\t\\n\\x0b\\x0c\\r\\x0e\\x0f\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f !"#$%&\\\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\x7f\\\\x80\\\\x81\\\\x82\\\\x83\\\\x84\\\\x85\\\\x86\\\\x87\\\\x88\\\\x89\\\\x8a\\\\x8b\\\\x8c\\\\x8d\\\\x8e\\\\x8f\\\\x90\\\\x91\\\\x92\\\\x93\\\\x94\\\\x95\\\\x96\\\\x97\\\\x98\\\\x99\\\\x9a\\\\x9b\\\\x9c\\\\x9d\\\\x9e\\\\x9f\\\\xa0\\\\xa1\\\\xa2\\\\xa3\\\\xa4\\\\xa5\\\\xa6\\\\xa7\\\\xa8\\\\xa9\\\\xaa\\\\xab\\\\xac\\\\xad\\\\xae\\\\xaf\\\\xb0\\\\xb1\\\\xb2\\\\xb3\\\\xb4\\\\xb5\\\\xb6\\\\xb7\\\\xb8\\\\xb9\\\\xba\\\\xbb\\\\xbc\\\\xbd\\\\xbe\\\\xbf\\\\xc0\\\\xc1\\\\xc2\\\\xc3\\\\xc4\\\\xc5\\\\xc6\\\\xc7\\\\xc8\\\\xc9\\\\xca\\\\xcb\\\\xcc\\\\xcd\\\\xce\\\\xcf\\\\xd0\\\\xd1\\\\xd2\\\\xd3\\\\xd4\\\\xd5\\\\xd6\\\\xd7\\\\xd8\\\\xd9\\\\xda\\\\xdb\\\\xdc\\\\xdd\\\\xde\\\\xdf\\\\xe0\\\\xe1\\\\xe2\\\\xe3\\\\xe4\\\\xe5\\\\xe6\\\\xe7\\\\xe8\\\\xe9\\\\xea\\\\xeb\\\\xec\\\\xed\\\\xee\\\\xef\\\\xf0\\\\xf1\\\\xf2\\\\xf3\\\\xf4\\\\xf5\\\\xf6\\\\xf7\\\\xf8\\\\xf9\\\\xfa\\\\xfb\\\\xfc\\\\xfd\\\\xfe\\\\xff\'""", + util.error_text_bytes_to_safe_str(bytes(range(256)))) + + def test_error_text_str_to_safe_str(self): + # ascii + self.assertEqual("'test'", util.error_text_str_to_safe_str("test")) + self.assertEqual('"test123 \'QWE"', util.error_text_str_to_safe_str("test123 'QWE")) + self.assertEqual("'prefix: \\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08malicious_stuff'", + util.error_text_str_to_safe_str("prefix: " + 8 * "\x08" + "malicious_stuff")) + # unicode + self.assertEqual("'here is some unicode: \\\\u20bf \\\\U0001f600 \\\\U0001f608'", + util.error_text_str_to_safe_str("here is some unicode: ₿ 😀 😈")) diff --git a/electrum/util.py b/electrum/util.py index 9ad8fbb0f..742f873a1 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -2059,3 +2059,26 @@ def get_running_loop() -> Optional[asyncio.AbstractEventLoop]: return asyncio.get_running_loop() except RuntimeError: return None + + +def error_text_str_to_safe_str(err: str) -> str: + """Converts an untrusted error string to a sane printable ascii str. + Never raises. + """ + return error_text_bytes_to_safe_str(err.encode("ascii", errors='backslashreplace')) + + +def error_text_bytes_to_safe_str(err: bytes) -> str: + """Converts an untrusted error bytes text to a sane printable ascii str. + Never raises. + + Note that naive ascii conversion would be insufficient. Fun stuff: + >>> b = b"my_long_prefix_blabla" + 21 * b"\x08" + b"malicious_stuff" + >>> s = b.decode("ascii") + >>> print(s) + malicious_stuffblabla + """ + # convert to ascii, to get rid of unicode stuff + ascii_text = err.decode("ascii", errors='backslashreplace') + # do repr to handle ascii special chars (especially when printing/logging the str) + return repr(ascii_text) From 36d800063f1ac7fbc6af5424a1300c34bbabe7e9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Apr 2023 10:16:18 +0000 Subject: [PATCH 0661/1143] lnutil: add docstring and more tests for extract_nodeid --- electrum/gui/qml/qechannelopener.py | 4 ++-- electrum/lnutil.py | 11 ++++++++++- electrum/tests/test_lnutil.py | 13 +++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index a4bae2eb0..1e108bb2d 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -129,13 +129,13 @@ def validate_nodeid(self, nodeid): try: self.nodeid_to_lnpeer(nodeid) except Exception as e: - self._logger.debug(repr(e)) + self._logger.debug(f"invalid nodeid. {e!r}") return False return True def nodeid_to_lnpeer(self, nodeid): node_pubkey, host_port = extract_nodeid(nodeid) - if host_port.__contains__(':'): + if host_port.__contains__(':'): # FIXME host_port can be None; can't construct LNPeerAddr then. host, port = host_port.split(':',1) else: host = host_port diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 4ffb77101..49b424ba1 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1450,7 +1450,16 @@ def split_host_port(host_port: str) -> Tuple[str, str]: # port returned as strin raise ConnStringFormatError(_('Port number must be decimal')) return host, port -def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]: + +def extract_nodeid(connect_contents: str) -> Tuple[bytes, Optional[str]]: + """Takes a connection-string-like str, and returns a tuple (node_id, rest), + where rest is typically a host (with maybe port). Examples: + - extract_nodeid(pubkey@host:port) == (pubkey, host:port) + - extract_nodeid(pubkey@host) == (pubkey, host) + - extract_nodeid(pubkey) == (pubkey, None) + - extract_nodeid(bolt11_invoice) == (pubkey, None) + Can raise ConnStringFormatError. + """ rest = None try: # connection string? diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 5133c1ad3..30926bcb3 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -2,6 +2,7 @@ import json from electrum import bitcoin +from electrum import ecc from electrum.json_db import StoredDict from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_seed, make_offered_htlc, make_received_htlc, make_commitment, make_htlc_tx_witness, make_htlc_tx_output, @@ -756,11 +757,23 @@ def test_split_host_port(self): split_host_port("electrum.org:") def test_extract_nodeid(self): + pubkey1 = ecc.GENERATOR.get_public_key_bytes(compressed=True) with self.assertRaises(ConnStringFormatError): extract_nodeid("00" * 32 + "@localhost") with self.assertRaises(ConnStringFormatError): extract_nodeid("00" * 33 + "@") + # pubkey + host self.assertEqual(extract_nodeid("00" * 33 + "@localhost"), (b"\x00" * 33, "localhost")) + self.assertEqual(extract_nodeid(f"{pubkey1.hex()}@11.22.33.44"), (pubkey1, "11.22.33.44")) + self.assertEqual(extract_nodeid(f"{pubkey1.hex()}@[2001:41d0:e:734::1]"), (pubkey1, "[2001:41d0:e:734::1]")) + # pubkey + host + port + self.assertEqual(extract_nodeid(f"{pubkey1.hex()}@11.22.33.44:5555"), (pubkey1, "11.22.33.44:5555")) + self.assertEqual(extract_nodeid(f"{pubkey1.hex()}@[2001:41d0:e:734::1]:8888"), (pubkey1, "[2001:41d0:e:734::1]:8888")) + # just pubkey + self.assertEqual(extract_nodeid(f"{pubkey1.hex()}"), (pubkey1, None)) + # bolt11 invoice + self.assertEqual(extract_nodeid("lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqszrwfgrl5k3rt4q4mclc8t00p2tcjsf9pmpcq6lu5zhmampyvk43fk30eqpdm8t5qmdpzan25aqxqaqdzmy0smrtduazjcxx975vz78ccpx0qhev"), + (bfh("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), None)) def test_ln_features_validate_transitive_dependencies(self): features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ From c0f8986188e2c46336b55b811b7e8dd9cca2813f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Apr 2023 12:33:53 +0000 Subject: [PATCH 0662/1143] qml: QEChannelOpener: accept any connection strings other GUIs accept trying to paste a bare nodeid errored silently, logging: 174.76 | D | gui.qml.qechannelopener | AttributeError("'NoneType' object has no attribute '__contains__'") --- .../gui/qml/components/OpenChannelDialog.qml | 18 ++--- electrum/gui/qml/qechannelopener.py | 79 ++++++++++--------- 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 31fe0f4ee..759ff4190 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -55,7 +55,7 @@ ElDialog { placeholderText: qsTr('Paste or scan node uri/pubkey') onActiveFocusChanged: { if (!activeFocus) - channelopener.nodeid = text + channelopener.connectStr = text } } @@ -67,9 +67,9 @@ ElDialog { icon.height: constants.iconSizeMedium icon.width: constants.iconSizeMedium onClicked: { - if (channelopener.validate_nodeid(AppController.clipboardToText())) { - channelopener.nodeid = AppController.clipboardToText() - node.text = channelopener.nodeid + if (channelopener.validate_connect_str(AppController.clipboardToText())) { + channelopener.connectStr = AppController.clipboardToText() + node.text = channelopener.connectStr } } } @@ -81,9 +81,9 @@ ElDialog { onClicked: { var page = app.stack.push(Qt.resolvedUrl('Scan.qml')) page.onFound.connect(function() { - if (channelopener.validate_nodeid(page.scanData)) { - channelopener.nodeid = page.scanData - node.text = channelopener.nodeid + if (channelopener.validate_connect_str(page.scanData)) { + channelopener.connectStr = page.scanData + node.text = channelopener.connectStr } app.stack.pop() }) @@ -99,13 +99,13 @@ ElDialog { model: channelopener.trampolineNodeNames onCurrentValueChanged: { if (activeFocus) - channelopener.nodeid = currentValue + channelopener.connectStr = currentValue } // preselect a random node Component.onCompleted: { if (!Config.useGossip) { currentIndex = Math.floor(Math.random() * channelopener.trampolineNodeNames.length) - channelopener.nodeid = currentValue + channelopener.connectStr = currentValue } } } diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 1e108bb2d..5abf5d627 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -7,7 +7,7 @@ from electrum.i18n import _ from electrum.gui import messages from electrum.util import bfh -from electrum.lnutil import extract_nodeid, LNPeerAddr, ln_dummy_address +from electrum.lnutil import extract_nodeid, LNPeerAddr, ln_dummy_address, ConnStringFormatError from electrum.lnworker import hardcoded_trampoline_nodes from electrum.logging import get_logger @@ -33,7 +33,7 @@ def __init__(self, parent=None): super().__init__(parent) self._wallet = None - self._nodeid = None + self._connect_str = None self._amount = QEAmount() self._valid = False self._opentx = None @@ -50,17 +50,17 @@ def wallet(self, wallet: QEWallet): self._wallet = wallet self.walletChanged.emit() - nodeidChanged = pyqtSignal() - @pyqtProperty(str, notify=nodeidChanged) - def nodeid(self): - return self._nodeid - - @nodeid.setter - def nodeid(self, nodeid: str): - if self._nodeid != nodeid: - self._logger.debug('nodeid set -> %s' % nodeid) - self._nodeid = nodeid - self.nodeidChanged.emit() + connectStrChanged = pyqtSignal() + @pyqtProperty(str, notify=connectStrChanged) + def connectStr(self): + return self._connect_str + + @connectStr.setter + def connectStr(self, connect_str: str): + if self._connect_str != connect_str: + self._logger.debug('connectStr set -> %s' % connect_str) + self._connect_str = connect_str + self.connectStrChanged.emit() self.validate() amountChanged = pyqtSignal() @@ -97,20 +97,27 @@ def trampolineNodeNames(self): # FIXME min channel funding amount # FIXME have requested funding amount def validate(self): - nodeid_valid = False - if self._nodeid: - self._logger.debug(f'checking if {self._nodeid} is valid') + """side-effects: sets self._valid, self._node_pubkey, self._connect_str_resolved""" + connect_str_valid = False + if self._connect_str: + self._logger.debug(f'checking if {self._connect_str=!r} is valid') if not self._wallet.wallet.config.get('use_gossip', False): - self._peer = hardcoded_trampoline_nodes()[self._nodeid] - nodeid_valid = True + # using trampoline: connect_str is the name of a trampoline node + peer_addr = hardcoded_trampoline_nodes()[self._connect_str] + self._node_pubkey = peer_addr.pubkey + self._connect_str_resolved = str(peer_addr) + connect_str_valid = True else: + # using gossip: connect_str is anything extract_nodeid() can parse try: - self._peer = self.nodeid_to_lnpeer(self._nodeid) - nodeid_valid = True - except: + self._node_pubkey, _rest = extract_nodeid(self._connect_str) + except ConnStringFormatError: pass + else: + self._connect_str_resolved = self._connect_str + connect_str_valid = True - if not nodeid_valid: + if not connect_str_valid: self._valid = False self.validChanged.emit() return @@ -125,23 +132,14 @@ def validate(self): self.validChanged.emit() @pyqtSlot(str, result=bool) - def validate_nodeid(self, nodeid): + def validate_connect_str(self, connect_str): try: - self.nodeid_to_lnpeer(nodeid) - except Exception as e: - self._logger.debug(f"invalid nodeid. {e!r}") + node_id, rest = extract_nodeid(connect_str) + except ConnStringFormatError as e: + self._logger.debug(f"invalid connect_str. {e!r}") return False return True - def nodeid_to_lnpeer(self, nodeid): - node_pubkey, host_port = extract_nodeid(nodeid) - if host_port.__contains__(':'): # FIXME host_port can be None; can't construct LNPeerAddr then. - host, port = host_port.split(':',1) - else: - host = host_port - port = 9735 - return LNPeerAddr(host, int(port), node_pubkey) - # FIXME "max" button in amount_dialog should enforce LN_MAX_FUNDING_SAT @pyqtSlot() @pyqtSlot(bool) @@ -149,10 +147,10 @@ def open_channel(self, confirm_backup_conflict=False): if not self.valid: return - self._logger.debug('Connect String: %s' % str(self._peer)) + self._logger.debug(f'Connect String: {self._connect_str!r}') lnworker = self._wallet.wallet.lnworker - if lnworker.has_conflicting_backup_with(self._peer.pubkey) and not confirm_backup_conflict: + if lnworker.has_conflicting_backup_with(self._node_pubkey) and not confirm_backup_conflict: self.conflictingBackup.emit(messages.MGS_CONFLICTING_BACKUP_INSTANCE) return @@ -164,10 +162,10 @@ def open_channel(self, confirm_backup_conflict=False): mktx = lambda amt: lnworker.mktx_for_open_channel( coins=coins, funding_sat=amt, - node_id=self._peer.pubkey, + node_id=self._node_pubkey, fee_est=None) - acpt = lambda tx: self.do_open_channel(tx, str(self._peer), self._wallet.password) + acpt = lambda tx: self.do_open_channel(tx, self._connect_str_resolved, self._wallet.password) self._finalizer = QETxFinalizer(self, make_tx=mktx, accept=acpt) self._finalizer.canRbf = False @@ -177,6 +175,9 @@ def open_channel(self, confirm_backup_conflict=False): @auth_protect def do_open_channel(self, funding_tx, conn_str, password): + """ + conn_str: a connection string that extract_nodeid can parse, i.e. cannot be a trampoline name + """ self._logger.debug('opening channel') # read funding_sat from tx; converts '!' to int value funding_sat = funding_tx.output_value_for_address(ln_dummy_address()) From 40cfa62c2db566275801d10c7d5726511a426ed7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Apr 2023 16:56:14 +0000 Subject: [PATCH 0663/1143] android readme: update "access datadir on Android from desktop" Added a section re pushing wallet to device. It is surprisingly tricky, but using specifically the "/data/local/tmp" folder as an intermediary, it works. --- contrib/android/Readme.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/contrib/android/Readme.md b/contrib/android/Readme.md index 059370fb3..92f57d20a 100644 --- a/contrib/android/Readme.md +++ b/contrib/android/Readme.md @@ -126,12 +126,21 @@ and [android dev docs](https://developer.android.com/studio/build/building-cmdli Note that this only works for debug builds! Otherwise the security model of Android does not let you access the internal storage of an app without root. (See [this](https://stackoverflow.com/q/9017073)) +To pull a file: ``` $ adb shell -$ run-as org.electrum.electrum ls /data/data/org.electrum.electrum/files/data -$ exit # to exit adb +adb$ run-as org.electrum.electrum ls /data/data/org.electrum.electrum/files/data +adb$ exit $ adb exec-out run-as org.electrum.electrum cat /data/data/org.electrum.electrum/files/data/wallets/my_wallet > my_wallet ``` +To push a file: +``` +$ adb push ~/wspace/tmp/my_wallet /data/local/tmp +$ adb shell +adb$ ls -la /data/local/tmp +adb$ run-as org.electrum.testnet.electrum cp /data/local/tmp/my_wallet /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets/ +adb$ rm /data/local/tmp/my_wallet +``` Or use Android Studio: "Device File Explorer", which can download/upload data directly from device (via adb). From e77b0560bf1ccc5e35578e3eac619c60de8a4b08 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 6 Apr 2023 17:10:46 +0000 Subject: [PATCH 0664/1143] android: fix notifications by fixing "plyer" dependency upstreamed patch at https://github.com/kivy/plyer/pull/756 --- contrib/android/p4a_recipes/plyer/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contrib/android/p4a_recipes/plyer/__init__.py b/contrib/android/p4a_recipes/plyer/__init__.py index 731dc22e8..491e00079 100644 --- a/contrib/android/p4a_recipes/plyer/__init__.py +++ b/contrib/android/p4a_recipes/plyer/__init__.py @@ -6,9 +6,8 @@ class PlyerRecipePinned(PythonRecipe): - version = "2.0.0" - sha512sum = "8088eeb41aac753435ff5be9835be74d57a55cf557ad76cbad8026352647e554571fae6172754e39882ea7ef07cc1e97fac16556a4426456de99daebe5cd01cf" - url = "https://pypi.python.org/packages/source/p/plyer/plyer-{version}.tar.gz" + version = "5262087c85b2c82c69e702fe944069f1d8465fdf" + url = "git+https://github.com/SomberNight/plyer" depends = ["setuptools"] From 750a9b3613ec15b4330cff7598f0995cfc10ff94 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 9 Apr 2023 11:29:14 +0200 Subject: [PATCH 0665/1143] network: remove network.notify() method; not really useful. Not worth the added complexity. --- electrum/network.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/electrum/network.py b/electrum/network.py index 1a3ffebc1..52fc7948d 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -425,7 +425,7 @@ async def _server_is_lagging(self) -> bool: def _set_status(self, status): self.connection_status = status - self.notify('status') + util.trigger_callback('status') def is_connected(self): interface = self.interface @@ -440,7 +440,7 @@ async def _request_server_info(self, interface: 'Interface'): async def get_banner(): self.banner = await interface.get_server_banner() - self.notify('banner') + util.trigger_callback('banner', self.banner) async def get_donation_address(): self.donation_address = await interface.get_donation_address() async def get_server_peers(): @@ -450,7 +450,7 @@ async def get_server_peers(): server_peers = server_peers[:max_accepted_peers] # note that 'parse_servers' also validates the data (which is untrusted input!) self.server_peers = parse_servers(server_peers) - self.notify('servers') + util.trigger_callback('servers', self.get_servers()) async def get_relay_fee(): self.relay_fee = await interface.get_relay_fee() @@ -466,28 +466,7 @@ async def _request_fee_estimates(self, interface): histogram = await interface.get_fee_histogram() self.config.mempool_fees = histogram self.logger.info(f'fee_histogram {histogram}') - self.notify('fee_histogram') - - def get_status_value(self, key): - if key == 'status': - value = self.connection_status - elif key == 'banner': - value = self.banner - elif key == 'fee': - value = self.config.fee_estimates - elif key == 'fee_histogram': - value = self.config.mempool_fees - elif key == 'servers': - value = self.get_servers() - else: - raise Exception('unexpected trigger key {}'.format(key)) - return value - - def notify(self, key): - if key in ['status', 'updated']: - util.trigger_callback(key) - else: - util.trigger_callback(key, self.get_status_value(key)) + util.trigger_callback('fee_histogram', self.config.mempool_fees) def get_parameters(self) -> NetworkParameters: return NetworkParameters(server=self.default_server, @@ -539,7 +518,7 @@ def update_fee_estimates(self, *, fee_est: Dict[int, int] = None): if not hasattr(self, "_prev_fee_est") or self._prev_fee_est != fee_est: self._prev_fee_est = copy.copy(fee_est) self.logger.info(f'fee_estimates {fee_est}') - self.notify('fee') + util.trigger_callback('fee', self.config.fee_estimates) @with_recent_servers_lock def get_servers(self): From ddd778f7f7c9d00dcaf6d62ca6f1abd702a82553 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 9 Apr 2023 11:32:01 +0200 Subject: [PATCH 0666/1143] follow-up previous commit --- electrum/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 52fc7948d..977a83990 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -425,7 +425,7 @@ async def _server_is_lagging(self) -> bool: def _set_status(self, status): self.connection_status = status - util.trigger_callback('status') + util.trigger_callback('status', self.connection_status) def is_connected(self): interface = self.interface From 697bf2b1c89e3792ac5dc6077de14d6af029e81b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 10 Apr 2023 21:23:50 +0200 Subject: [PATCH 0667/1143] Revert commit ddd778f7f7c9d00dcaf6d62ca6f1abd702a82553. This fixes #8298. The initial commit was good. --- electrum/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 977a83990..52fc7948d 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -425,7 +425,7 @@ async def _server_is_lagging(self) -> bool: def _set_status(self, status): self.connection_status = status - util.trigger_callback('status', self.connection_status) + util.trigger_callback('status') def is_connected(self): interface = self.interface From 75f63a4666a93707d5b5450a614f655b17fb7e40 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Apr 2023 10:22:48 +0200 Subject: [PATCH 0668/1143] qml: remove yesClicked signal, use accept()/accepted signal in MessageDialog. --- electrum/gui/qml/components/ChannelDetails.qml | 2 +- electrum/gui/qml/components/MessageDialog.qml | 14 +++----------- electrum/gui/qml/components/OpenChannelDialog.qml | 2 +- electrum/gui/qml/components/Preferences.qml | 2 +- electrum/gui/qml/components/TxDetails.qml | 3 +-- electrum/gui/qml/components/WalletDetails.qml | 8 ++++---- electrum/gui/qml/components/WalletMainView.qml | 2 +- electrum/gui/qml/components/main.qml | 6 ++---- 8 files changed, 14 insertions(+), 25 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 99d9fdd01..b57708bc5 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -249,7 +249,7 @@ Pane { : qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), yesno: true }) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { channeldetails.deleteChannel() app.stack.pop() Daemon.currentWallet.historyModel.init_model(true) // needed here? diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index b8c10335c..4b01c915a 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -16,8 +16,6 @@ ElDialog { property alias text: message.text property bool richText: false - signal yesClicked - z: 1 // raise z so it also covers dialogs using overlay as parent anchors.centerIn: parent @@ -49,7 +47,7 @@ ElDialog { text: qsTr('Ok') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: !yesno - onClicked: dialog.close() + onClicked: accept() } FlatButton { @@ -59,10 +57,7 @@ ElDialog { text: qsTr('Yes') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: yesno - onClicked: { - yesClicked() - dialog.close() - } + onClicked: accept() } FlatButton { Layout.fillWidth: true @@ -71,10 +66,7 @@ ElDialog { text: qsTr('No') icon.source: Qt.resolvedUrl('../../icons/closebutton.png') visible: yesno - onClicked: { - reject() - dialog.close() - } + onClicked: reject() } } } diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 759ff4190..1870d8186 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -194,7 +194,7 @@ ElDialog { onConflictingBackup: { var dialog = app.messageDialog.createObject(app, { 'text': message, 'yesno': true }) dialog.open() - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { channelopener.open_channel(true) }) } diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index bc090c9ea..b4d07c127 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -258,7 +258,7 @@ Pane { text: qsTr('Using plain gossip mode is not recommended on mobile. Are you sure?'), yesno: true }) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { Config.useGossip = true }) dialog.rejected.connect(function() { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index b8c99bd96..6e46904f2 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -400,8 +400,7 @@ Pane { onLabelChanged: root.detailsChanged() onConfirmRemoveLocalTx: { var dialog = app.messageDialog.createObject(app, { text: message, yesno: true }) - dialog.yesClicked.connect(function() { - dialog.close() + dialog.accepted.connect(function() { txdetails.removeLocalTx(true) root.close() }) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 1c3e977ac..290b25bf0 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -18,7 +18,7 @@ Pane { function enableLightning() { var dialog = app.messageDialog.createObject(rootItem, {'text': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { Daemon.currentWallet.enableLightning() }) dialog.open() @@ -27,7 +27,7 @@ Pane { function deleteWallet() { var dialog = app.messageDialog.createObject(rootItem, {'text': qsTr('Really delete this wallet?'), 'yesno': true}) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet) }) dialog.open() @@ -476,13 +476,13 @@ Pane { function onWalletDeleteError(code, message) { if (code == 'unpaid_requests') { var dialog = app.messageDialog.createObject(app, {text: message, yesno: true }) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet, true) }) dialog.open() } else if (code == 'balance') { var dialog = app.messageDialog.createObject(app, {text: message, yesno: true }) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet, true, true) }) dialog.open() diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 30d56158f..921e5a117 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -372,7 +372,7 @@ Item { text: qsTr('Import Channel backup?'), yesno: true }) - dialog.yesClicked.connect(function() { + dialog.accepted.connect(function() { Daemon.currentWallet.importChannelBackup(data) close() }) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 7baaf45ee..2a83729a0 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -382,8 +382,7 @@ ApplicationWindow wallet: Daemon.currentWallet onConfirm: { var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) - dialog.yesClicked.connect(function() { - dialog.close() + dialog.accepted.connect(function() { __swaphelper.executeSwap(true) }) dialog.open() @@ -459,8 +458,7 @@ ApplicationWindow text: qsTr('Close Electrum?'), yesno: true }) - dialog.yesClicked.connect(function() { - dialog.close() + dialog.accepted.connect(function() { app._wantClose = true app.close() }) From a0939aad753e212064645be3bc3171cb1f50829a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Apr 2023 11:29:00 +0200 Subject: [PATCH 0669/1143] qml: add doAccept and doReject functions to ElDialog. These functions make sure no duplicate accepted/rejected signals are emitted. --- .../components/ImportAddressesKeysDialog.qml | 2 +- .../components/ImportChannelBackupDialog.qml | 9 ++++---- electrum/gui/qml/components/MessageDialog.qml | 6 +++--- .../gui/qml/components/PasswordDialog.qml | 2 +- .../qml/components/ReceiveDetailsDialog.qml | 2 +- electrum/gui/qml/components/TxDetails.qml | 8 +++---- .../gui/qml/components/controls/ElDialog.qml | 21 ++++++++++++++++++- electrum/gui/qml/components/wizard/Wizard.qml | 4 ++-- 8 files changed, 37 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml index 10a069b19..703543221 100644 --- a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml +++ b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml @@ -104,7 +104,7 @@ ElDialog { Layout.fillWidth: true text: qsTr('Import') enabled: valid - onClicked: accept() + onClicked: doAccept() } } diff --git a/electrum/gui/qml/components/ImportChannelBackupDialog.qml b/electrum/gui/qml/components/ImportChannelBackupDialog.qml index 42dce5d96..309066ac5 100644 --- a/electrum/gui/qml/components/ImportChannelBackupDialog.qml +++ b/electrum/gui/qml/components/ImportChannelBackupDialog.qml @@ -23,6 +23,10 @@ ElDialog { return valid = Daemon.currentWallet.isValidChannelBackup(text) } + onAccepted: { + Daemon.currentWallet.importChannelBackup(channelbackup_ta.text) + } + ColumnLayout { anchors.fill: parent spacing: 0 @@ -84,10 +88,7 @@ ElDialog { Layout.fillWidth: true enabled: valid text: qsTr('Import') - onClicked: { - Daemon.currentWallet.importChannelBackup(channelbackup_ta.text) - root.accept() - } + onClicked: doAccept() } } diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 4b01c915a..1256df7e8 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -47,7 +47,7 @@ ElDialog { text: qsTr('Ok') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: !yesno - onClicked: accept() + onClicked: doAccept() } FlatButton { @@ -57,7 +57,7 @@ ElDialog { text: qsTr('Yes') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: yesno - onClicked: accept() + onClicked: doAccept() } FlatButton { Layout.fillWidth: true @@ -66,7 +66,7 @@ ElDialog { text: qsTr('No') icon.source: Qt.resolvedUrl('../../icons/closebutton.png') visible: yesno - onClicked: reject() + onClicked: doReject() } } } diff --git a/electrum/gui/qml/components/PasswordDialog.qml b/electrum/gui/qml/components/PasswordDialog.qml index 85d2dc853..4b3cef5e4 100644 --- a/electrum/gui/qml/components/PasswordDialog.qml +++ b/electrum/gui/qml/components/PasswordDialog.qml @@ -73,7 +73,7 @@ ElDialog { enabled: confirmPassword ? pw_1.text.length >= 6 && pw_1.text == pw_2.text : true onClicked: { password = pw_1.text - passworddialog.accept() + passworddialog.doAccept() } } } diff --git a/electrum/gui/qml/components/ReceiveDetailsDialog.qml b/electrum/gui/qml/components/ReceiveDetailsDialog.qml index 4950f1e34..0f663fb73 100644 --- a/electrum/gui/qml/components/ReceiveDetailsDialog.qml +++ b/electrum/gui/qml/components/ReceiveDetailsDialog.qml @@ -97,7 +97,7 @@ ElDialog { Layout.fillWidth: true text: qsTr('Create request') icon.source: '../../icons/confirmed.png' - onClicked: accept() + onClicked: doAccept() } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 6e46904f2..0a2878dcb 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -57,10 +57,10 @@ Pane { Layout.bottomMargin: constants.paddingLarge visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel || txdetails.canRemove text: txdetails.canRemove - ? qsTr('This transaction is local to your wallet. It has not been published yet.') - : qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel - ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') - : qsTr('You can bump its fee to speed up its confirmation')) + ? qsTr('This transaction is local to your wallet. It has not been published yet.') + : qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel + ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') + : qsTr('You can bump its fee to speed up its confirmation')) } RowLayout { diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index 82bafd16f..ebc133919 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -9,8 +9,27 @@ Dialog { property string iconSource property bool resizeWithKeyboard: true + property bool _result: false + + // called to finally close dialog after checks by onClosing handler in main.qml function doClose() { - close() + doReject() + } + + // avoid potential multiple signals, only emit once + function doAccept() { + if (_result) + return + _result = true + accept() + } + + // avoid potential multiple signals, only emit once + function doReject() { + if (_result) + return + _result = true + reject() } parent: resizeWithKeyboard ? Overlay.overlay.children[0] : Overlay.overlay diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index 80818c0db..5df89f842 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -131,7 +131,7 @@ ElDialog { function finish() { currentItem.accept() _setWizardData(pages.contentChildren[currentIndex].wizard_data) - wizard.accept() + wizard.doAccept() } property bool pagevalid: false @@ -163,7 +163,7 @@ ElDialog { Layout.preferredWidth: 1 visible: pages.currentIndex == 0 text: qsTr("Cancel") - onClicked: wizard.reject() + onClicked: wizard.doReject() } FlatButton { Layout.fillWidth: true From 9bbc354e0ebfc49e51a6557f98e9fd94073515ec Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Apr 2023 12:13:41 +0200 Subject: [PATCH 0670/1143] qml: refactor txaccepted/txcancelled signals to standard accepted/rejected. --- electrum/gui/qml/components/ConfirmTxDialog.qml | 10 ++-------- electrum/gui/qml/components/CpfpBumpFeeDialog.qml | 9 ++------- electrum/gui/qml/components/OpenChannelDialog.qml | 2 +- electrum/gui/qml/components/RbfBumpFeeDialog.qml | 9 ++------- electrum/gui/qml/components/RbfCancelDialog.qml | 9 ++------- electrum/gui/qml/components/TxDetails.qml | 6 +++--- electrum/gui/qml/components/WalletMainView.qml | 2 +- 7 files changed, 13 insertions(+), 34 deletions(-) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 1dc5b22c9..e1241fbe6 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -17,9 +17,6 @@ ElDialog { property alias amountLabelText: amountLabel.text property alias sendButtonText: sendButton.text - signal txcancelled - signal txaccepted - title: qsTr('Confirm Transaction') // copy these to finalizer @@ -223,12 +220,9 @@ ElDialog { : qsTr('Pay') icon.source: '../../icons/confirmed.png' enabled: finalizer.valid - onClicked: { - txaccepted() - dialog.close() - } + onClicked: doAccept() } } - onClosed: txcancelled() + onClosed: doReject() } diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index a81c235a6..94811b27d 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -13,8 +13,6 @@ ElDialog { required property string txid required property QtObject cpfpfeebumper - signal txaccepted - title: qsTr('Bump Fee') iconSource: Qt.resolvedUrl('../../icons/rocket.png') @@ -224,17 +222,14 @@ ElDialog { text: qsTr('Ok') icon.source: '../../icons/confirmed.png' enabled: cpfpfeebumper.valid - onClicked: { - txaccepted() - dialog.close() - } + onClicked: doAccept() } } Connections { target: cpfpfeebumper function onTxMined() { - dialog.close() + dialog.doReject() } } } diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 1870d8186..51a1b104c 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -202,7 +202,7 @@ ElDialog { var dialog = confirmOpenChannelDialog.createObject(app, { 'satoshis': channelopener.amount }) - dialog.txaccepted.connect(function() { + dialog.accepted.connect(function() { dialog.finalizer.signAndSend() }) dialog.open() diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 0d05b118d..e613b46d6 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -13,8 +13,6 @@ ElDialog { required property string txid required property QtObject rbffeebumper - signal txaccepted - title: qsTr('Bump Fee') iconSource: Qt.resolvedUrl('../../icons/rocket.png') @@ -237,17 +235,14 @@ ElDialog { text: qsTr('Ok') icon.source: '../../icons/confirmed.png' enabled: rbffeebumper.valid - onClicked: { - txaccepted() - dialog.close() - } + onClicked: doAccept() } } Connections { target: rbffeebumper function onTxMined() { - dialog.close() + dialog.doReject() } } } diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 9238592e4..b6727e96c 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -13,8 +13,6 @@ ElDialog { required property string txid required property QtObject txcanceller - signal txaccepted - title: qsTr('Cancel Transaction') width: parent.width @@ -207,17 +205,14 @@ ElDialog { text: qsTr('Ok') icon.source: '../../icons/confirmed.png' enabled: txcanceller.valid - onClicked: { - txaccepted() - dialog.close() - } + onClicked: doAccept() } } Connections { target: txcanceller function onTxMined() { - dialog.close() + dialog.doReject() } } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 0a2878dcb..723d5e395 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -440,7 +440,7 @@ Pane { wallet: Daemon.currentWallet txid: dialog.txid } - onTxaccepted: { + onAccepted: { root.rawtx = rbffeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign_and_broadcast() @@ -465,7 +465,7 @@ Pane { txid: dialog.txid } - onTxaccepted: { + onAccepted: { // replaces parent tx with cpfp tx root.rawtx = cpfpfeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { @@ -491,7 +491,7 @@ Pane { txid: dialog.txid } - onTxaccepted: { + onAccepted: { root.rawtx = txcanceller.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.sign_and_broadcast() diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 921e5a117..3e0501977 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -328,7 +328,7 @@ Item { message: invoice.message }) var canComplete = !Daemon.currentWallet.isWatchOnly && Daemon.currentWallet.canSignWithoutCosigner - dialog.txaccepted.connect(function() { + dialog.accepted.connect(function() { if (!canComplete) { if (Daemon.currentWallet.isWatchOnly) { dialog.finalizer.save() From 4d5be450d9aec7078112af2428bbf7fa1d428c6e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Apr 2023 15:19:29 +0200 Subject: [PATCH 0671/1143] qml: default to system locale on android --- electrum/gui/qml/__init__.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 114551751..a09539c5b 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -19,7 +19,7 @@ QT_VERSION_STR, PYQT_VERSION_STR) from PyQt5.QtGui import QGuiApplication -from electrum.i18n import set_language, languages, language +from electrum.i18n import _, set_language, languages from electrum.plugin import run_hook from electrum.util import profiler from electrum.logging import Logger @@ -33,15 +33,16 @@ from .qeapp import ElectrumQmlApplication, Exception_Hook +if 'ANDROID_DATA' in os.environ: + from jnius import autoclass, cast + jLocale = autoclass("java.util.Locale") + class ElectrumTranslator(QTranslator): def __init__(self, parent=None): super().__init__(parent) def translate(self, context, source_text, disambiguation, n): - if source_text == "": - return "" - return language.gettext(source_text) - + return _(source_text) class ElectrumGui(BaseElectrumGui, Logger): @@ -49,7 +50,12 @@ class ElectrumGui(BaseElectrumGui, Logger): def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) - set_language(config.get('language', self.get_default_language())) + + lang = config.get('language','') + if not lang: + lang = self.get_default_language() + self.logger.info(f'setting language {lang}') + set_language(lang) # uncomment to debug plugin and import tracing # os.environ['QML_IMPORT_TRACE'] = '1' @@ -117,8 +123,10 @@ def stop(self): self.app.quit() def get_default_language(self): - # On Android this does not return the system locale - # TODO: retrieve through Android API - name = QLocale.system().name() - self.logger.debug(f'System default locale: {name}') + # On Android QLocale does not return the system locale + try: + name = str(jLocale.getDefault().toString()) + except Exception: + name = QLocale.system().name() + self.logger.info(f'System default locale: {name}') return name if name in languages else 'en_GB' From 0544c4b6516740decd69ab51a969a4787ad48a85 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Apr 2023 11:28:16 +0200 Subject: [PATCH 0672/1143] payserver: fix #8299 --- electrum/plugins/payserver/payserver.py | 1 - electrum/plugins/payserver/qt.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index 2e45401de..8f46c99bf 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -70,7 +70,6 @@ def wallet_export_request(self, d, key): if view_url := self.view_url(key): d['view_url'] = view_url - class PayServer(Logger, EventListener): WWW_DIR = os.path.join(os.path.dirname(__file__), 'www') diff --git a/electrum/plugins/payserver/qt.py b/electrum/plugins/payserver/qt.py index 2e485e62a..f6f18aeaa 100644 --- a/electrum/plugins/payserver/qt.py +++ b/electrum/plugins/payserver/qt.py @@ -33,6 +33,18 @@ class Plugin(PayServerPlugin): + _init_qt_received = False + + @hook + def init_qt(self, gui: 'ElectrumGui'): + if self._init_qt_received: # only need/want the first signal + return + self._init_qt_received = True + # If the user just enabled the plugin, the 'load_wallet' hook would not + # get called for already loaded wallets, hence we call it manually for those: + for window in gui.windows: + self.daemon_wallet_loaded(gui.daemon, window.wallet) + def requires_settings(self): return True @@ -68,6 +80,8 @@ def settings_dialog(self, window: WindowModalDialog): self.config.set_key('payserver_address', str(address_e.text())) self.config.set_key('ssl_keyfile', str(keyfile_e.text())) self.config.set_key('ssl_certfile', str(certfile_e.text())) + # fixme: restart the server + window.show_message('Please restart Electrum to enable those changes') @hook def receive_list_menu(self, parent, menu, key): From 2203bba4ea9d347fb4b9990bd762a065420efb31 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Apr 2023 11:59:52 +0200 Subject: [PATCH 0673/1143] fix flake8 test --- electrum/plugins/payserver/qt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/plugins/payserver/qt.py b/electrum/plugins/payserver/qt.py index f6f18aeaa..15ddab07c 100644 --- a/electrum/plugins/payserver/qt.py +++ b/electrum/plugins/payserver/qt.py @@ -30,6 +30,9 @@ from electrum.gui.qt.util import WindowModalDialog, OkButton, Buttons, EnterButton, webopen from .payserver import PayServerPlugin +if TYPE_CHECKING: + from electrum.gui.qt import ElectrumGui + class Plugin(PayServerPlugin): From 46b25317a98805fd81497c838ea2335e59d1c23b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Apr 2023 12:01:47 +0200 Subject: [PATCH 0674/1143] qt and qml: update swap dialogs when new fees are received (see #8295) --- electrum/gui/qml/qenetwork.py | 1 + electrum/gui/qml/qeswaphelper.py | 15 +++++++++++++-- electrum/gui/qt/swap_dialog.py | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index 11ed92cb1..b6eda0bb4 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -55,6 +55,7 @@ def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): self._height = network.get_local_height() # init here, update event can take a while self._server_height = network.get_server_height() # init here, update event can take a while self.register_callbacks() + self.destroyed.connect(self.unregister_callbacks) self._qeconfig.useGossipChanged.connect(self.on_gossip_setting_changed) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 393067cad..e9d019113 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -14,8 +14,9 @@ from .auth import AuthMixin, auth_protect from .qetypes import QEAmount from .qewallet import QEWallet +from .util import QtEventListener, qt_event_listener -class QESwapHelper(AuthMixin, QObject): +class QESwapHelper(AuthMixin, QObject, QtEventListener): _logger = get_logger(__name__) confirm = pyqtSignal([str], arguments=['message']) @@ -52,12 +53,14 @@ def __init__(self, parent=None): self._leftVoid = 0 self._rightVoid = 0 + self.register_callbacks() + self.destroyed.connect(self.unregister_callbacks) + self._fwd_swap_updatetx_timer = QTimer(self) self._fwd_swap_updatetx_timer.setSingleShot(True) # self._fwd_swap_updatetx_timer.setInterval(500) self._fwd_swap_updatetx_timer.timeout.connect(self.fwd_swap_updatetx) - walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) def wallet(self): @@ -281,6 +284,14 @@ def update_tx(self, onchain_amount: Union[int, str]): self._tx = None self.valid = False + @qt_event_listener + def on_event_fee_histogram(self, *args): + self.swap_slider_moved() + + @qt_event_listener + def on_event_fee(self, *args): + self.swap_slider_moved() + def swap_slider_moved(self): if not self._service_available: return diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 4b8902843..5966e029e 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -12,6 +12,7 @@ from . import util from .util import (WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit) +from .util import qt_event_listener, QtEventListener from .amountedit import BTCAmountEdit from .fee_slider import FeeSlider, FeeComboBox from .my_treeview import create_toolbar_with_menu @@ -27,7 +28,7 @@ """ -class SwapDialog(WindowModalDialog): +class SwapDialog(WindowModalDialog, QtEventListener): tx: Optional[PartialTransaction] @@ -103,6 +104,21 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No self.update() self.needs_tx_update = True self.window.gui_object.timer.timeout.connect(self.timer_actions) + self.register_callbacks() + + def closeEvent(self, event): + self.unregister_callbacks() + event.accept() + + @qt_event_listener + def on_event_fee_histogram(self, *args): + self.on_send_edited() + self.on_recv_edited() + + @qt_event_listener + def on_event_fee(self, *args): + self.on_send_edited() + self.on_recv_edited() def timer_actions(self): if self.needs_tx_update: From 2773e0d8b8eb0b8581ad0ed3b5f27dba28a93c11 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 12:14:11 +0200 Subject: [PATCH 0675/1143] qml: fix 46b25317a98805fd81497c838ea2335e59d1c23b --- electrum/gui/qml/qenetwork.py | 5 ++++- electrum/gui/qml/qeswaphelper.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index b6eda0bb4..bcc930500 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -55,10 +55,13 @@ def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): self._height = network.get_local_height() # init here, update event can take a while self._server_height = network.get_server_height() # init here, update event can take a while self.register_callbacks() - self.destroyed.connect(self.unregister_callbacks) + self.destroyed.connect(lambda: self.on_destroy()) self._qeconfig.useGossipChanged.connect(self.on_gossip_setting_changed) + def on_destroy(self): + self.self.unregister_callbacks() + @event_listener def on_event_network_updated(self, *args): self.networkUpdated.emit() diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index e9d019113..be519e4d3 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -54,13 +54,16 @@ def __init__(self, parent=None): self._rightVoid = 0 self.register_callbacks() - self.destroyed.connect(self.unregister_callbacks) + self.destroyed.connect(lambda: self.on_destroy()) self._fwd_swap_updatetx_timer = QTimer(self) self._fwd_swap_updatetx_timer.setSingleShot(True) # self._fwd_swap_updatetx_timer.setInterval(500) self._fwd_swap_updatetx_timer.timeout.connect(self.fwd_swap_updatetx) + def on_destroy(self): + self.unregister_callbacks() + walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) def wallet(self): From 2e70776a72b9d7f600b3cfb6ab6509f1cec6669d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 12 Apr 2023 12:20:39 +0200 Subject: [PATCH 0676/1143] fix missing import --- electrum/plugins/payserver/qt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/plugins/payserver/qt.py b/electrum/plugins/payserver/qt.py index 15ddab07c..48c65e03c 100644 --- a/electrum/plugins/payserver/qt.py +++ b/electrum/plugins/payserver/qt.py @@ -24,7 +24,9 @@ # SOFTWARE. from functools import partial +from typing import TYPE_CHECKING from PyQt5 import QtWidgets + from electrum.i18n import _ from electrum.plugin import hook from electrum.gui.qt.util import WindowModalDialog, OkButton, Buttons, EnterButton, webopen From 51356dff18694ab3767cd701ea290c1fc2ed5de6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 12:17:08 +0200 Subject: [PATCH 0677/1143] Revert "qml: add busy property and guards around swaphelper" This reverts commit 5d350184a4762f343b33b328906090d6b9788053. --- electrum/gui/qml/components/SwapDialog.qml | 2 +- electrum/gui/qml/qeswaphelper.py | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index 19c94d3ef..ba24db9cc 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -242,7 +242,7 @@ ElDialog { Layout.fillWidth: true text: qsTr('Ok') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') - enabled: swaphelper.valid && !swaphelper.busy + enabled: swaphelper.valid onClicked: { console.log('Swap triggered from dialog ' + this + ' using swaphelper ' + swaphelper) swaphelper.executeSwap() diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index be519e4d3..889066d2c 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -34,7 +34,6 @@ def __init__(self, parent=None): self._rangeMax = 0 self._tx = None self._valid = False - self._busy = False self._userinfo = ' '.join([ _('Move the slider to set the amount and direction of the swap.'), _('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'), @@ -208,17 +207,6 @@ def isReverse(self, isReverse): self._isReverse = isReverse self.isReverseChanged.emit() - busyChanged = pyqtSignal() - @pyqtProperty(bool, notify=busyChanged) - def busy(self): - return self._busy - - @busy.setter - def busy(self, busy): - if self._busy != busy: - self._busy = busy - self.busyChanged.emit() - def init_swap_slider_range(self): lnworker = self._wallet.wallet.lnworker @@ -375,8 +363,6 @@ def swap_task(): except Exception as e: self._logger.error(str(e)) self.swapFailed.emit(str(e)) - finally: - self.busy = False threading.Thread(target=swap_task, daemon=True).start() @@ -402,8 +388,6 @@ def swap_task(): except Exception as e: self._logger.error(str(e)) self.swapFailed.emit(str(e)) - finally: - self.busy = False threading.Thread(target=swap_task, daemon=True).start() @@ -413,11 +397,6 @@ def executeSwap(self, confirm=False): if not self._wallet.wallet.network: self.error.emit(_("You are offline.")) return - - if self._busy: - self._logger.error('swap already in progress for this swaphelper') - return - if confirm: self._do_execute_swap() return @@ -431,7 +410,6 @@ def executeSwap(self, confirm=False): @auth_protect def _do_execute_swap(self): - self.busy = True if self.isReverse: lightning_amount = self._send_amount onchain_amount = self._receive_amount From b0778d0281858614b8bb3267827490d0c3168495 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 12:31:16 +0200 Subject: [PATCH 0678/1143] qml: fix typo --- electrum/gui/qml/components/SwapProgressDialog.qml | 1 + electrum/gui/qml/qenetwork.py | 2 +- electrum/gui/qml/qeswaphelper.py | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/SwapProgressDialog.qml b/electrum/gui/qml/components/SwapProgressDialog.qml index 308aecdb1..56abbb988 100644 --- a/electrum/gui/qml/components/SwapProgressDialog.qml +++ b/electrum/gui/qml/components/SwapProgressDialog.qml @@ -14,6 +14,7 @@ ElDialog { width: parent.width height: parent.height + resizeWithKeyboard: false iconSource: Qt.resolvedUrl('../../icons/update.png') title: swaphelper.isReverse diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index bcc930500..eeef2d096 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -60,7 +60,7 @@ def __init__(self, network: 'Network', qeconfig: 'QEConfig', parent=None): self._qeconfig.useGossipChanged.connect(self.on_gossip_setting_changed) def on_destroy(self): - self.self.unregister_callbacks() + self.unregister_callbacks() @event_listener def on_event_network_updated(self, *args): diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 889066d2c..c533e7389 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -363,6 +363,8 @@ def swap_task(): except Exception as e: self._logger.error(str(e)) self.swapFailed.emit(str(e)) + finally: + self.deleteLater() threading.Thread(target=swap_task, daemon=True).start() @@ -388,6 +390,8 @@ def swap_task(): except Exception as e: self._logger.error(str(e)) self.swapFailed.emit(str(e)) + finally: + self.deleteLater() threading.Thread(target=swap_task, daemon=True).start() From 27cd7fe8a886836681ab8077722a9260b5364ee5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 12:31:47 +0200 Subject: [PATCH 0679/1143] qml: deduplicate swap initiation, move to main.qml --- .../gui/qml/components/BalanceDetails.qml | 24 +---------------- electrum/gui/qml/components/Channels.qml | 27 +------------------ electrum/gui/qml/components/main.qml | 26 ++++++++++++++++++ 3 files changed, 28 insertions(+), 49 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index 0a558fdcf..138c79d19 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -171,15 +171,7 @@ Pane { visible: Daemon.currentWallet.isLightning enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 icon.source: Qt.resolvedUrl('../../icons/update.png') - onClicked: { - var swaphelper = app.swaphelper.createObject(app) - swaphelper.swapStarted.connect(function() { - var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) - dialog.open() - }) - var dialog = swapDialog.createObject(rootItem, { swaphelper: swaphelper }) - dialog.open() - } + onClicked: app.startSwap() } FlatButton { @@ -199,20 +191,6 @@ Pane { } - Component { - id: swapDialog - SwapDialog { - onClosed: destroy() - } - } - - Component { - id: swapProgressDialog - SwapProgressDialog { - onClosed: destroy() - } - } - Component { id: openChannelDialog OpenChannelDialog { diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index f1f1370dc..19a9ed915 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -125,15 +125,7 @@ Pane { text: qsTr('Swap'); enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 icon.source: Qt.resolvedUrl('../../icons/update.png') - onClicked: { - var swaphelper = app.swaphelper.createObject(app) - swaphelper.swapStarted.connect(function() { - var dialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) - dialog.open() - }) - var dialog = swapDialog.createObject(root, { swaphelper: swaphelper }) - dialog.open() - } + onClicked: app.startSwap() } FlatButton { @@ -152,23 +144,6 @@ Pane { } - Component { - id: swapDialog - SwapDialog { - onClosed: { - console.log('Destroying SwapDialog ' + this) - destroy() - } - } - } - - Component { - id: swapProgressDialog - SwapProgressDialog { - onClosed: destroy() - } - } - Component { id: openChannelDialog OpenChannelDialog { diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 2a83729a0..f7435574c 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -362,6 +362,21 @@ ApplicationWindow id: _channelOpenProgressDialog } + Component { + id: swapDialog + SwapDialog { + onClosed: destroy() + } + } + + Component { + id: swapProgressDialog + SwapProgressDialog { + onClosed: destroy() + } + } + + NotificationPopup { id: notificationPopup width: parent.width @@ -572,6 +587,17 @@ ApplicationWindow } } + function startSwap() { + var swaphelper = app.swaphelper.createObject(app) + var swapdialog = swapDialog.createObject(app, { swaphelper: swaphelper }) + swaphelper.swapStarted.connect(function() { + swapdialog.close() + var progressdialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + progressdialog.open() + }) + swapdialog.open() + } + property var _lastActive: 0 // record time of last activity property bool _lockDialogShown: false From 778d5f456af66357a21f8516d402836415c286c9 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 13:13:25 +0200 Subject: [PATCH 0680/1143] qml: swap progress gimmick --- .../gui/qml/components/SwapProgressDialog.qml | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/components/SwapProgressDialog.qml b/electrum/gui/qml/components/SwapProgressDialog.qml index 56abbb988..c404d1bf4 100644 --- a/electrum/gui/qml/components/SwapProgressDialog.qml +++ b/electrum/gui/qml/components/SwapProgressDialog.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.15 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.14 import QtQuick.Controls.Material 2.0 @@ -30,13 +30,13 @@ ElDialog { }, State { name: 'success' - PropertyChanges { target: spinner; running: false } + PropertyChanges { target: spinner; visible: false } PropertyChanges { target: helpText; text: qsTr('Success') } PropertyChanges { target: icon; source: '../../icons/confirmed.png' } }, State { name: 'failed' - PropertyChanges { target: spinner; running: false } + PropertyChanges { target: spinner; visible: false } PropertyChanges { target: helpText; text: qsTr('Failed') } PropertyChanges { target: errorText; visible: true } PropertyChanges { target: icon; source: '../../icons/warning.png' } @@ -72,11 +72,31 @@ ElDialog { Layout.preferredWidth: constants.iconSizeXXLarge Layout.preferredHeight: constants.iconSizeXXLarge - BusyIndicator { + Item { id: spinner - visible: s.state == '' - width: constants.iconSizeXXLarge - height: constants.iconSizeXXLarge + property real rot: 0 + RotationAnimation on rot { + duration: 2000 + loops: Animation.Infinite + from: 0 + to: 360 + running: spinner.visible + easing.type: Easing.InOutQuint + } + Image { + x: constants.iconSizeXLarge/2 * Math.cos(spinner.rot*2*Math.PI/360) + y: constants.iconSizeXLarge/2 * Math.sin(spinner.rot*2*Math.PI/360) + width: constants.iconSizeXLarge + height: constants.iconSizeXLarge + source: swaphelper.isReverse ? '../../icons/bitcoin.png' : '../../icons/lightning.png' + } + Image { + x: constants.iconSizeXLarge/2 * Math.cos(Math.PI + spinner.rot*2*Math.PI/360) + y: constants.iconSizeXLarge/2 * Math.sin(Math.PI + spinner.rot*2*Math.PI/360) + width: constants.iconSizeXLarge + height: constants.iconSizeXLarge + source: swaphelper.isReverse ? '../../icons/lightning.png' : '../../icons/bitcoin.png' + } } Image { From 1a263b46becfbcb7c64c9ee467ca30708010b6db Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 14:29:32 +0200 Subject: [PATCH 0681/1143] qml: keep QEAmount instances in qechanneldetails, use millisat amounts for local/remote capacity and can send/receive, refactor channel capacity graphic to ChannelBar and use that as well in ChannelDetails --- .../gui/qml/components/ChannelDetails.qml | 292 ++++++++++-------- .../qml/components/controls/ChannelBar.qml | 48 +++ .../components/controls/ChannelDelegate.qml | 42 +-- electrum/gui/qml/qechanneldetails.py | 32 +- 4 files changed, 241 insertions(+), 173 deletions(-) create mode 100644 electrum/gui/qml/components/controls/ChannelBar.qml diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index b57708bc5..435c68dcd 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -31,177 +31,213 @@ Pane { clip:true interactive: height < contentHeight - GridLayout { + ColumnLayout { id: rootLayout width: parent.width - columns: 2 Heading { - Layout.columnSpan: 2 + // Layout.columnSpan: 2 text: !channeldetails.isBackup ? qsTr('Lightning Channel') : qsTr('Channel Backup') } - Label { - visible: channeldetails.name - text: qsTr('Node name') - color: Material.accentColor - } - - Label { - visible: channeldetails.name - text: channeldetails.name - } + GridLayout { + // id: rootLayout + // width: parent.width + Layout.fillWidth: true + columns: 2 - Label { - text: qsTr('Short channel ID') - color: Material.accentColor - } + Label { + visible: channeldetails.name + text: qsTr('Node name') + color: Material.accentColor + } - Label { - text: channeldetails.short_cid - } + Label { + Layout.fillWidth: true + visible: channeldetails.name + text: channeldetails.name + } - Label { - text: qsTr('State') - color: Material.accentColor - } + Label { + text: qsTr('Short channel ID') + color: Material.accentColor + } - Label { - text: channeldetails.state - } + Label { + text: channeldetails.short_cid + } - Label { - text: qsTr('Initiator') - color: Material.accentColor - } + Label { + text: qsTr('State') + color: Material.accentColor + } - Label { - text: channeldetails.initiator - } + Label { + text: channeldetails.state + color: channeldetails.state == 'OPEN' + ? constants.colorChannelOpen + : Material.foreground + } - Label { - text: qsTr('Capacity') - color: Material.accentColor - } + Label { + text: qsTr('Initiator') + color: Material.accentColor + } - FormattedAmount { - amount: channeldetails.capacity - } + Label { + text: channeldetails.initiator + } - Label { - text: qsTr('Can send') - color: Material.accentColor - } + Label { + text: qsTr('Channel type') + color: Material.accentColor + } - RowLayout { - visible: channeldetails.isOpen - FormattedAmount { - visible: !channeldetails.frozenForSending - amount: channeldetails.canSend - singleLine: false + Label { + text: channeldetails.channelType } + Label { - visible: channeldetails.frozenForSending - text: qsTr('n/a (frozen)') + text: qsTr('Remote node ID') + Layout.columnSpan: 2 + color: Material.accentColor } - Item { + + TextHighlightPane { + Layout.columnSpan: 2 Layout.fillWidth: true - Layout.preferredHeight: 1 - } - Pane { - background: Rectangle { color: Material.dialogColor } - padding: 0 - FlatButton { - Layout.minimumWidth: implicitWidth - text: channeldetails.frozenForSending ? qsTr('Unfreeze') : qsTr('Freeze') - onClicked: channeldetails.freezeForSending() + + RowLayout { + width: parent.width + Label { + text: channeldetails.pubkey + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + Layout.fillWidth: true + wrapMode: Text.Wrap + } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = app.genericShareDialog.createObject(root, + { title: qsTr('Channel node ID'), text: channeldetails.pubkey } + ) + dialog.open() + } + } } } } Label { - visible: !channeldetails.isOpen - text: qsTr('n/a (channel not open)') - } - - Label { - text: qsTr('Can Receive') + text: qsTr('Capacity and ratio') color: Material.accentColor } - RowLayout { - visible: channeldetails.isOpen - FormattedAmount { - visible: !channeldetails.frozenForReceiving - amount: channeldetails.canReceive - singleLine: false - } + TextHighlightPane { + Layout.fillWidth: true + padding: constants.paddingLarge - Label { - visible: channeldetails.frozenForReceiving - text: qsTr('n/a (frozen)') - } - Item { - Layout.fillWidth: true - Layout.preferredHeight: 1 - } - Pane { - background: Rectangle { color: Material.dialogColor } - padding: 0 - FlatButton { - Layout.minimumWidth: implicitWidth - text: channeldetails.frozenForReceiving ? qsTr('Unfreeze') : qsTr('Freeze') - onClicked: channeldetails.freezeForReceiving() + GridLayout { + width: parent.width + columns: 2 + rowSpacing: constants.paddingSmall + + ChannelBar { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingXLarge + capacity: channeldetails.capacity + localCapacity: channeldetails.localCapacity + remoteCapacity: channeldetails.remoteCapacity } - } - } - Label { - visible: !channeldetails.isOpen - text: qsTr('n/a (channel not open)') - } + Label { + text: qsTr('Capacity') + color: Material.accentColor + } - Label { - text: qsTr('Channel type') - color: Material.accentColor - } + FormattedAmount { + amount: channeldetails.capacity + } - Label { - text: channeldetails.channelType - } + Label { + text: qsTr('Can send') + color: Material.accentColor + } - Label { - text: qsTr('Remote node ID') - Layout.columnSpan: 2 - color: Material.accentColor - } + RowLayout { + visible: channeldetails.isOpen + FormattedAmount { + visible: !channeldetails.frozenForSending + amount: channeldetails.canSend + singleLine: false + } + Label { + visible: channeldetails.frozenForSending + text: qsTr('n/a (frozen)') + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 + FlatButton { + Layout.minimumWidth: implicitWidth + text: channeldetails.frozenForSending ? qsTr('Unfreeze') : qsTr('Freeze') + onClicked: channeldetails.freezeForSending() + } + } + } - TextHighlightPane { - Layout.columnSpan: 2 - Layout.fillWidth: true + Label { + visible: !channeldetails.isOpen + text: qsTr('n/a (channel not open)') + } - RowLayout { - width: parent.width Label { - text: channeldetails.pubkey - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - Layout.fillWidth: true - wrapMode: Text.Wrap + text: qsTr('Can Receive') + color: Material.accentColor } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - onClicked: { - var dialog = app.genericShareDialog.createObject(root, - { title: qsTr('Channel node ID'), text: channeldetails.pubkey } - ) - dialog.open() + + RowLayout { + visible: channeldetails.isOpen + FormattedAmount { + visible: !channeldetails.frozenForReceiving + amount: channeldetails.canReceive + singleLine: false + } + + Label { + visible: channeldetails.frozenForReceiving + text: qsTr('n/a (frozen)') + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: 1 } + Pane { + background: Rectangle { color: Material.dialogColor } + padding: 0 + FlatButton { + Layout.minimumWidth: implicitWidth + text: channeldetails.frozenForReceiving ? qsTr('Unfreeze') : qsTr('Freeze') + onClicked: channeldetails.freezeForReceiving() + } + } + } + + Label { + visible: !channeldetails.isOpen + text: qsTr('n/a (channel not open)') } } - } + } } } diff --git a/electrum/gui/qml/components/controls/ChannelBar.qml b/electrum/gui/qml/components/controls/ChannelBar.qml new file mode 100644 index 000000000..34378158a --- /dev/null +++ b/electrum/gui/qml/components/controls/ChannelBar.qml @@ -0,0 +1,48 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.0 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +Item { + property Amount capacity + property Amount localCapacity + property Amount remoteCapacity + + height: 10 + implicitWidth: 100 + + onWidthChanged: { + var cap = capacity.satsInt * 1000 + var twocap = cap * 2 + b1.width = width * (cap - localCapacity.msatsInt) / twocap + b2.width = width * localCapacity.msatsInt / twocap + b3.width = width * remoteCapacity.msatsInt / twocap + b4.width = width * (cap - remoteCapacity.msatsInt) / twocap + } + Rectangle { + id: b1 + x: 0 + height: parent.height + color: 'gray' + } + Rectangle { + id: b2 + anchors.left: b1.right + height: parent.height + color: constants.colorLightningLocal + } + Rectangle { + id: b3 + anchors.left: b2.right + height: parent.height + color: constants.colorLightningRemote + } + Rectangle { + id: b4 + anchors.left: b3.right + height: parent.height + color: 'gray' + } +} diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index bbeb8e797..d02e08b3c 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -105,44 +105,14 @@ ItemDelegate { } } - Item { - id: chviz - visible: !_closed + ChannelBar { Layout.fillWidth: true - height: 10 - onWidthChanged: { - var cap = model.capacity.satsInt * 1000 - var twocap = cap * 2 - b1.width = width * (cap - model.local_capacity.msatsInt) / twocap - b2.width = width * model.local_capacity.msatsInt / twocap - b3.width = width * model.remote_capacity.msatsInt / twocap - b4.width = width * (cap - model.remote_capacity.msatsInt) / twocap - } - Rectangle { - id: b1 - x: 0 - height: parent.height - color: 'gray' - } - Rectangle { - id: b2 - anchors.left: b1.right - height: parent.height - color: constants.colorLightningLocal - } - Rectangle { - id: b3 - anchors.left: b2.right - height: parent.height - color: constants.colorLightningRemote - } - Rectangle { - id: b4 - anchors.left: b3.right - height: parent.height - color: 'gray' - } + visible: !_closed + capacity: model.capacity + localCapacity: model.local_capacity + remoteCapacity: model.remote_capacity } + Item { visible: _closed Layout.fillWidth: true diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index e219e3394..3c7fa0c87 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -33,6 +33,12 @@ def __init__(self, parent=None): self._channelid = None self._channel = None + self._capacity = QEAmount() + self._local_capacity = QEAmount() + self._remote_capacity = QEAmount() + self._can_receive = QEAmount() + self._can_send = QEAmount() + self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) @@ -101,23 +107,31 @@ def initiator(self): @pyqtProperty(QEAmount, notify=channelChanged) def capacity(self): - self._capacity = QEAmount(amount_sat=self._channel.get_capacity()) + self._capacity.copyFrom(QEAmount(amount_sat=self._channel.get_capacity())) return self._capacity + @pyqtProperty(QEAmount, notify=channelChanged) + def localCapacity(self): + if not self._channel.is_backup(): + self._local_capacity = QEAmount(amount_msat=self._channel.balance(LOCAL)) + return self._local_capacity + + @pyqtProperty(QEAmount, notify=channelChanged) + def remoteCapacity(self): + if not self._channel.is_backup(): + self._remote_capacity.copyFrom(QEAmount(amount_msat=self._channel.balance(REMOTE))) + return self._remote_capacity + @pyqtProperty(QEAmount, notify=channelChanged) def canSend(self): - if self._channel.is_backup(): - self._can_send = QEAmount() - else: - self._can_send = QEAmount(amount_sat=self._channel.available_to_spend(LOCAL)/1000) + if not self._channel.is_backup(): + self._can_send.copyFrom(QEAmount(amount_msat=self._channel.available_to_spend(LOCAL))) return self._can_send @pyqtProperty(QEAmount, notify=channelChanged) def canReceive(self): - if self._channel.is_backup(): - self._can_receive = QEAmount() - else: - self._can_receive = QEAmount(amount_sat=self._channel.available_to_spend(REMOTE)/1000) + if not self._channel.is_backup(): + self._can_receive.copyFrom(QEAmount(amount_msat=self._channel.available_to_spend(REMOTE))) return self._can_receive @pyqtProperty(bool, notify=channelChanged) From d6403400bc7fab33f08b829cddbacd60a4d4dd08 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 14:32:02 +0200 Subject: [PATCH 0682/1143] qml: remove leftover commented code --- electrum/gui/qml/components/ChannelDetails.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 435c68dcd..1cef67ffe 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -36,13 +36,10 @@ Pane { width: parent.width Heading { - // Layout.columnSpan: 2 text: !channeldetails.isBackup ? qsTr('Lightning Channel') : qsTr('Channel Backup') } GridLayout { - // id: rootLayout - // width: parent.width Layout.fillWidth: true columns: 2 From d5ce9c0994cc88d1d418ad4154232bcc81e0f386 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 16:09:37 +0200 Subject: [PATCH 0683/1143] qml: destroy qeswaphelper with SwapDialog and catch RuntimeErrors if qeswalhelper members are accessed after --- electrum/gui/qml/components/main.qml | 5 +++- electrum/gui/qml/qeswaphelper.py | 34 +++++++++++++++++----------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index f7435574c..b00c3c88b 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -365,7 +365,10 @@ ApplicationWindow Component { id: swapDialog SwapDialog { - onClosed: destroy() + onClosed: { + swaphelper.destroy() + destroy() + } } } diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index c533e7389..6e45f6498 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -359,12 +359,16 @@ def swap_task(): fut = asyncio.run_coroutine_threadsafe(coro, loop) self.swapStarted.emit() txid = fut.result() - self.swapSuccess.emit() + try: # swaphelper might be destroyed at this point + self.swapSuccess.emit() + except RuntimeError: + pass except Exception as e: - self._logger.error(str(e)) - self.swapFailed.emit(str(e)) - finally: - self.deleteLater() + try: # swaphelper might be destroyed at this point + self._logger.error(str(e)) + self.swapFailed.emit(str(e)) + except RuntimeError: + pass threading.Thread(target=swap_task, daemon=True).start() @@ -383,15 +387,19 @@ def swap_task(): fut = asyncio.run_coroutine_threadsafe(coro, loop) self.swapStarted.emit() success = fut.result() - if success: - self.swapSuccess.emit() - else: - self.swapFailed.emit('') + try: # swaphelper might be destroyed at this point + if success: + self.swapSuccess.emit() + else: + self.swapFailed.emit('') + except RuntimeError: + pass except Exception as e: - self._logger.error(str(e)) - self.swapFailed.emit(str(e)) - finally: - self.deleteLater() + try: # swaphelper might be destroyed at this point + self._logger.error(str(e)) + self.swapFailed.emit(str(e)) + except RuntimeError: + pass threading.Thread(target=swap_task, daemon=True).start() From 7a8e9807127fdb836f47db693e973534b3f7ffdc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 16:19:17 +0200 Subject: [PATCH 0684/1143] qml: since qeswaphelper is tied to SwapDialog anyway, let's make it a direct child --- electrum/gui/qml/components/main.qml | 57 ++++++++++++---------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index b00c3c88b..20484c3e0 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -365,9 +365,29 @@ ApplicationWindow Component { id: swapDialog SwapDialog { - onClosed: { - swaphelper.destroy() - destroy() + onClosed: destroy() + swaphelper: SwapHelper { + id: _swaphelper + wallet: Daemon.currentWallet + onConfirm: { + var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) + dialog.accepted.connect(function() { + _swaphelper.executeSwap(true) + }) + dialog.open() + } + onAuthRequired: { + app.handleAuthRequired(_swaphelper, method) + } + onError: { + var dialog = app.messageDialog.createObject(app, { text: message }) + dialog.open() + } + onSwapStarted: { + // swapdialog.close() + var progressdialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + progressdialog.open() + } } } } @@ -392,29 +412,6 @@ ApplicationWindow } } - property alias swaphelper: _swaphelper - Component { - id: _swaphelper - SwapHelper { - id: __swaphelper - wallet: Daemon.currentWallet - onConfirm: { - var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) - dialog.accepted.connect(function() { - __swaphelper.executeSwap(true) - }) - dialog.open() - } - onAuthRequired: { - app.handleAuthRequired(__swaphelper, method) - } - onError: { - var dialog = app.messageDialog.createObject(app, { text: message }) - dialog.open() - } - } - } - Component.onCompleted: { coverTimer.start() @@ -591,13 +588,7 @@ ApplicationWindow } function startSwap() { - var swaphelper = app.swaphelper.createObject(app) - var swapdialog = swapDialog.createObject(app, { swaphelper: swaphelper }) - swaphelper.swapStarted.connect(function() { - swapdialog.close() - var progressdialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) - progressdialog.open() - }) + var swapdialog = swapDialog.createObject(app) swapdialog.open() } From f43cd7b2785fd8985fa21829074a0b66b51b218f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 12 Apr 2023 16:21:31 +0200 Subject: [PATCH 0685/1143] followup prev --- electrum/gui/qml/components/main.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 20484c3e0..6e03b1440 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -384,8 +384,7 @@ ApplicationWindow dialog.open() } onSwapStarted: { - // swapdialog.close() - var progressdialog = swapProgressDialog.createObject(app, { swaphelper: swaphelper }) + var progressdialog = swapProgressDialog.createObject(app, { swaphelper: _swaphelper }) progressdialog.open() } } From 8e9491e33015c816d7502e7fd1fab5ff466357f3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Apr 2023 10:49:41 +0200 Subject: [PATCH 0686/1143] messageDialog: move yes to the right, no to the left. According to the Google Android guidelines, "The dismissive action of a dialog is always on the left." source: https://uxplanet.org/primary-secondary-action-buttons-c16df9b36150 --- electrum/gui/qml/components/MessageDialog.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 1256df7e8..285270254 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -54,19 +54,19 @@ ElDialog { Layout.fillWidth: true Layout.preferredWidth: 1 textUnderIcon: false - text: qsTr('Yes') - icon.source: Qt.resolvedUrl('../../icons/confirmed.png') + text: qsTr('No') + icon.source: Qt.resolvedUrl('../../icons/closebutton.png') visible: yesno - onClicked: doAccept() + onClicked: doReject() } FlatButton { Layout.fillWidth: true Layout.preferredWidth: 1 textUnderIcon: false - text: qsTr('No') - icon.source: Qt.resolvedUrl('../../icons/closebutton.png') + text: qsTr('Yes') + icon.source: Qt.resolvedUrl('../../icons/confirmed.png') visible: yesno - onClicked: doReject() + onClicked: doAccept() } } } From 8774e5934703ed156e3ea14ad7012acfc4168d84 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 13 Apr 2023 12:03:22 +0200 Subject: [PATCH 0687/1143] exchange rate: if default unit is sat, display value of 1000 sats --- electrum/exchange_rate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index fa3d0de19..2f63b65a9 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -652,8 +652,11 @@ def format_amount_and_units(self, btc_balance, *, timestamp: int = None) -> str: def get_fiat_status_text(self, btc_balance, base_unit, decimal_point): rate = self.exchange_rate() - return _(" (No FX rate available)") if rate.is_nan() else " 1 %s~%s %s" % (base_unit, - self.value_str(COIN / (10**(8 - decimal_point)), rate), self.ccy) + if rate.is_nan(): + return _(" (No FX rate available)") + amount = 1000 if decimal_point == 0 else 1 + value = self.value_str(amount * COIN / (10**(8 - decimal_point)), rate) + return " %d %s~%s %s" % (amount, base_unit, value, self.ccy) def fiat_value(self, satoshis, rate) -> Decimal: return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate) From 488dc4871e3d10f5a59bc0b4a9e727f4df26a34c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Apr 2023 14:42:06 +0000 Subject: [PATCH 0688/1143] wallet: is_up_to_date() to return False if taskgroup stopped If the taskgroup died unexpectedly, this will result in the GUI showing we are in the "synchronizing" state instead of the green orb. Being stuck in "synchronizing" provides at least *some* feedback to the user that something is wrong. see https://github.com/spesmilo/electrum/issues/8301 --- electrum/wallet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/wallet.py b/electrum/wallet.py index 94d9fe8fa..6b217c027 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -373,6 +373,7 @@ async def main_loop(self): except Exception as e: self.logger.exception("taskgroup died.") finally: + util.trigger_callback('wallet_updated', self) self.logger.info("taskgroup stopped.") async def do_synchronize_loop(self): @@ -460,6 +461,8 @@ async def stop(self): self.save_db() def is_up_to_date(self) -> bool: + if self.taskgroup.joined: # either stop() was called, or the taskgroup died + return False return self._up_to_date def tx_is_related(self, tx): From 2c1abf24fa75bc1c4b1d98cf1c0ec01ea22325f4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 13 Apr 2023 23:08:02 +0000 Subject: [PATCH 0689/1143] (trivial) use util.get_asyncio_loop() in some places --- electrum/gui/kivy/uix/dialogs/lightning_channels.py | 8 ++++---- electrum/gui/qml/qeinvoice.py | 6 +++--- electrum/gui/qml/qeswaphelper.py | 6 +++--- electrum/gui/qml/qewallet.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py index 66dfa21cd..aaefac03a 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_channels.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_channels.py @@ -10,7 +10,7 @@ from electrum.lnchannel import AbstractChannel, Channel, ChannelState, ChanCloseOption from electrum.gui.kivy.i18n import _ from electrum.transaction import PartialTxOutput, Transaction -from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, format_fee_satoshis, quantize_feerate +from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, format_fee_satoshis, quantize_feerate, get_asyncio_loop from electrum.lnutil import ln_dummy_address from electrum.gui import messages @@ -434,7 +434,7 @@ def request_force_close(self): def _request_force_close(self, b): if not b: return - loop = self.app.wallet.network.asyncio_loop + loop = get_asyncio_loop() coro = asyncio.run_coroutine_threadsafe(self.app.wallet.lnworker.request_force_close(self.chan.channel_id), loop) try: coro.result(5) @@ -527,7 +527,7 @@ def close(self, close_options): dialog.open() def _close(self, choice): - loop = self.app.wallet.network.asyncio_loop + loop = get_asyncio_loop() if choice == 0: coro = self.app.wallet.lnworker.close_channel(self.chan.channel_id) msg = _('Channel closed') @@ -600,7 +600,7 @@ def _confirm_force_close(self): def _do_force_close(self, b): if not b: return - loop = self.app.wallet.network.asyncio_loop + loop = get_asyncio_loop() coro = asyncio.run_coroutine_threadsafe(self.app.wallet.lnworker.force_close_channel(self.chan.channel_id), loop) try: coro.result(1) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 40ed6e13a..dc0225c7e 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -15,7 +15,7 @@ from electrum.logging import get_logger from electrum.transaction import PartialTxOutput from electrum.util import (parse_URI, InvalidBitcoinURI, InvoiceError, - maybe_extract_lightning_payment_identifier) + maybe_extract_lightning_payment_identifier, get_asyncio_loop) from electrum.lnutil import format_short_channel_id from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl from electrum.bitcoin import COIN @@ -582,7 +582,7 @@ def resolve_lnurl(self, lnurl): def resolve_task(): try: coro = request_lnurl(url) - fut = asyncio.run_coroutine_threadsafe(coro, self._wallet.wallet.network.asyncio_loop) + fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) self.on_lnurl(fut.result()) except Exception as e: self.validationError.emit('lnurl', repr(e)) @@ -629,7 +629,7 @@ def fetch_invoice_task(): if comment: params['comment'] = comment coro = callback_lnurl(self._lnurlData['callback_url'], params) - fut = asyncio.run_coroutine_threadsafe(coro, self._wallet.wallet.network.asyncio_loop) + fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) self.on_lnurl_invoice(amount, fut.result()) except Exception as e: self._logger.error(repr(e)) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 6e45f6498..ff6458396 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -9,7 +9,7 @@ from electrum.lnutil import ln_dummy_address from electrum.logging import get_logger from electrum.transaction import PartialTxOutput -from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler +from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop from .auth import AuthMixin, auth_protect from .qetypes import QEAmount @@ -346,7 +346,7 @@ def do_normal_swap(self, lightning_amount, onchain_amount): assert self._tx if lightning_amount is None or onchain_amount is None: return - loop = self._wallet.wallet.network.asyncio_loop + loop = get_asyncio_loop() coro = self._wallet.wallet.lnworker.swap_manager.normal_swap( lightning_amount_sat=lightning_amount, expected_onchain_amount_sat=onchain_amount, @@ -376,7 +376,7 @@ def do_reverse_swap(self, lightning_amount, onchain_amount): if lightning_amount is None or onchain_amount is None: return swap_manager = self._wallet.wallet.lnworker.swap_manager - loop = self._wallet.wallet.network.asyncio_loop + loop = get_asyncio_loop() coro = swap_manager.reverse_swap( lightning_amount_sat=lightning_amount, expected_onchain_amount_sat=onchain_amount + swap_manager.get_claim_fee(), diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 0a365f665..db0d7874c 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -13,7 +13,7 @@ from electrum.logging import get_logger from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.transaction import PartialTxOutput, PartialTransaction -from electrum.util import parse_max_spend, InvalidPassword, event_listener, AddTransactionException +from electrum.util import parse_max_spend, InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop from electrum.plugin import run_hook from electrum.wallet import Multisig_Wallet from electrum.crypto import pw_decode_with_version_and_mac @@ -604,7 +604,7 @@ def pay_lightning_invoice(self, invoice: 'QEInvoice'): def pay_thread(): try: coro = self.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat) - fut = asyncio.run_coroutine_threadsafe(coro, self.wallet.network.asyncio_loop) + fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) fut.result() except Exception as e: self.paymentFailed.emit(invoice.get_id(), repr(e)) From 22745365ad4eb84bda9886c7a8cabe019f231d8a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Apr 2023 10:27:41 +0200 Subject: [PATCH 0690/1143] qeswaphelper: factorize code --- electrum/gui/qml/qeswaphelper.py | 45 +++++++++----------------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index ff6458396..caf9947ec 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -293,39 +293,20 @@ def swap_slider_moved(self): # pay_amount and receive_amounts are always with fees already included # so they reflect the net balance change after the swap - if position < 0: # reverse swap - self.isReverse = True - - self._send_amount = abs(position) - self.tosend = QEAmount(amount_sat=self._send_amount) - - self._receive_amount = swap_manager.get_recv_amount( - send_amount=self._send_amount, is_reverse=True) - self.toreceive = QEAmount(amount_sat=self._receive_amount) - - # fee breakdown - self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' - server_miningfee = swap_manager.lockup_fee - self.server_miningfee = QEAmount(amount_sat=server_miningfee) - self.miningfee = QEAmount(amount_sat=swap_manager.get_claim_fee()) - + self.isReverse = (position < 0) + self._send_amount = abs(position) + self.tosend = QEAmount(amount_sat=self._send_amount) + self._receive_amount = swap_manager.get_recv_amount(send_amount=self._send_amount, is_reverse=self.isReverse) + self.toreceive = QEAmount(amount_sat=self._receive_amount) + # fee breakdown + self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' + server_miningfee = swap_manager.lockup_fee if self.isReverse else swap_manager.normal_fee + self.server_miningfee = QEAmount(amount_sat=server_miningfee) + if self.isReverse: self.check_valid(self._send_amount, self._receive_amount) - else: # forward (normal) swap - self.isReverse = False - self._send_amount = position - self.tosend = QEAmount(amount_sat=self._send_amount) - - self._receive_amount = swap_manager.get_recv_amount(send_amount=position, is_reverse=False) - self.toreceive = QEAmount(amount_sat=self._receive_amount) - - # fee breakdown - self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' - server_miningfee = swap_manager.normal_fee - self.server_miningfee = QEAmount(amount_sat=server_miningfee) - - # the slow stuff we delegate to a delay timer which triggers after slider - # doesn't update for a while - self.valid = False # wait for timer + else: + # update tx only if slider isn't moved for a while + self.valid = False self._fwd_swap_updatetx_timer.start(250) def check_valid(self, send_amount, receive_amount): From 5d4e6b1cd70ce76358570c32fa55b7ece267c535 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 10:52:46 +0200 Subject: [PATCH 0691/1143] qml: fix setting empty password when switching to already open wallet with password --- electrum/gui/qml/qedaemon.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index db36a6ec6..99ace56cb 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -196,16 +196,22 @@ def load_wallet_task(): self.loadingChanged.emit() try: - wallet = self.daemon.load_wallet(self._path, password) + local_password = password # need this in local scope + wallet = self.daemon.load_wallet(self._path, local_password) if wallet is None: self._logger.info('could not open wallet') self.walletOpenError.emit('could not open wallet') return + if wallet_already_open: + # wallet already open. daemon.load_wallet doesn't mind, but + # we need the correct current wallet password below + local_password = QEWallet.getInstanceFor(wallet).password + if self.daemon.config.get('single_password'): - self._use_single_password = self.daemon.update_password_for_directory(old_password=password, new_password=password) - self._password = password + self._use_single_password = self.daemon.update_password_for_directory(old_password=local_password, new_password=local_password) + self._password = local_password self.singlePasswordChanged.emit() self._logger.info(f'use single password: {self._use_single_password}') else: @@ -215,7 +221,7 @@ def load_wallet_task(): run_hook('load_wallet', wallet) - self._backendWalletLoaded.emit(password) + self._backendWalletLoaded.emit(local_password) except WalletFileException as e: self._logger.error(str(e)) self.walletOpenError.emit(str(e)) From 21d1a6239fbdf97d6d84d71da63234dfea032b80 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 12:21:38 +0200 Subject: [PATCH 0692/1143] qml: always pass wallet password to init_lightning. emit also dataChanged so UI updates node pubkey --- electrum/gui/qml/components/main.qml | 1 - electrum/gui/qml/qedaemon.py | 1 - electrum/gui/qml/qewallet.py | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 6e03b1440..bb20cd909 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -218,7 +218,6 @@ ApplicationWindow StackView { id: mainStackView - // anchors.fill: parent width: parent.width height: inputPanel.y - header.height initialItem: Qt.resolvedUrl('WalletMainView.qml') diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 99ace56cb..0dbfff7c8 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -83,7 +83,6 @@ def remove_wallet(self, path): if wallet_path == path: remove = i else: - self._logger.debug('HM, %s is not %s', wallet_path, path) wallets.append((wallet_name, wallet_path)) i += 1 diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index db0d7874c..0b6366620 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -473,8 +473,9 @@ def lightningNumPeers(self): @pyqtSlot() def enableLightning(self): - self.wallet.init_lightning(password=None) # TODO pass password if needed + self.wallet.init_lightning(password=self.password) self.isLightningChanged.emit() + self.dataChanged.emit() @pyqtSlot(str, int, int, bool) def send_onchain(self, address, amount, fee=None, rbf=False): From 3d75cf42235f17b2f1e9dceab26266935b1b6ab5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 12:44:34 +0200 Subject: [PATCH 0693/1143] qml: skip confirm messagedialog if pin is enabled --- electrum/gui/qml/qeswaphelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index caf9947ec..7418e4550 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -390,7 +390,7 @@ def executeSwap(self, confirm=False): if not self._wallet.wallet.network: self.error.emit(_("You are offline.")) return - if confirm: + if confirm or self._wallet.wallet.config.get('pin_code', ''): self._do_execute_swap() return From f562ad38cf5e1d32a4618a717fe368506ebc0060 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Apr 2023 14:18:49 +0200 Subject: [PATCH 0694/1143] qml: add confirm dialog before disabling recoverable channels (similar to trampoline) --- electrum/gui/qml/components/Preferences.qml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index b4d07c127..569f75add 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -287,8 +287,23 @@ Pane { Switch { id: useRecoverableChannels onCheckedChanged: { - if (activeFocus) - Config.useRecoverableChannels = checked + if (activeFocus) { + if (!checked) { + var dialog = app.messageDialog.createObject(app, { + text: qsTr('Are you sure? This option allows you to recover your lightning funds if you lose your device, or if you uninstall this app while lightning channels are active. Do not disable it unless you know how to recover channels from backups.'), + yesno: true + }) + dialog.accepted.connect(function() { + Config.useRecoverableChannels = False + }) + dialog.rejected.connect(function() { + checked = true // revert + }) + dialog.open() + } else { + Config.useRecoverableChannels = checked + } + } } } Label { From 79d57110031decd825f37f2c2b2a97c756250e37 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 13:34:05 +0200 Subject: [PATCH 0695/1143] qml: InfoTextArea add Spinner to styles --- .../qml/components/controls/InfoTextArea.qml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index b8867eb77..dcdab8c4c 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -11,7 +11,8 @@ TextHighlightPane { Error, Progress, Pending, - Done + Done, + Spinner } property alias text: infotext.text @@ -24,7 +25,7 @@ TextHighlightPane { ? constants.colorWarning : iconStyle == InfoTextArea.IconStyle.Error ? constants.colorError - : iconStyle == InfoTextArea.IconStyle.Progress + : iconStyle == InfoTextArea.IconStyle.Progress || iconStyle == InfoTextArea.IconStyle.Spinner ? constants.colorProgress : iconStyle == InfoTextArea.IconStyle.Done ? constants.colorDone @@ -36,6 +37,9 @@ TextHighlightPane { spacing: constants.paddingLarge Image { + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium + visible: iconStyle != InfoTextArea.IconStyle.Spinner source: iconStyle == InfoTextArea.IconStyle.Info ? "../../../icons/info.png" : iconStyle == InfoTextArea.IconStyle.Warn @@ -49,8 +53,19 @@ TextHighlightPane { : iconStyle == InfoTextArea.IconStyle.Done ? "../../../icons/confirmed.png" : "" + } + + Item { Layout.preferredWidth: constants.iconSizeMedium Layout.preferredHeight: constants.iconSizeMedium + visible: iconStyle == InfoTextArea.IconStyle.Spinner + + BusyIndicator { + anchors.centerIn: parent + scale: 0.66 + smooth: true + running: visible + } } Label { From f77ff2723c85208c1db0b6998b13e6cae38d4e04 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 13:45:21 +0200 Subject: [PATCH 0696/1143] qml: update userinfo --- electrum/gui/qml/qeswaphelper.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 7418e4550..1363eb725 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -338,9 +338,11 @@ def do_normal_swap(self, lightning_amount, onchain_amount): def swap_task(): try: fut = asyncio.run_coroutine_threadsafe(coro, loop) + self.userinfo = _('Performing swap...') self.swapStarted.emit() txid = fut.result() try: # swaphelper might be destroyed at this point + self.userinfo = _('Swap successful!') self.swapSuccess.emit() except RuntimeError: pass @@ -366,17 +368,21 @@ def do_reverse_swap(self, lightning_amount, onchain_amount): def swap_task(): try: fut = asyncio.run_coroutine_threadsafe(coro, loop) + self.userinfo = _('Performing swap...') self.swapStarted.emit() success = fut.result() try: # swaphelper might be destroyed at this point if success: + self.userinfo = _('Swap successful!') self.swapSuccess.emit() else: + self.userinfo = _('Swap failed!') self.swapFailed.emit('') except RuntimeError: pass except Exception as e: try: # swaphelper might be destroyed at this point + self.userinfo = _('Swap failed!') self._logger.error(str(e)) self.swapFailed.emit(str(e)) except RuntimeError: From 3cab3b86b4581f8caa6c70328e85c21eff440940 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 14:32:23 +0200 Subject: [PATCH 0697/1143] qml: remove SwapProgressDialog, introduce qeswaphelper.state and enable dialog elements depending on qeswaphelper.state TODO: we can now retrieve the pairs from the service asynchronously, which should eliminate the startup delay when showing the SwapDialog --- electrum/gui/qml/components/SwapDialog.qml | 32 ++-- .../gui/qml/components/SwapProgressDialog.qml | 140 ------------------ electrum/gui/qml/components/main.qml | 12 -- electrum/gui/qml/qeswaphelper.py | 34 ++++- 4 files changed, 52 insertions(+), 166 deletions(-) delete mode 100644 electrum/gui/qml/components/SwapProgressDialog.qml diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index ba24db9cc..de0aaeb67 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -26,12 +26,20 @@ ElDialog { spacing: constants.paddingLarge InfoTextArea { + id: userinfoText Layout.leftMargin: constants.paddingXXLarge Layout.rightMargin: constants.paddingXXLarge Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter visible: swaphelper.userinfo != '' text: swaphelper.userinfo + iconStyle: swaphelper.state == SwapHelper.Started + ? InfoTextArea.IconStyle.Spinner + : swaphelper.state == SwapHelper.Failed + ? InfoTextArea.IconStyle.Error + : swaphelper.state == SwapHelper.Success + ? InfoTextArea.IconStyle.Done + : InfoTextArea.IconStyle.Info } GridLayout { @@ -155,12 +163,16 @@ ElDialog { Slider { id: swapslider + Layout.fillWidth: true + Layout.topMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingLarge Layout.leftMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid Layout.rightMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid - Layout.fillWidth: true + property real scenter: -swapslider.from/(swapslider.to-swapslider.from) + + enabled: swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed background: Rectangle { x: swapslider.leftPadding @@ -170,7 +182,9 @@ ElDialog { width: swapslider.availableWidth height: implicitHeight radius: 2 - color: Material.accentColor + color: enabled + ? Material.accentColor + : Material.sliderDisabledColor // full width somehow misaligns with handle, define rangeWidth property int rangeWidth: width - swapslider.leftPadding @@ -183,7 +197,9 @@ ElDialog { ? (swapslider.visualPosition-swapslider.scenter) * parent.rangeWidth : (swapslider.scenter-swapslider.visualPosition) * parent.rangeWidth height: parent.height - color: Material.accentColor + color: enabled + ? Material.accentColor + : Material.sliderDisabledColor radius: 2 } @@ -205,8 +221,6 @@ ElDialog { } } - property real scenter: -swapslider.from/(swapslider.to-swapslider.from) - from: swaphelper.rangeMin to: swaphelper.rangeMax @@ -242,9 +256,9 @@ ElDialog { Layout.fillWidth: true text: qsTr('Ok') icon.source: Qt.resolvedUrl('../../icons/confirmed.png') - enabled: swaphelper.valid + enabled: swaphelper.valid && (swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed) + onClicked: { - console.log('Swap triggered from dialog ' + this + ' using swaphelper ' + swaphelper) swaphelper.executeSwap() } } @@ -255,13 +269,9 @@ ElDialog { function onSliderPosChanged() { swapslider.value = swaphelper.sliderPos } - function onSwapSuccess() { - root.close() - } } Component.onCompleted: { - console.log('Created SwapDialog ' + this) swapslider.value = swaphelper.sliderPos } diff --git a/electrum/gui/qml/components/SwapProgressDialog.qml b/electrum/gui/qml/components/SwapProgressDialog.qml deleted file mode 100644 index c404d1bf4..000000000 --- a/electrum/gui/qml/components/SwapProgressDialog.qml +++ /dev/null @@ -1,140 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Layouts 1.0 -import QtQuick.Controls 2.14 -import QtQuick.Controls.Material 2.0 - -import org.electrum 1.0 - -import "controls" - -ElDialog { - id: dialog - - required property QtObject swaphelper - - width: parent.width - height: parent.height - resizeWithKeyboard: false - - iconSource: Qt.resolvedUrl('../../icons/update.png') - title: swaphelper.isReverse - ? qsTr('Reverse swap...') - : qsTr('Swap...') - - Item { - id: s - state: '' - states: [ - State { - name: '' - }, - State { - name: 'success' - PropertyChanges { target: spinner; visible: false } - PropertyChanges { target: helpText; text: qsTr('Success') } - PropertyChanges { target: icon; source: '../../icons/confirmed.png' } - }, - State { - name: 'failed' - PropertyChanges { target: spinner; visible: false } - PropertyChanges { target: helpText; text: qsTr('Failed') } - PropertyChanges { target: errorText; visible: true } - PropertyChanges { target: icon; source: '../../icons/warning.png' } - } - ] - transitions: [ - Transition { - from: '' - to: 'success' - PropertyAnimation { target: helpText; properties: 'text'; duration: 0} - NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 200 } - NumberAnimation { target: icon; properties: 'scale'; from: 0; to: 1; duration: 500 - easing.type: Easing.OutBack - easing.overshoot: 10 - } - }, - Transition { - from: '' - to: 'failed' - PropertyAnimation { target: helpText; properties: 'text'; duration: 0} - NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 500 } - } - ] - } - - ColumnLayout { - id: content - anchors.centerIn: parent - width: parent.width - - Item { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: constants.iconSizeXXLarge - Layout.preferredHeight: constants.iconSizeXXLarge - - Item { - id: spinner - property real rot: 0 - RotationAnimation on rot { - duration: 2000 - loops: Animation.Infinite - from: 0 - to: 360 - running: spinner.visible - easing.type: Easing.InOutQuint - } - Image { - x: constants.iconSizeXLarge/2 * Math.cos(spinner.rot*2*Math.PI/360) - y: constants.iconSizeXLarge/2 * Math.sin(spinner.rot*2*Math.PI/360) - width: constants.iconSizeXLarge - height: constants.iconSizeXLarge - source: swaphelper.isReverse ? '../../icons/bitcoin.png' : '../../icons/lightning.png' - } - Image { - x: constants.iconSizeXLarge/2 * Math.cos(Math.PI + spinner.rot*2*Math.PI/360) - y: constants.iconSizeXLarge/2 * Math.sin(Math.PI + spinner.rot*2*Math.PI/360) - width: constants.iconSizeXLarge - height: constants.iconSizeXLarge - source: swaphelper.isReverse ? '../../icons/lightning.png' : '../../icons/bitcoin.png' - } - } - - Image { - id: icon - width: constants.iconSizeXXLarge - height: constants.iconSizeXXLarge - } - } - - Label { - id: helpText - Layout.alignment: Qt.AlignHCenter - text: qsTr('Performing swap...') - font.pixelSize: constants.fontSizeXXLarge - } - - Label { - id: errorText - Layout.preferredWidth: parent.width - Layout.alignment: Qt.AlignHCenter - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - } - } - - Connections { - target: swaphelper - function onSwapSuccess() { - console.log('swap succeeded!') - s.state = 'success' - } - function onSwapFailed(message) { - console.log('swap failed: ' + message) - s.state = 'failed' - if (message) - errorText.text = message - } - } - -} diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index bb20cd909..3a86199fb 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -382,22 +382,10 @@ ApplicationWindow var dialog = app.messageDialog.createObject(app, { text: message }) dialog.open() } - onSwapStarted: { - var progressdialog = swapProgressDialog.createObject(app, { swaphelper: _swaphelper }) - progressdialog.open() - } } } } - Component { - id: swapProgressDialog - SwapProgressDialog { - onClosed: destroy() - } - } - - NotificationPopup { id: notificationPopup width: parent.width diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 1363eb725..e6ab36721 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -3,7 +3,7 @@ import math from typing import Union -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS from electrum.i18n import _ from electrum.lnutil import ln_dummy_address @@ -19,6 +19,15 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): _logger = get_logger(__name__) + class State: + Initialized = 0 + ServiceReady = 1 + Started = 2 + Failed = 3 + Success = 4 + + Q_ENUMS(State) + confirm = pyqtSignal([str], arguments=['message']) error = pyqtSignal([str], arguments=['message']) swapStarted = pyqtSignal() @@ -34,6 +43,7 @@ def __init__(self, parent=None): self._rangeMax = 0 self._tx = None self._valid = False + self._state = QESwapHelper.State.Initialized self._userinfo = ' '.join([ _('Move the slider to set the amount and direction of the swap.'), _('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'), @@ -130,6 +140,17 @@ def valid(self, valid): self._valid = valid self.validChanged.emit() + stateChanged = pyqtSignal() + @pyqtProperty(int, notify=stateChanged) + def state(self): + return self._state + + @state.setter + def state(self, state): + if self._state != state: + self._state = state + self.stateChanged.emit() + userinfoChanged = pyqtSignal() @pyqtProperty(str, notify=userinfoChanged) def userinfo(self): @@ -215,7 +236,7 @@ def init_swap_slider_range(self): swap_manager = lnworker.swap_manager try: asyncio.run(swap_manager.get_pairs()) - self._service_available = True + self.state = QESwapHelper.State.ServiceReady except Exception as e: self.error.emit(_('Swap service unavailable')) self._logger.error(f'could not get pairs for swap: {repr(e)}') @@ -284,7 +305,7 @@ def on_event_fee(self, *args): self.swap_slider_moved() def swap_slider_moved(self): - if not self._service_available: + if self._state == QESwapHelper.State.Initialized: return position = int(self._sliderPos) @@ -339,15 +360,18 @@ def swap_task(): try: fut = asyncio.run_coroutine_threadsafe(coro, loop) self.userinfo = _('Performing swap...') + self.state = QESwapHelper.State.Started self.swapStarted.emit() txid = fut.result() try: # swaphelper might be destroyed at this point self.userinfo = _('Swap successful!') + self.state = QESwapHelper.State.Success self.swapSuccess.emit() except RuntimeError: pass except Exception as e: try: # swaphelper might be destroyed at this point + self.state = QESwapHelper.State.Failed self._logger.error(str(e)) self.swapFailed.emit(str(e)) except RuntimeError: @@ -369,20 +393,24 @@ def swap_task(): try: fut = asyncio.run_coroutine_threadsafe(coro, loop) self.userinfo = _('Performing swap...') + self.state = QESwapHelper.State.Started self.swapStarted.emit() success = fut.result() try: # swaphelper might be destroyed at this point if success: self.userinfo = _('Swap successful!') + self.state = QESwapHelper.State.Success self.swapSuccess.emit() else: self.userinfo = _('Swap failed!') + self.state = QESwapHelper.State.Failed self.swapFailed.emit('') except RuntimeError: pass except Exception as e: try: # swaphelper might be destroyed at this point self.userinfo = _('Swap failed!') + self.state = QESwapHelper.State.Failed self._logger.error(str(e)) self.swapFailed.emit(str(e)) except RuntimeError: From e059a3c04bb9f1035fe251bc04f29662b5682879 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 15:02:38 +0200 Subject: [PATCH 0698/1143] qml: apply long-press implementation also on ReceiveRequests --- electrum/gui/qml/components/Invoices.qml | 1 + .../gui/qml/components/ReceiveRequests.qml | 114 +++++++++--------- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 8e243d154..9d7bc6086 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -44,6 +44,7 @@ Pane { anchors.fill: parent clip: true currentIndex: -1 + model: DelegateModel { id: delegateModel model: Daemon.currentWallet.invoiceModel diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index 456544728..9c5e0f3d1 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -12,70 +12,78 @@ import "controls" Pane { id: root objectName: 'ReceiveRequests' - property string selected_key + + padding: 0 ColumnLayout { anchors.fill: parent + spacing: 0 - InfoTextArea { + ColumnLayout { Layout.fillWidth: true - Layout.bottomMargin: constants.paddingLarge - visible: !Config.userKnowsPressAndHold - text: qsTr('To access this list from the main screen, press and hold the Receive button') - } + Layout.margins: constants.paddingLarge - Heading { - text: qsTr('Pending requests') - } + InfoTextArea { + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: !Config.userKnowsPressAndHold + text: qsTr('To access this list from the main screen, press and hold the Receive button') + } - Frame { - background: PaneInsetBackground {} + Heading { + text: qsTr('Pending requests') + } - verticalPadding: 0 - horizontalPadding: 0 - Layout.fillHeight: true - Layout.fillWidth: true + Frame { + background: PaneInsetBackground {} + + verticalPadding: 0 + horizontalPadding: 0 + Layout.fillHeight: true + Layout.fillWidth: true - ListView { - id: listview - anchors.fill: parent - clip: true - - model: DelegateModel { - id: delegateModel - model: Daemon.currentWallet.requestModel - delegate: InvoiceDelegate { - onClicked: { - app.stack.getRoot().openRequest(model.key) - selected_key = '' + ListView { + id: listview + anchors.fill: parent + clip: true + currentIndex: -1 + + model: DelegateModel { + id: delegateModel + model: Daemon.currentWallet.requestModel + delegate: InvoiceDelegate { + onClicked: { + app.stack.getRoot().openRequest(model.key) + listview.currentIndex = -1 + } + onPressAndHold: listview.currentIndex = index } - onPressAndHold: { - selected_key = model.key - } } - } - add: Transition { - NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 } - NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 } - } - addDisplaced: Transition { - SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 } - } - remove: Transition { - NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } - NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } - } - removeDisplaced: Transition { - SequentialAnimation { - PauseAnimation { duration: 200 } - SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } + add: Transition { + NumberAnimation { properties: 'scale'; from: 0.75; to: 1; duration: 500 } + NumberAnimation { properties: 'opacity'; from: 0; to: 1; duration: 500 } + } + addDisplaced: Transition { + SpringAnimation { properties: 'y'; duration: 200; spring: 5; damping: 0.5; mass: 2 } } - } - ScrollIndicator.vertical: ScrollIndicator { } + remove: Transition { + NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } + NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { duration: 200 } + SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } + } + } + + ScrollIndicator.vertical: ScrollIndicator { } + } } } + ButtonContainer { Layout.fillWidth: true FlatButton { @@ -83,10 +91,9 @@ Pane { Layout.preferredWidth: 1 text: qsTr('Delete') icon.source: '../../icons/delete.png' - visible: selected_key != '' + visible: listview.currentIndex >= 0 onClicked: { - Daemon.currentWallet.delete_request(selected_key) - selected_key = '' + Daemon.currentWallet.delete_request(listview.currentItem.getKey()) } } FlatButton { @@ -94,10 +101,9 @@ Pane { Layout.preferredWidth: 1 text: qsTr('View') icon.source: '../../icons/tab_receive.png' - visible: selected_key != '' + visible: listview.currentIndex >= 0 onClicked: { - app.stack.getRoot().openRequest(selected_key) - selected_key = '' + app.stack.getRoot().openRequest(listview.currentItem.getKey()) } } } From 08c478f8d2927599ecac74e68dce7090b8723b96 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Apr 2023 15:55:03 +0200 Subject: [PATCH 0699/1143] network: use IntEnum for connection states. Export user-visible strings in get_connection_status_for_GUI --- .../gui/qml/components/BalanceDetails.qml | 2 +- .../components/controls/BalanceSummary.qml | 8 +++--- .../OnchainNetworkStatusIndicator.qml | 2 +- electrum/gui/qml/qenetwork.py | 15 ++++++++--- electrum/network.py | 25 +++++++++++++++---- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index 138c79d19..e2abc0a9d 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -38,7 +38,7 @@ Pane { InfoTextArea { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: Daemon.currentWallet.synchronizing || Network.server_status != 'connected' + visible: Daemon.currentWallet.synchronizing || !Network.is_connected text: Daemon.currentWallet.synchronizing ? qsTr('Your wallet is not synchronized. The displayed balance may be inaccurate.') : qsTr('Your wallet is not connected to an Electrum server. The displayed balance may be outdated.') diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 0559b9a5d..e02ef92fe 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -31,7 +31,7 @@ Item { GridLayout { id: balanceLayout columns: 3 - opacity: Daemon.currentWallet.synchronizing || Network.server_status != 'connected' ? 0 : 1 + opacity: Daemon.currentWallet.synchronizing || !Network.is_connected ? 0 : 1 Label { font.pixelSize: constants.fontSizeXLarge @@ -129,7 +129,7 @@ Item { } Label { - opacity: Daemon.currentWallet.synchronizing && Network.server_status == 'connected' ? 1 : 0 + opacity: Daemon.currentWallet.synchronizing && Network.is_connected ? 1 : 0 anchors.centerIn: balancePane text: Daemon.currentWallet.synchronizingProgress color: Material.accentColor @@ -137,9 +137,9 @@ Item { } Label { - opacity: Network.server_status != 'connected' ? 1 : 0 + opacity: !Network.is_connected ? 1 : 0 anchors.centerIn: balancePane - text: qsTr('Disconnected') + text: Network.server_status color: Material.accentColor font.pixelSize: constants.fontSizeLarge } diff --git a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml index 690639e63..54242040b 100644 --- a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml +++ b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml @@ -6,7 +6,7 @@ Image { sourceSize.width: constants.iconSizeMedium sourceSize.height: constants.iconSizeMedium - property bool connected: Network.server_status == 'connected' + property bool connected: Network.is_connected property bool lagging: connected && Network.isLagging property bool fork: connected && Network.chaintips > 1 property bool syncing: connected && Daemon.currentWallet && Daemon.currentWallet.synchronizing diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index eeef2d096..c72a91bff 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -35,6 +35,7 @@ class QENetwork(QObject, QtEventListener): _height = 0 _server = "" + _is_connected = False _server_status = "" _network_status = "" _chaintips = 1 @@ -95,7 +96,11 @@ def _update_status(self): self._logger.debug('network_status updated: %s' % network_status) self._network_status = network_status self.statusChanged.emit() - server_status = self.network.connection_status + is_connected = self.network.is_connected() + if self._is_connected != is_connected: + self._is_connected = is_connected + self.statusChanged.emit() + server_status = self.network.get_connection_status_for_GUI() if self._server_status != server_status: self._logger.debug('server_status updated: %s' % server_status) self._server_status = server_status @@ -209,7 +214,7 @@ def server(self, server: str): @pyqtProperty(str, notify=statusChanged) def serverWithStatus(self): server = self._server - if self._server_status != "connected": # connecting or disconnected + if not self.network.is_connected(): # connecting or disconnected return f"{server} (connecting...)" return server @@ -219,7 +224,11 @@ def status(self): @pyqtProperty(str, notify=statusChanged) def server_status(self): - return self._server_status + return self.network.get_connection_status_for_GUI() + + @pyqtProperty(bool, notify=statusChanged) + def is_connected(self): + return self._is_connected @pyqtProperty(int, notify=chaintipsChanged) def chaintips(self): diff --git a/electrum/network.py b/electrum/network.py index 52fc7948d..b288a5578 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -37,6 +37,7 @@ from concurrent import futures import copy import functools +from enum import IntEnum import aiorpcx from aiorpcx import ignore_after @@ -82,6 +83,12 @@ T = TypeVar('T') +class ConnectionState(IntEnum): + DISCONNECTED = 0 + CONNECTING = 1 + CONNECTED = 2 + + def parse_servers(result: Sequence[Tuple[str, str, List[str]]]) -> Dict[str, dict]: """Convert servers list (from protocol method "server.peers.subscribe") into dict format. Also validate values, such as IP addresses and ports. @@ -330,7 +337,7 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): # Dump network messages (all interfaces). Set at runtime from the console. self.debug = False - self._set_status('disconnected') + self._set_status(ConnectionState.DISCONNECTED) self._has_ever_managed_to_connect_to_server = False self._was_started = False @@ -432,7 +439,15 @@ def is_connected(self): return interface is not None and interface.is_connected_and_ready() def is_connecting(self): - return self.connection_status == 'connecting' + return self.connection_status == ConnectionState.CONNECTING + + def get_connection_status_for_GUI(self): + ConnectionStates = { + ConnectionState.DISCONNECTED: _('Disconnected'), + ConnectionState.CONNECTING: _('Connecting'), + ConnectionState.CONNECTED: _('Connected'), + } + return ConnectionStates[self.connection_status] async def _request_server_info(self, interface: 'Interface'): await interface.ready @@ -731,7 +746,7 @@ async def switch_to_interface(self, server: ServerAddr): util.trigger_callback('default_server_changed') self.default_server_changed_event.set() self.default_server_changed_event.clear() - self._set_status('connected') + self._set_status(ConnectionState.CONNECTED) util.trigger_callback('network_updated') if blockchain_updated: util.trigger_callback('blockchain_updated') @@ -769,7 +784,7 @@ async def connection_down(self, interface: Interface): We distinguish by whether it is in self.interfaces.''' if not interface: return if interface.server == self.default_server: - self._set_status('disconnected') + self._set_status(ConnectionState.DISCONNECTED) await self._close_interface(interface) util.trigger_callback('network_updated') @@ -792,7 +807,7 @@ async def _run_new_interface(self, server: ServerAddr): self._connecting_ifaces.add(server) if server == self.default_server: self.logger.info(f"connecting to {server} as new interface") - self._set_status('connecting') + self._set_status(ConnectionState.CONNECTING) self._trying_addr_now(server) interface = Interface(network=self, server=server, proxy=self.proxy) From 98c4c86a002952c0fe610047feb03aca1e19e823 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 14 Apr 2023 16:27:38 +0200 Subject: [PATCH 0700/1143] qeswaphelper: enrich user info --- electrum/gui/qml/qeswaphelper.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index e6ab36721..711b8b02e 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -364,7 +364,12 @@ def swap_task(): self.swapStarted.emit() txid = fut.result() try: # swaphelper might be destroyed at this point - self.userinfo = _('Swap successful!') + self.userinfo = ' '.join([ + _('Success!'), + _('Your funding transaction has been broadcast.'), + _('The swap will be finalized once your transaction is confirmed.'), + _('You will need to be online to finalize the swap, or the transaction will be refunded to you after some delay.'), + ]) self.state = QESwapHelper.State.Success self.swapSuccess.emit() except RuntimeError: @@ -398,7 +403,12 @@ def swap_task(): success = fut.result() try: # swaphelper might be destroyed at this point if success: - self.userinfo = _('Swap successful!') + self.userinfo = ' '.join([ + _('Success!'), + _('The funding transaction has been detected.'), + _('Your claiming transaction will be broadcast when the funding transaction is confirmed.'), + _('You may broadcast it before that manually, but this is not trustless.'), + ]) self.state = QESwapHelper.State.Success self.swapSuccess.emit() else: From 89003bba4a369323becf46cb20a511c96dff8600 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Apr 2023 20:34:46 +0200 Subject: [PATCH 0701/1143] qml: remove swapStarted, swapSuccess and swapFailed signals the state property and associated stateChanged signal can be used instead --- electrum/gui/qml/qeswaphelper.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 711b8b02e..45ef169f4 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -30,9 +30,6 @@ class State: confirm = pyqtSignal([str], arguments=['message']) error = pyqtSignal([str], arguments=['message']) - swapStarted = pyqtSignal() - swapSuccess = pyqtSignal() - swapFailed = pyqtSignal([str], arguments=['message']) def __init__(self, parent=None): super().__init__(parent) @@ -361,7 +358,6 @@ def swap_task(): fut = asyncio.run_coroutine_threadsafe(coro, loop) self.userinfo = _('Performing swap...') self.state = QESwapHelper.State.Started - self.swapStarted.emit() txid = fut.result() try: # swaphelper might be destroyed at this point self.userinfo = ' '.join([ @@ -371,14 +367,12 @@ def swap_task(): _('You will need to be online to finalize the swap, or the transaction will be refunded to you after some delay.'), ]) self.state = QESwapHelper.State.Success - self.swapSuccess.emit() except RuntimeError: pass except Exception as e: try: # swaphelper might be destroyed at this point self.state = QESwapHelper.State.Failed self._logger.error(str(e)) - self.swapFailed.emit(str(e)) except RuntimeError: pass @@ -399,7 +393,6 @@ def swap_task(): fut = asyncio.run_coroutine_threadsafe(coro, loop) self.userinfo = _('Performing swap...') self.state = QESwapHelper.State.Started - self.swapStarted.emit() success = fut.result() try: # swaphelper might be destroyed at this point if success: @@ -410,11 +403,9 @@ def swap_task(): _('You may broadcast it before that manually, but this is not trustless.'), ]) self.state = QESwapHelper.State.Success - self.swapSuccess.emit() else: self.userinfo = _('Swap failed!') self.state = QESwapHelper.State.Failed - self.swapFailed.emit('') except RuntimeError: pass except Exception as e: @@ -422,7 +413,6 @@ def swap_task(): self.userinfo = _('Swap failed!') self.state = QESwapHelper.State.Failed self._logger.error(str(e)) - self.swapFailed.emit(str(e)) except RuntimeError: pass From 460c198b02d34c230c4f2572fc3f5674bde605ab Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 15 Apr 2023 11:31:44 +0200 Subject: [PATCH 0702/1143] qml: remove send_onchain (dead code) --- electrum/gui/qml/qewallet.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 0b6366620..54bcbc334 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -477,28 +477,6 @@ def enableLightning(self): self.isLightningChanged.emit() self.dataChanged.emit() - @pyqtSlot(str, int, int, bool) - def send_onchain(self, address, amount, fee=None, rbf=False): - self._logger.info('send_onchain: %s %d' % (address,amount)) - coins = self.wallet.get_spendable_coins(None) - if not bitcoin.is_address(address): - self._logger.warning('Invalid Bitcoin Address: ' + address) - return False - - outputs = [PartialTxOutput.from_address_and_value(address, amount)] - self._logger.info(str(outputs)) - output_values = [x.value for x in outputs] - if any(parse_max_spend(outval) for outval in output_values): - output_value = '!' - else: - output_value = sum(output_values) - self._logger.info(str(output_value)) - # see qt/confirm_tx_dialog qt/main_window - tx = self.wallet.make_unsigned_transaction(coins=coins,outputs=outputs, fee=None) - self._logger.info(str(tx.to_json())) - tx.set_rbf(True) - self.sign(tx, broadcast=True) - @auth_protect def sign(self, tx, *, broadcast: bool = False): sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast), From c9cc56b68785696fb32a727b3c94b1b274a4988c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 16 Apr 2023 21:14:15 +0000 Subject: [PATCH 0703/1143] transaction: don't include WIT_UTXO for non-segwit txins probably regression from d3227d7489fe327bd40e891a517c86bd207227ec fixes https://github.com/spesmilo/electrum/issues/8305 --- electrum/tests/test_wallet_vertical.py | 95 ++++++++++++++++++++++++++ electrum/wallet.py | 7 ++ 2 files changed, 102 insertions(+) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index c0d07dfba..a91843d28 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -2756,6 +2756,101 @@ async def test_export_psbt_with_xpubs__singlesig(self, mock_save_db): self.assertEqual("70736274ff0100710200000001916fa04d7080ae0cb19bd08671d37dbe3dc925be383737bb34b3097d82830dc70000000000fdffffff0240e20100000000001600147e45d43294b0ff2b08a5f45232649815e516cff0ceaa05000000000016001456ec9cad206160ab578fa1dfbe13311b3be4a3107f4a24000001011f96a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd70100fd2e010200000000010122c3730eb6314cf59e11988c41bfdd73f70cb55b294ec6f2eda828b5c939c0980100000000fdffffff0196a007000000000016001413ce91db66299806c4f35b2b4f8426b0bd4f2cd704004730440220112840ce5486c6b2d15bc3b12e45c2a4518828e1b34f9bb0b3a78220c0cec52f02205b146a1f683289909ecbd3f53932d5acc321444101d8002e435b38a54adbf47201473044022058dfb4c75de119595119f35dcd7b1b2c28c40d7e2e746baeae83f09396c6bb9e02201c3c40fb684253638f12392af3934a90a6c6a512441aac861022f927473c952001475221022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc482750492103cf9a6ac058d36a6dc325b19715a2223c6416e1cef13bc047a99bded8c99463ca52ae4a4a24002206029e65093d22877cbfcc27cb754c58d144ec96635af1fcc63e5a7b90b23bb6acb81830cf1be5540000800100008000000080000000000000000000002202031503b2e74b21d4583b7f0d9e65b2c0ef19fd6e8aae7d0524fc770a1d2b2127501830cf1be5540000800100008000000080010000000000000000", tx.serialize_as_bytes().hex()) + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + async def test_export_psbt__rm_witness_utxo_from_non_segwit_input(self, mock_save_db): + """We sometimes convert full utxo to witness_utxo in psbt inputs when using QR codes, to save space, + even for non-segwit inputs (which goes against the spec). + This tests that upon scanning the QR code, if we can add the full utxo to the input (e.g. via network), + we remove the witness_utxo before e.g. re-exporting it. (see #8305) + """ + wallet1a = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_bip43_rootseed(keystore.bip39_to_seed("income sample useless art skate lucky fold field bargain course hope chest", ''), "m/45h/0", xtype="standard"), + keystore.from_xpub('tpubDC1y33c2iTcxCBFva3zxbQxUnbzBT1TPVrwLgwVHtqSnVRx2pbJsrHzNYmXnKEnrNqyKk9BERrpSatqVu4JHV4K4hepFQdqnMojA5NVKxcF'), + ], + '2of2', gap_limit=2, + config=self.config, + ) + wallet1a.get_keystores()[1].add_key_origin(derivation_prefix="m/45h/0", root_fingerprint="25750cf7") + wallet1b = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_xpub('tpubDAKtPDG6fezcwhB7rNJ9NVEWwGokNzowW3AaMVYFTS4WKoBTNESS1NpntWYDq2uABVYM1xa5cVmu8LD2xKYipMRVLy1VjBQeVe6pixJeBgr'), + keystore.from_xpub('tpubDC1y33c2iTcxCBFva3zxbQxUnbzBT1TPVrwLgwVHtqSnVRx2pbJsrHzNYmXnKEnrNqyKk9BERrpSatqVu4JHV4K4hepFQdqnMojA5NVKxcF'), + ], + '2of2', gap_limit=2, + config=self.config, + ) + wallet1b.get_keystores()[0].add_key_origin(derivation_prefix="m/45h/0", root_fingerprint="18c2928f") + wallet1b.get_keystores()[1].add_key_origin(derivation_prefix="m/45h/0", root_fingerprint="25750cf7") + wallet1b_offline = WalletIntegrityHelper.create_multisig_wallet( + [ + keystore.from_bip43_rootseed(keystore.bip39_to_seed("wear wasp subject october amount essay maximum monkey excuse plastic ginger donor", ''), "m/45h/0", xtype="standard"), + keystore.from_xpub('tpubDAKtPDG6fezcwhB7rNJ9NVEWwGokNzowW3AaMVYFTS4WKoBTNESS1NpntWYDq2uABVYM1xa5cVmu8LD2xKYipMRVLy1VjBQeVe6pixJeBgr'), + ], + '2of2', gap_limit=2, + config=self.config, + ) + wallet1b_offline.get_keystores()[1].add_key_origin(derivation_prefix="m/45h/0", root_fingerprint="18c2928f") + + # bootstrap wallet + funding_tx = Transaction('0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a112500') + funding_txid = funding_tx.txid() + self.assertEqual('e1a5465e813b51047e1ee95a2c635416f0105b52361084c7e005325f685f374e', funding_txid) + wallet1a.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + wallet1b.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # cosignerA creates and signs the tx + outputs = [PartialTxOutput.from_address_and_value("tb1qgacvp0zvgtk3etggjayuezrc2mkql8veshv4xw", 200_000)] + coins = wallet1a.get_spendable_coins(domain=None) + tx = wallet1a.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) + tx.set_rbf(True) + tx.locktime = 2429212 + tx.version = 2 + wallet1a.sign_transaction(tx, password=None) + + # cosignerA shares psbt with cosignerB + orig_tx1 = tx + for uses_qr_code1 in (False, True, ): + with self.subTest(uses_qr_code1=uses_qr_code1): + tx = copy.deepcopy(orig_tx1) + if uses_qr_code1: + partial_tx, is_complete = tx.to_qr_data() + self.assertEqual("3PMZFRKS5WP6JMMK.-I6Z5JFJ+3ABTDQ.SEM2ATLOB0EF-5I3VH0+Z:P$3SWOO75P/P41QSRJ+4-P*V6MJLC0H.XH1CJ+066VC6IV/5+H1S0R*1NNW.EBSHKZ7IA3T$-$OTUQMP22B+ZVM4QSL/K/BIT8WOM1712MQWDH1DQA/0DEUH$YKYDYDC+/MO-$ZXBM:L+/8F83FD5*:N8HU45:9YULHULQ/P.HLIHVHFQR+WRVT7P.DTUE0BE91DK56:S$Y8+ZBJ0ZSSRRUPNE$I18Y.TXFRM.CTZSGVTSQWNX8Z+YLWR5F8.RVZ1039*U.H7BN6ZMHSBWS*PLY3SK+9LV/FBGJK4+YU3IGI3S4Z9RXS8$JVP+VZUZ:PDJI$KI-6DG2A//O5PRDLP3RUSX.KBFP.IY2JZV+B:DF3.C+R9LU0JUXF26W3SME9A*/WWNNH0-59RCI-YKG:SOO:U0F*SV5R5VERVP2J57EJMO*9.GH++/7P55YE/QTLU$MB8.KT*HD4S2ISP35+*R14HXP:SDUGWGGH$Y8O/NZSH0*CXQZ+H3G7E5:5HFFB8C-BA/O*04I/GF6.X0DKYETTJ:NO27RKHTL:/44U.PK/F/9+9V4D:N3*YS5OTA7+/:P70+L/JMB0OD7ZMO/HFJXRFCK7GS1-K464$96KODYGML8IJLR31-2W1EI0HXOWG:3N9M7QRTU83-NK*G:6SI.JU*71UW85MZ./Y:03L6KZTG7SJ.VKO3WFZU.XV+745QZ.OWET:VNV/.QNR-ETA2S/LTV-U-M2OC2LV7.*1AIN4XW3LR$*75/BVIV.KG1ZGMBJ7L0IE9F-7O4+1QSZ8JR$GECW6RZFKPZ516O+2GV9FTA:3L1C1QL/6YVSF*L8-38/7L1$**Y7K5FLOP-4T20.*1*8JK-M$C+:5U+S*KLZW3E3U0N$ODSMT", + partial_tx) + self.assertFalse(is_complete) + else: + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007202000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e10000000000fdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500000100df0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a1125002202026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd4730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf459010104475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152ae2206026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd1418c2928f2d000080000000000000000000000000220603a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb9811425750cf72d000080000000000000000000000000000001004752210212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe6276821028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b52ae22020212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe627681425750cf72d0000800000000001000000000000002202028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b1418c2928f2d00008000000000010000000000000000", + partial_tx) + # load tx into cosignerB's online wallet + tx = tx_from_any(partial_tx) + self.assertFalse(tx.is_segwit()) + self.assertFalse(tx.is_complete()) + tx.add_info_from_wallet(wallet1b) + + # cosignerB moves psbt from his online wallet to offline wallet + orig_tx2 = tx + for uses_qr_code2 in (False, True, ): + with self.subTest(uses_qr_code2=uses_qr_code2): + tx = copy.deepcopy(orig_tx2) + if uses_qr_code2: + partial_tx, is_complete = tx.to_qr_data() + self.assertEqual("3PMZFRKS5WP6JMMK.-I6Z5JFJ+3ABTDQ.SEM2ATLOB0EF-5I3VH0+Z:P$3SWOO75P/P41QSRJ+4-P*V6MJLC0H.XH1CJ+066VC6IV/5+H1S0R*1NNW.EBSHKZ7IA3T$-$OTUQMP22B+ZVM4QSL/K/BIT8WOM1712MQWDH1DQA/0DEUH$YKYDYDC+/MO-$ZXBM:L+/8F83FD5*:N8HU45:9YULHULQ/P.HLIHVHFQR+WRVT7P.DTUE0BE91DK56:S$Y8+ZBJ0ZSSRRUPNE$I18Y.TXFRM.CTZSGVTSQWNX8Z+YLWR5F8.RVZ1039*U.H7BN6ZMHSBWS*PLY3SK+9LV/FBGJK4+YU3IGI3S4Z9RXS8$JVP+VZUZ:PDJI$KI-6DG2A//O5PRDLP3RUSX.KBFP.IY2JZV+B:DF3.C+R9LU0JUXF26W3SME9A*/WWNNH0-59RCI-YKG:SOO:U0F*SV5R5VERVP2J57EJMO*9.GH++/7P55YE/QTLU$MB8.KT*HD4S2ISP35+*R14HXP:SDUGWGGH$Y8O/NZSH0*CXQZ+H3G7E5:5HFFB8C-BA/O*04I/GF6.X0DKYETTJ:NO27RKHTL:/44U.PK/F/9+9V4D:N3*YS5OTA7+/:P70+L/JMB0OD7ZMO/HFJXRFCK7GS1-K464$96KODYGML8IJLR31-2W1EI0HXOWG:3N9M7QRTU83-NK*G:6SI.JU*71UW85MZ./Y:03L6KZTG7SJ.VKO3WFZU.XV+745QZ.OWET:VNV/.QNR-ETA2S/LTV-U-M2OC2LV7.*1AIN4XW3LR$*75/BVIV.KG1ZGMBJ7L0IE9F-7O4+1QSZ8JR$GECW6RZFKPZ516O+2GV9FTA:3L1C1QL/6YVSF*L8-38/7L1$**Y7K5FLOP-4T20.*1*8JK-M$C+:5U+S*KLZW3E3U0N$ODSMT", + partial_tx) + self.assertFalse(is_complete) + else: + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007202000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e10000000000fdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500000100df0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a1125002202026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd4730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf459010104475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152ae2206026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd1418c2928f2d000080000000000000000000000000220603a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb9811425750cf72d000080000000000000000000000000000001004752210212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe6276821028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b52ae22020212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe627681425750cf72d0000800000000001000000000000002202028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b1418c2928f2d00008000000000010000000000000000", + partial_tx) + # load tx into cosignerB's online wallet + tx = tx_from_any(partial_tx) + wallet1b_offline.sign_transaction(tx, password=None) + + self.assertEqual('02000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e100000000d9004730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf4590147304402203ba7cc21e407ce31c1eecd11c367df716a5d47f06e0bf7109f08063ede25a364022039f6bef0dd401aa2c3103b8cbab57cc4fed3905ccb0a726dc6594bf5930ae0b401475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152aefdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500', + str(tx)) + self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.txid()) + self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.wtxid()) + class TestWalletOfflineSigning(ElectrumTestCase): TESTNET = True diff --git a/electrum/wallet.py b/electrum/wallet.py index 6b217c027..68bb85223 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2210,14 +2210,21 @@ def _add_input_utxo_info( # - For witness v1, witness_utxo will be enough though (bip-0341 sighash fixes known prior issues). # - We cannot include UTXO if the prev tx is not signed yet (chain of unsigned txs). address = address or txin.address + # add witness_utxo if txin.witness_utxo is None and txin.is_segwit() and address: received, spent = self.adb.get_addr_io(address) item = received.get(txin.prevout.to_str()) if item: txin_value = item[2] txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value) + # add utxo if txin.utxo is None: txin.utxo = self.db.get_transaction(txin.prevout.txid.hex()) + # Maybe remove witness_utxo. witness_utxo should not be present for non-segwit inputs. + # If it is present, it might be because another electrum instance added it when sharing the psbt via QR code. + # If we have the full utxo available, we can remove it without loss of information. + if txin.witness_utxo and not txin.is_segwit() and txin.utxo: + txin.witness_utxo = None def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput], address: str) -> bool: From fb480fe5ed2aa97308ba65d88762195134bc11f4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 16 Apr 2023 21:25:27 +0000 Subject: [PATCH 0704/1143] follow-up prev: fix typo in comment --- electrum/tests/test_wallet_vertical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index a91843d28..5b43a43de 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -2842,7 +2842,7 @@ async def test_export_psbt__rm_witness_utxo_from_non_segwit_input(self, mock_sav partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff01007202000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e10000000000fdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500000100df0200000000010199b6eb9629c9763e9e95c49f2e81d7a9bda0c8e96165897ce42df0c7a4757aa60100000000fdffffff0220a107000000000017a91482e2921d413a7cad08f76d1d35565dbcc85088db8750560e000000000016001481e6fc4a427d0176373bdd7482b8c1d08f3563300247304402202cf7be624cc30640e2b928adeb25b21ed581f32149f78bc1b0fa9c01da785486022066fadccb1aef8d46841388e83386f85ca5776f50890b9921f165f093fabfd2800121022e43546769a51181fad61474a773b0813106895971b6e3f1d43278beb7154d0a1a1125002202026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd4730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf459010104475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152ae2206026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd1418c2928f2d000080000000000000000000000000220603a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb9811425750cf72d000080000000000000000000000000000001004752210212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe6276821028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b52ae22020212de0581d6570d3cc432cdad2b07514807007dc80b792fafeb47bed69fe627681425750cf72d0000800000000001000000000000002202028748a66f10b13944ccb14640ba36f65dc7a1f3462e9aca65ba8b05013842270b1418c2928f2d00008000000000010000000000000000", partial_tx) - # load tx into cosignerB's online wallet + # load tx into cosignerB's offline wallet tx = tx_from_any(partial_tx) wallet1b_offline.sign_transaction(tx, password=None) From 6b1e6f077541567ad5bcac7d899a525ffc17a61d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 16 Apr 2023 21:28:24 +0000 Subject: [PATCH 0705/1143] follow-up prev again... --- electrum/tests/test_wallet_vertical.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 5b43a43de..8b1dedbe4 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -2846,10 +2846,10 @@ async def test_export_psbt__rm_witness_utxo_from_non_segwit_input(self, mock_sav tx = tx_from_any(partial_tx) wallet1b_offline.sign_transaction(tx, password=None) - self.assertEqual('02000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e100000000d9004730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf4590147304402203ba7cc21e407ce31c1eecd11c367df716a5d47f06e0bf7109f08063ede25a364022039f6bef0dd401aa2c3103b8cbab57cc4fed3905ccb0a726dc6594bf5930ae0b401475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152aefdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500', - str(tx)) - self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.txid()) - self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.wtxid()) + self.assertEqual('02000000014e375f685f3205e0c7841036525b10f01654632c5ae91e7e04513b815e46a5e100000000d9004730440220414287f36a02b004d2e9a3892e1862edaf49c35d50b65ae10b601879b8c793ef0220073234c56d5a8ae9f4fcfeaecaa757e2724bf830d45aabfab8ffe37329ebf4590147304402203ba7cc21e407ce31c1eecd11c367df716a5d47f06e0bf7109f08063ede25a364022039f6bef0dd401aa2c3103b8cbab57cc4fed3905ccb0a726dc6594bf5930ae0b401475221026addf5fd752c92e8a53955e430ca5964feb1b900ce569f968290f65ae7fecbfd2103a8b896e5216fe7239516a494407c0cc90c6dc33918c7df04d1cda8d57a3bb98152aefdffffff02400d0300000000001600144770c0bc4c42ed1cad089749cc887856ec0f9d99588004000000000017a914493900cdec652a41c633436b53d574647e329b18871c112500', + str(tx)) + self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.txid()) + self.assertEqual('d6823918ff82ed240995e9e6f02e0d2f3f15e0b942616ab34481ce8a3399dc72', tx.wtxid()) class TestWalletOfflineSigning(ElectrumTestCase): From 12a81c1a34bb88c1eda17c998d3e0c72bea5ee60 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 16 Apr 2023 22:43:34 +0000 Subject: [PATCH 0706/1143] tests: add tests for util.age --- electrum/tests/test_util.py | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index 9668aa2e1..9013d6afd 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -1,3 +1,4 @@ +from datetime import datetime from decimal import Decimal from electrum import util @@ -367,3 +368,72 @@ def test_error_text_str_to_safe_str(self): # unicode self.assertEqual("'here is some unicode: \\\\u20bf \\\\U0001f600 \\\\U0001f608'", util.error_text_str_to_safe_str("here is some unicode: ₿ 😀 😈")) + + def test_age(self): + now = datetime(2023, 4, 16, 22, 30, 00) + self.assertEqual("Unknown", + util.age(from_date=None, since_date=now)) + # past + self.assertEqual("less than a minute ago", + util.age(from_date=now.timestamp()-1, since_date=now)) + self.assertEqual("1 seconds ago", + util.age(from_date=now.timestamp()-1, since_date=now, include_seconds=True)) + self.assertEqual("25 seconds ago", + util.age(from_date=now.timestamp()-25, since_date=now, include_seconds=True)) + self.assertEqual("about 30 minutes ago", + util.age(from_date=now.timestamp()-1800, since_date=now)) + self.assertEqual("about 30 minutes ago", + util.age(from_date=now.timestamp()-1800, since_date=now, include_seconds=True)) + self.assertEqual("about 1 hour ago", + util.age(from_date=now.timestamp()-3300, since_date=now)) + self.assertEqual("about 2 hours ago", + util.age(from_date=now.timestamp()-8700, since_date=now)) + self.assertEqual("about 7 hours ago", + util.age(from_date=now.timestamp()-26700, since_date=now)) + self.assertEqual("about 1 day ago", + util.age(from_date=now.timestamp()-109800, since_date=now)) + self.assertEqual("about 3 days ago", + util.age(from_date=now.timestamp()-282600, since_date=now)) + self.assertEqual("about 15 days ago", + util.age(from_date=now.timestamp()-1319400, since_date=now)) + self.assertEqual("about 1 month ago", + util.age(from_date=now.timestamp()-3220200, since_date=now)) + self.assertEqual("about 3 months ago", + util.age(from_date=now.timestamp()-8317800, since_date=now)) + self.assertEqual("about 1 year ago", + util.age(from_date=now.timestamp()-39853800, since_date=now)) + self.assertEqual("over 3 years ago", + util.age(from_date=now.timestamp()-103012200, since_date=now)) + # future + self.assertEqual("in less than a minute", + util.age(from_date=now.timestamp()+1, since_date=now)) + self.assertEqual("in 1 seconds", + util.age(from_date=now.timestamp()+1, since_date=now, include_seconds=True)) + self.assertEqual("in 25 seconds", + util.age(from_date=now.timestamp()+25, since_date=now, include_seconds=True)) + self.assertEqual("in about 30 minutes", + util.age(from_date=now.timestamp()+1800, since_date=now)) + self.assertEqual("in about 30 minutes", + util.age(from_date=now.timestamp()+1800, since_date=now, include_seconds=True)) + self.assertEqual("in about 1 hour", + util.age(from_date=now.timestamp()+3300, since_date=now)) + self.assertEqual("in about 2 hours", + util.age(from_date=now.timestamp()+8700, since_date=now)) + self.assertEqual("in about 7 hours", + util.age(from_date=now.timestamp()+26700, since_date=now)) + self.assertEqual("in about 1 day", + util.age(from_date=now.timestamp()+109800, since_date=now)) + self.assertEqual("in about 3 days", + util.age(from_date=now.timestamp()+282600, since_date=now)) + self.assertEqual("in about 15 days", + util.age(from_date=now.timestamp()+1319400, since_date=now)) + self.assertEqual("in about 1 month", + util.age(from_date=now.timestamp()+3220200, since_date=now)) + self.assertEqual("in about 3 months", + util.age(from_date=now.timestamp()+8317800, since_date=now)) + self.assertEqual("in about 1 year", + util.age(from_date=now.timestamp()+39853800, since_date=now)) + self.assertEqual("in over 3 years", + util.age(from_date=now.timestamp()+103012200, since_date=now)) + + From f528758c292c006ad4c12df392789819acb3d372 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 16 Apr 2023 23:05:22 +0000 Subject: [PATCH 0707/1143] util: merge time_difference() into age(), make age() localisable fixes https://github.com/spesmilo/electrum/issues/8304 follow-up 4d4d2e2206c7d596ec36132e19254f1e7106933b --- electrum/gui/qml/util.py | 2 +- electrum/util.py | 75 +++++++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qml/util.py b/electrum/gui/qml/util.py index 0ac83721e..19db9c537 100644 --- a/electrum/gui/qml/util.py +++ b/electrum/gui/qml/util.py @@ -32,7 +32,7 @@ def decorator(self, *args): # return delay in msec when expiry time string should be updated # returns 0 when expired or expires > 1 day away (no updates needed) def status_update_timer_interval(exp): - # very roughly according to util.time_difference + # very roughly according to util.age exp_in = int(exp - time()) exp_in_min = int(exp_in/60) diff --git a/electrum/util.py b/electrum/util.py index 742f873a1..adf502293 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -794,8 +794,14 @@ def format_time(timestamp: Union[int, float, None]) -> str: return date.isoformat(' ', timespec="minutes") if date else _("Unknown") -# Takes a timestamp and returns a string with the approximation of the age -def age(from_date, since_date = None, target_tz=None, include_seconds=False): +def age( + from_date: Union[int, float, None], # POSIX timestamp + *, + since_date: datetime = None, + target_tz=None, + include_seconds: bool = False, +) -> str: + """Takes a timestamp and returns a string with the approximation of the age""" if from_date is None: return _("Unknown") @@ -803,38 +809,67 @@ def age(from_date, since_date = None, target_tz=None, include_seconds=False): if since_date is None: since_date = datetime.now(target_tz) - td = time_difference(from_date - since_date, include_seconds) - return (_("{} ago") if from_date < since_date else _("in {}")).format(td) - - -def time_difference(distance_in_time, include_seconds): - #distance_in_time = since_date - from_date + distance_in_time = from_date - since_date + is_in_past = from_date < since_date distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds))) - distance_in_minutes = int(round(distance_in_seconds/60)) + distance_in_minutes = int(round(distance_in_seconds / 60)) if distance_in_minutes == 0: if include_seconds: - return _("{} seconds").format(distance_in_seconds) + if is_in_past: + return _("{} seconds ago").format(distance_in_seconds) + else: + return _("in {} seconds").format(distance_in_seconds) else: - return _("less than a minute") + if is_in_past: + return _("less than a minute ago") + else: + return _("in less than a minute") elif distance_in_minutes < 45: - return _("about {} minutes").format(distance_in_minutes) + if is_in_past: + return _("about {} minutes ago").format(distance_in_minutes) + else: + return _("in about {} minutes").format(distance_in_minutes) elif distance_in_minutes < 90: - return _("about 1 hour") + if is_in_past: + return _("about 1 hour ago") + else: + return _("in about 1 hour") elif distance_in_minutes < 1440: - return _("about {} hours").format(round(distance_in_minutes / 60.0)) + if is_in_past: + return _("about {} hours ago").format(round(distance_in_minutes / 60.0)) + else: + return _("in about {} hours").format(round(distance_in_minutes / 60.0)) elif distance_in_minutes < 2880: - return _("about 1 day") + if is_in_past: + return _("about 1 day ago") + else: + return _("in about 1 day") elif distance_in_minutes < 43220: - return _("about {} days").format(round(distance_in_minutes / 1440)) + if is_in_past: + return _("about {} days ago").format(round(distance_in_minutes / 1440)) + else: + return _("in about {} days").format(round(distance_in_minutes / 1440)) elif distance_in_minutes < 86400: - return _("about 1 month") + if is_in_past: + return _("about 1 month ago") + else: + return _("in about 1 month") elif distance_in_minutes < 525600: - return _("about {} months").format(round(distance_in_minutes / 43200)) + if is_in_past: + return _("about {} months ago").format(round(distance_in_minutes / 43200)) + else: + return _("in about {} months").format(round(distance_in_minutes / 43200)) elif distance_in_minutes < 1051200: - return _("about 1 year") + if is_in_past: + return _("about 1 year ago") + else: + return _("in about 1 year") else: - return _("over {} years").format(round(distance_in_minutes / 525600)) + if is_in_past: + return _("over {} years ago").format(round(distance_in_minutes / 525600)) + else: + return _("in over {} years").format(round(distance_in_minutes / 525600)) mainnet_block_explorers = { 'Bitupper Explorer': ('https://bitupper.com/en/explorer/bitcoin/', From 47033369bd947e65e2411cfcbc8208521429931c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 09:43:31 +0200 Subject: [PATCH 0708/1143] qml: auth.py use f'' string format notation --- electrum/gui/qml/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py index c8dd3d825..1d1e5104a 100644 --- a/electrum/gui/qml/auth.py +++ b/electrum/gui/qml/auth.py @@ -34,7 +34,7 @@ def authProceed(self): r = func(self, *args, **kwargs) return r except Exception as e: - self._auth_logger.error('Error executing wrapped fn(): %s' % repr(e)) + self._auth_logger.error(f'Error executing wrapped fn(): {repr(e)}') raise e finally: delattr(self,'__auth_fcall') @@ -51,9 +51,9 @@ def authCancel(self): if hasattr(self, reject): getattr(self, reject)() else: - self._auth_logger.error('Reject method \'%s\' not defined' % reject) + self._auth_logger.error(f'Reject method "{reject}" not defined') except Exception as e: - self._auth_logger.error('Error executing reject function \'%s\': %s' % (reject, repr(e))) + self._auth_logger.error(f'Error executing reject function "{reject}": {repr(e)}') raise e finally: delattr(self, '__auth_fcall') From 1bca30166197ea533a686d275237299742b36d52 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 10:35:31 +0200 Subject: [PATCH 0709/1143] qml: piechart visible also when only unconfirmed, add unconfirmed to legend --- .../gui/qml/components/BalanceDetails.qml | 32 ++++++++++++++++--- electrum/gui/qml/components/Constants.qml | 1 + 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index e2abc0a9d..3071a1c58 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -51,13 +51,16 @@ Pane { Piechart { id: piechart - visible: Daemon.currentWallet.totalBalance.satsInt > 0 + + property real total: 0 + + visible: total > 0 Layout.preferredWidth: parent.width implicitHeight: 220 // TODO: sane value dependent on screen innerOffset: 6 function updateSlices() { var p = Daemon.currentWallet.getBalancesForPiechart() - var total = p['total'] + total = p['total'] piechart.slices = [ { v: p['lightning']/total, color: constants.colorPiechartLightning, text: qsTr('Lightning') }, @@ -79,8 +82,13 @@ Pane { Layout.alignment: Qt.AlignHCenter visible: Daemon.currentWallet columns: 3 - Item { - Layout.preferredWidth: 1; Layout.preferredHeight: 1 + + Rectangle { + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + border.color: constants.colorPiechartTotal + color: 'transparent' + radius: constants.iconSizeXSmall/2 } Label { text: qsTr('Total') @@ -114,7 +122,6 @@ Pane { Label { visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty text: qsTr('On-chain') - } FormattedAmount { visible: Daemon.currentWallet.isLightning || !Daemon.currentWallet.frozenBalance.isEmpty @@ -135,6 +142,21 @@ Pane { amount: Daemon.currentWallet.frozenBalance visible: !Daemon.currentWallet.frozenBalance.isEmpty } + + Rectangle { + visible: !Daemon.currentWallet.unconfirmedBalance.isEmpty + Layout.preferredWidth: constants.iconSizeXSmall + Layout.preferredHeight: constants.iconSizeXSmall + color: constants.colorPiechartUnconfirmed + } + Label { + visible: !Daemon.currentWallet.unconfirmedBalance.isEmpty + text: qsTr('Unconfirmed') + } + FormattedAmount { + amount: Daemon.currentWallet.unconfirmedBalance + visible: !Daemon.currentWallet.unconfirmedBalance.isEmpty + } } Heading { diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 0beec55cb..43d1180c5 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -46,6 +46,7 @@ Item { property color colorLightningRemote: "yellow" property color colorChannelOpen: "#ff80ff80" + property color colorPiechartTotal: Material.accentColor property color colorPiechartOnchain: Qt.darker(Material.accentColor, 1.50) property color colorPiechartFrozen: 'gray' property color colorPiechartLightning: 'orange' From 25c59f700c94f68c1bc9b02f802bea9612e82940 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 10:42:55 +0200 Subject: [PATCH 0710/1143] qml: ConfirmTxDialog amount fixed font --- electrum/gui/qml/components/ConfirmTxDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index e1241fbe6..5910bc1cf 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -58,6 +58,7 @@ ElDialog { Label { id: btcValue font.bold: true + font.family: FixedFont } Label { From 93ef0131110fbf0d9c38a7980adbd63ddc7f5e30 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 11:40:09 +0200 Subject: [PATCH 0711/1143] qml: fix recoverable channels setting --- electrum/gui/qml/components/Preferences.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 569f75add..a2cbababc 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -294,7 +294,7 @@ Pane { yesno: true }) dialog.accepted.connect(function() { - Config.useRecoverableChannels = False + Config.useRecoverableChannels = false }) dialog.rejected.connect(function() { checked = true // revert From 01b9cee6439e1e2b592f246787f1e7cc3f228364 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 11:41:02 +0200 Subject: [PATCH 0712/1143] qml: add recoverable channels warning to OpenChannelDialog --- .../gui/qml/components/OpenChannelDialog.qml | 28 +++++++++++++++++++ electrum/gui/qml/qewallet.py | 8 ++++++ 2 files changed, 36 insertions(+) diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 51a1b104c..8fa5585ba 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -39,6 +39,34 @@ ElDialog { columns: 4 + InfoTextArea { + Layout.fillWidth: true + Layout.columnSpan: 4 + visible: !Daemon.currentWallet.lightningHasDeterministicNodeId + iconStyle: InfoTextArea.IconStyle.Warn + text: Daemon.currentWallet.seedType == 'segwit' + ? [ qsTr('Your channels cannot be recovered from seed, because they were created with an old version of Electrum.'), + qsTr('This means that you must save a backup of your wallet everytime you create a new channel.'), + '\n\n', + qsTr('If you want this wallet to have recoverable channels, you must close your existing channels and restore this wallet from seed.') + ].join(' ') + : [ qsTr('Your channels cannot be recovered from seed.'), + qsTr('This means that you must save a backup of your wallet everytime you create a new channel.'), + '\n\n', + qsTr('If you want to have recoverable channels, you must create a new wallet with an Electrum seed') + ].join(' ') + } + + InfoTextArea { + Layout.fillWidth: true + Layout.columnSpan: 4 + visible: Daemon.currentWallet.lightningHasDeterministicNodeId && !Config.useRecoverableChannels + iconStyle: InfoTextArea.IconStyle.Warn + text: [ qsTr('You currently have recoverable channels setting disabled.'), + qsTr('This means your channels cannot be recovered from seed.') + ].join(' ') + } + Label { text: qsTr('Node') color: Material.accentColor diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 54bcbc334..a6e6ca346 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -360,6 +360,10 @@ def txinType(self): return self.wallet.txin_type return self.wallet.get_txin_type(self.wallet.dummy_address()) + @pyqtProperty(str, notify=dataChanged) + def seedType(self): + return self.wallet.db.get('seed_type') + @pyqtProperty(bool, notify=dataChanged) def isWatchOnly(self): return self.wallet.is_watching_only() @@ -394,6 +398,10 @@ def keystores(self): def lightningNodePubkey(self): return self.wallet.lnworker.node_keypair.pubkey.hex() if self.wallet.lnworker else '' + @pyqtProperty(bool, notify=dataChanged) + def lightningHasDeterministicNodeId(self): + return self.wallet.lnworker.has_deterministic_node_id() if self.wallet.lnworker else False + @pyqtProperty(str, notify=dataChanged) def derivationPrefix(self): keystores = self.wallet.get_keystores() From d80de3424b1f7361b93bf3fdc85a3cbbf2575ccd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 12:17:32 +0200 Subject: [PATCH 0713/1143] qml: piechart improve font rendering, use app font --- electrum/gui/qml/components/controls/Piechart.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/Piechart.qml b/electrum/gui/qml/components/controls/Piechart.qml index 8e5329849..16dc69ed0 100644 --- a/electrum/gui/qml/components/controls/Piechart.qml +++ b/electrum/gui/qml/components/controls/Piechart.qml @@ -17,6 +17,7 @@ Canvas { var ctx = getContext('2d') ctx.reset() + ctx.font = "" + constants.fontSizeSmall + "px '" + app.font.family + "', sans-serif" ctx.strokeStyle = Qt.rgba(1, 1, 1, 1) ctx.lineWidth = 2 var pcx = width/2 @@ -67,7 +68,7 @@ Canvas { ctx.lineTo(pcx+dx2+ddx, pcy+dy2) ctx.moveTo(pcx+dx2, pcy+dy2) - ctx.text(slice.text, xtext, pcy+dy2 - constants.paddingXSmall) + ctx.fillText(slice.text, xtext, pcy+dy2 - constants.paddingXSmall) ctx.stroke() } From 3be5db15d2bb6143101c17a8054797efa48237e1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 12:19:59 +0200 Subject: [PATCH 0714/1143] qml: don't show channel backup data in share screen --- electrum/gui/qml/components/ChannelOpenProgressDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ChannelOpenProgressDialog.qml b/electrum/gui/qml/components/ChannelOpenProgressDialog.qml index 1094974c8..199b4cdd0 100644 --- a/electrum/gui/qml/components/ChannelOpenProgressDialog.qml +++ b/electrum/gui/qml/components/ChannelOpenProgressDialog.qml @@ -117,7 +117,7 @@ ElDialog { var sharedialog = app.genericShareDialog.createObject(app, { title: qsTr('Save Channel Backup'), - text: dialog.channelBackup, + text_qr: dialog.channelBackup, text_help: qsTr('The channel you created is not recoverable from seed.') + ' ' + qsTr('To prevent fund losses, please save this backup on another device.') + ' ' + qsTr('It may be imported in another Electrum wallet with the same seed.') From a1314f5992444ad45246950b6d254b48bd981c06 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 12:48:51 +0200 Subject: [PATCH 0715/1143] qml: only show channel ratio bar when appropriate --- electrum/gui/qml/components/ChannelDetails.qml | 6 ++++++ electrum/gui/qml/components/controls/ChannelDelegate.qml | 2 +- electrum/gui/qml/qechanneldetails.py | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 1cef67ffe..3043cea1e 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -61,6 +61,7 @@ Pane { } Label { + Layout.fillWidth: true text: channeldetails.short_cid } @@ -77,11 +78,13 @@ Pane { } Label { + visible: !channeldetails.isBackup text: qsTr('Initiator') color: Material.accentColor } Label { + visible: !channeldetails.isBackup text: channeldetails.initiator } @@ -146,6 +149,9 @@ Pane { Layout.fillWidth: true Layout.topMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingXLarge + visible: channeldetails.stateCode != ChannelDetails.Redeemed + && channeldetails.stateCode != ChannelDetails.Closed + && !channeldetails.isBackup capacity: channeldetails.capacity localCapacity: channeldetails.localCapacity remoteCapacity: channeldetails.remoteCapacity diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index d02e08b3c..bf54d0a76 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -107,7 +107,7 @@ ItemDelegate { ChannelBar { Layout.fillWidth: true - visible: !_closed + visible: !_closed && !model.is_backup capacity: model.capacity localCapacity: model.local_capacity remoteCapacity: model.remote_capacity diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 3c7fa0c87..572f076c8 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -99,6 +99,10 @@ def short_cid(self): def state(self): return self._channel.get_state_for_GUI() + @pyqtProperty(int, notify=channelChanged) + def stateCode(self): + return self._channel.get_state() + @pyqtProperty(str, notify=channelChanged) def initiator(self): if self._channel.is_backup(): From 73f89d516aff9579e4b3b7ad34c0fff2b707595d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 13:52:16 +0200 Subject: [PATCH 0716/1143] qml: don't determine channel state on gui string, use state enum instead --- electrum/gui/qml/components/ChannelDetails.qml | 2 +- electrum/gui/qml/components/controls/ChannelDelegate.qml | 2 +- electrum/gui/qml/qechanneldetails.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 3043cea1e..99965410f 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -72,7 +72,7 @@ Pane { Label { text: channeldetails.state - color: channeldetails.state == 'OPEN' + color: channeldetails.stateCode == ChannelDetails.Open ? constants.colorChannelOpen : Material.foreground } diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index bf54d0a76..af1fcce84 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -79,7 +79,7 @@ ItemDelegate { font.pixelSize: constants.fontSizeMedium color: _closed ? constants.mutedForeground - : model.state == 'OPEN' + : model.state_code == ChannelDetails.Open ? constants.colorChannelOpen : Material.foreground } diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 572f076c8..d24c9ec34 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -17,6 +17,7 @@ class QEChannelDetails(QObject, QtEventListener): _logger = get_logger(__name__) class State: # subset, only ones we currently need in UI + Open = ChannelState.OPEN Closed = ChannelState.CLOSED Redeemed = ChannelState.REDEEMED From 76786ab670c62ecf6f9c5072a2e523dccc1aa5a4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 13:56:40 +0200 Subject: [PATCH 0717/1143] qml: qechanneldetails fix remaining assignment --- electrum/gui/qml/qechanneldetails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index d24c9ec34..af25a7784 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -118,7 +118,7 @@ def capacity(self): @pyqtProperty(QEAmount, notify=channelChanged) def localCapacity(self): if not self._channel.is_backup(): - self._local_capacity = QEAmount(amount_msat=self._channel.balance(LOCAL)) + self._local_capacity.copyFrom(QEAmount(amount_msat=self._channel.balance(LOCAL))) return self._local_capacity @pyqtProperty(QEAmount, notify=channelChanged) From 99a78d4d6c98bbd4eaf4d6c05b9f9e21942e844c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 10:46:22 +0200 Subject: [PATCH 0718/1143] wallet: don't restart wallet/network when init_lightning --- electrum/wallet.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 68bb85223..ec75dab95 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -438,11 +438,9 @@ def init_lightning(self, *, password) -> None: node = BIP32Node.from_rootseed(seed, xtype='standard') ln_xprv = node.to_xprv() self.db.put('lightning_privkey2', ln_xprv) - if self.network: - self.network.run_from_another_thread(self.stop()) self.lnworker = LNWallet(self, ln_xprv) if self.network: - self.start_network(self.network) + self.start_network_lightning() async def stop(self): """Stop all networking and save DB to disk.""" @@ -539,10 +537,15 @@ def start_network(self, network): asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop) self.adb.start_network(network) if self.lnworker: - self.lnworker.start_network(network) - # only start gossiping when we already have channels - if self.db.get('channels'): - self.network.start_gossip() + self.start_network_lightning() + + def start_network_lightning(self): + assert self.lnworker + assert self.lnworker.network is None, 'lnworker network already initialized' + self.lnworker.start_network(self.network) + # only start gossiping when we already have channels + if self.db.get('channels'): + self.network.start_gossip() @abstractmethod def load_keystore(self) -> None: From 62af3265cb0f80bbedeeb3c3225aea5947274cb0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 15:35:39 +0200 Subject: [PATCH 0719/1143] qml: disable menu option associated with current page --- electrum/gui/qml/components/About.qml | 2 ++ electrum/gui/qml/components/Addresses.qml | 2 ++ electrum/gui/qml/components/Channels.qml | 2 ++ electrum/gui/qml/components/Preferences.qml | 1 + .../gui/qml/components/WalletMainView.qml | 25 ++++++++++--------- electrum/gui/qml/components/main.qml | 17 +++++++------ 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qml/components/About.qml b/electrum/gui/qml/components/About.qml index b66453432..18d5e0c9d 100644 --- a/electrum/gui/qml/components/About.qml +++ b/electrum/gui/qml/components/About.qml @@ -4,6 +4,8 @@ import QtQuick.Controls 2.0 import QtQuick.Controls.Material 2.0 Pane { + objectName: 'About' + property string title: qsTr("About Electrum") Flickable { diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index 280239334..3008c9612 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -9,6 +9,8 @@ import "controls" Pane { id: rootItem + objectName: 'Addresses' + padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 19a9ed915..7a67256be 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -9,6 +9,8 @@ import "controls" Pane { id: root + objectName: 'Channels' + padding: 0 ColumnLayout { diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index a2cbababc..c7ffe8cf3 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -9,6 +9,7 @@ import "controls" Pane { id: preferences + objectName: 'Properties' property string title: qsTr("Preferences") diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 3e0501977..8769c35e3 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -78,41 +78,42 @@ Item { id: menu MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/wallet.png' action: Action { text: qsTr('Wallet details') - enabled: Daemon.currentWallet + enabled: Daemon.currentWallet && app.stack.currentItem.objectName != 'WalletDetails' onTriggered: menu.openPage(Qt.resolvedUrl('WalletDetails.qml')) - icon.source: '../../icons/wallet.png' } } MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/tab_addresses.png' action: Action { text: qsTr('Addresses'); onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml')); - enabled: Daemon.currentWallet - icon.source: '../../icons/tab_addresses.png' + enabled: Daemon.currentWallet && app.stack.currentItem.objectName != 'Addresses' } } MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/lightning.png' action: Action { text: qsTr('Channels'); - enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning + enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning && app.stack.currentItem.objectName != 'Channels' onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml')) - icon.source: '../../icons/lightning.png' } } MenuSeparator { } MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/file.png' action: Action { - text: qsTr('Other wallets'); + text: qsTr('Other wallets') + enabled: app.stack.currentItem.objectName != 'Wallets' onTriggered: menu.openPage(Qt.resolvedUrl('Wallets.qml')) - icon.source: '../../icons/file.png' } } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 3a86199fb..de4f656dd 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -49,29 +49,32 @@ ApplicationWindow id: menu MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/network.png' action: Action { text: qsTr('Network') onTriggered: menu.openPage(Qt.resolvedUrl('NetworkOverview.qml')) - icon.source: '../../icons/network.png' + enabled: stack.currentItem.objectName != 'NetworkOverview' } } MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/preferences.png' action: Action { - text: qsTr('Preferences'); + text: qsTr('Preferences') onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml')) - icon.source: '../../icons/preferences.png' + enabled: stack.currentItem.objectName != 'Properties' } } MenuItem { - icon.color: 'transparent' + icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor + icon.source: '../../icons/electrum.png' action: Action { text: qsTr('About'); onTriggered: menu.openPage(Qt.resolvedUrl('About.qml')) - icon.source: '../../icons/electrum.png' + enabled: stack.currentItem.objectName != 'About' } } From 5b6a16e09739b2991249273e8fde827486ac9eeb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 17 Apr 2023 16:32:20 +0200 Subject: [PATCH 0720/1143] qml: add auth_message to AuthMixin, which is displayed above the Pin entry textfield, or shown in a messageDialog for confirmation. --- electrum/gui/qml/auth.py | 10 +++++++- electrum/gui/qml/components/Pin.qml | 37 ++++++++++++++-------------- electrum/gui/qml/components/main.qml | 30 +++++++++++++++------- electrum/gui/qml/qeswaphelper.py | 14 +++++------ 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py index 1d1e5104a..9aef53f0b 100644 --- a/electrum/gui/qml/auth.py +++ b/electrum/gui/qml/auth.py @@ -1,6 +1,6 @@ from functools import wraps, partial -from PyQt5.QtCore import pyqtSignal, pyqtSlot +from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty from electrum.logging import get_logger @@ -25,6 +25,12 @@ class AuthMixin: authRequired = pyqtSignal([str],arguments=['method']) + auth_message = '' + _authMixinMessageChanged = pyqtSignal() + @pyqtProperty(str, notify=_authMixinMessageChanged) + def authMessage(self): + return self.auth_message + @pyqtSlot() def authProceed(self): self._auth_logger.debug('Proceeding with authed fn()') @@ -38,6 +44,7 @@ def authProceed(self): raise e finally: delattr(self,'__auth_fcall') + self.auth_message = '' @pyqtSlot() def authCancel(self): @@ -57,3 +64,4 @@ def authCancel(self): raise e finally: delattr(self, '__auth_fcall') + self.auth_message = '' diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index fb9b9a62d..eb0e63c92 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -10,11 +10,21 @@ import "controls" ElDialog { id: root + property bool canCancel: true + property string mode // [check, enter, change] + property string pincode // old one passed in when change, new one passed out + property bool checkError: false + property string authMessage + property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin + property string _pin + title: qsTr('PIN') iconSource: '../../../icons/lock.png' - z: 1000 - width: parent.width * 3/4 + z: 1000 + focus: true + closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose + allowClose: canCancel anchors.centerIn: parent @@ -22,22 +32,6 @@ ElDialog { color: canCancel ? "#aa000000" : "#ff000000" } - focus: true - - closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose - - property bool canCancel: true - - allowClose: canCancel - - property string mode // [check, enter, change] - property string pincode // old one passed in when change, new one passed out - - property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin - property string _pin - - property bool checkError: false - function submit() { if (_phase == 0) { if (pin.text == pincode) { @@ -76,6 +70,13 @@ ElDialog { ColumnLayout { width: parent.width + Label { + Layout.fillWidth: true + visible: authMessage + text: authMessage + wrapMode: Text.Wrap + } + Label { text: [qsTr('Enter PIN'), qsTr('Enter New PIN'), qsTr('Re-enter New PIN')][_phase] font.pixelSize: constants.fontSizeXXLarge diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index de4f656dd..3d385cc70 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -371,13 +371,6 @@ ApplicationWindow swaphelper: SwapHelper { id: _swaphelper wallet: Daemon.currentWallet - onConfirm: { - var dialog = app.messageDialog.createObject(app, {text: message, yesno: true}) - dialog.accepted.connect(function() { - _swaphelper.executeSwap(true) - }) - dialog.open() - } onAuthRequired: { app.handleAuthRequired(_swaphelper, method) } @@ -558,9 +551,13 @@ ApplicationWindow } else if (method == 'pin') { if (Config.pinCode == '') { // no PIN configured - qtobject.authProceed() + handleAuthConfirmationOnly(qtobject) } else { - var dialog = app.pinDialog.createObject(app, {mode: 'check', pincode: Config.pinCode}) + var dialog = app.pinDialog.createObject(app, { + mode: 'check', + pincode: Config.pinCode, + authMessage: qtobject.authMessage + }) dialog.accepted.connect(function() { qtobject.authProceed() dialog.close() @@ -576,6 +573,21 @@ ApplicationWindow } } + function handleAuthConfirmationOnly(qtobject) { + if (!qtobject.authMessage) { + qtobject.authProceed() + return + } + var dialog = app.messageDialog.createObject(app, {text: qtobject.authMessage, yesno: true}) + dialog.accepted.connect(function() { + qtobject.authProceed() + }) + dialog.rejected.connect(function() { + qtobject.authCancel() + }) + dialog.open() + } + function startSwap() { var swapdialog = swapDialog.createObject(app) swapdialog.open() diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 45ef169f4..38423ee05 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -419,21 +419,19 @@ def swap_task(): threading.Thread(target=swap_task, daemon=True).start() @pyqtSlot() - @pyqtSlot(bool) - def executeSwap(self, confirm=False): + def executeSwap(self): if not self._wallet.wallet.network: self.error.emit(_("You are offline.")) return - if confirm or self._wallet.wallet.config.get('pin_code', ''): - self._do_execute_swap() - return if self.isReverse: - self.confirm.emit(_('Do you want to do a reverse submarine swap?')) + self.auth_message = _('Do you want to do a reverse submarine swap?') else: - self.confirm.emit(_('Do you want to do a submarine swap? ' + self.auth_message = _('Do you want to do a submarine swap? ' 'You will need to wait for the swap transaction to confirm.' - )) + ) + + self._do_execute_swap() @auth_protect def _do_execute_swap(self): From 85291b2de37510dea773f45ebc04863659131b3c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Apr 2023 16:54:26 +0200 Subject: [PATCH 0721/1143] follow-up 5b6a16e09739b2991249273e8fde827486ac9eeb --- electrum/gui/qml/qeinvoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index dc0225c7e..be5e16a63 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -383,6 +383,7 @@ def pay_lightning_invoice(self): # TODO: is update amount_msat for overrideAmount sufficient? self._effectiveInvoice.amount_msat = self.amountOverride.satsInt * 1000 + self._wallet.auth_message = _('Pay Lightning Invoice?') self._wallet.pay_lightning_invoice(self._effectiveInvoice) def get_max_spendable_onchain(self): From e43006335113a8b325303fe151e370bfbe159e53 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Apr 2023 17:08:11 +0200 Subject: [PATCH 0722/1143] PIN dialog: display auth message as title, if available --- electrum/gui/qml/components/Pin.qml | 9 +-------- electrum/gui/qml/components/controls/ElDialog.qml | 1 + 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/electrum/gui/qml/components/Pin.qml b/electrum/gui/qml/components/Pin.qml index eb0e63c92..591e4795a 100644 --- a/electrum/gui/qml/components/Pin.qml +++ b/electrum/gui/qml/components/Pin.qml @@ -18,7 +18,7 @@ ElDialog { property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin property string _pin - title: qsTr('PIN') + title: authMessage ? authMessage : qsTr('PIN') iconSource: '../../../icons/lock.png' width: parent.width * 3/4 z: 1000 @@ -70,13 +70,6 @@ ElDialog { ColumnLayout { width: parent.width - Label { - Layout.fillWidth: true - visible: authMessage - text: authMessage - wrapMode: Text.Wrap - } - Label { text: [qsTr('Enter PIN'), qsTr('Enter New PIN'), qsTr('Re-enter New PIN')][_phase] font.pixelSize: constants.fontSizeXXLarge diff --git a/electrum/gui/qml/components/controls/ElDialog.qml b/electrum/gui/qml/components/controls/ElDialog.qml index ebc133919..82da7759c 100644 --- a/electrum/gui/qml/components/controls/ElDialog.qml +++ b/electrum/gui/qml/components/controls/ElDialog.qml @@ -73,6 +73,7 @@ Dialog { Label { text: title + wrapMode: Text.Wrap elide: Label.ElideRight Layout.fillWidth: true leftPadding: constants.paddingXLarge From 1421df57ad8419a1488584d9ce67458fb091cf64 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Apr 2023 17:14:23 +0200 Subject: [PATCH 0723/1143] add auth message for open_channel --- electrum/gui/qml/qechannelopener.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 5abf5d627..9f97553ae 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -165,6 +165,7 @@ def open_channel(self, confirm_backup_conflict=False): node_id=self._node_pubkey, fee_est=None) + self.auth_message = _('Open Lightning channel?') acpt = lambda tx: self.do_open_channel(tx, self._connect_str_resolved, self._wallet.password) self._finalizer = QETxFinalizer(self, make_tx=mktx, accept=acpt) From 73dd6827e0c58b9bc56fe0c9518f531aebfcf48c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Apr 2023 17:54:55 +0200 Subject: [PATCH 0724/1143] add auth_message to delete_wallet This changes the flow slightly: pin confirmation is asked after we have checked that the wallet does not have open channels, which is better --- electrum/gui/qml/components/WalletDetails.qml | 11 +---------- electrum/gui/qml/qedaemon.py | 1 + 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 290b25bf0..f33fdf640 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -24,15 +24,6 @@ Pane { dialog.open() } - function deleteWallet() { - var dialog = app.messageDialog.createObject(rootItem, - {'text': qsTr('Really delete this wallet?'), 'yesno': true}) - dialog.accepted.connect(function() { - Daemon.checkThenDeleteWallet(Daemon.currentWallet) - }) - dialog.open() - } - function changePassword() { // trigger dialog via wallet (auth then signal) Daemon.startChangePassword() @@ -418,7 +409,7 @@ Pane { Layout.fillWidth: true Layout.preferredWidth: 1 text: qsTr('Delete Wallet') - onClicked: rootItem.deleteWallet() + onClicked: Daemon.checkThenDeleteWallet(Daemon.currentWallet) icon.source: '../../icons/delete.png' } FlatButton { diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 0dbfff7c8..25876a918 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -261,6 +261,7 @@ def checkThenDeleteWallet(self, wallet, confirm_requests=False, confirm_balance= self.walletDeleteError.emit('balance', _('There are still coins present in this wallet. Really delete?')) return + self.auth_message = _('Really delete this wallet?') self.delete_wallet(wallet) @auth_protect From a03f4769caa482ed45614e1ea523703f6e6082cb Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Apr 2023 18:17:29 +0200 Subject: [PATCH 0725/1143] auth_protect: pass authMessage in the auth_protect decorator, instead of relying on side-effects This is probably safer, and also more self-contained. --- electrum/gui/qml/auth.py | 40 ++++++++++++---------------- electrum/gui/qml/components/main.qml | 26 +++++++++--------- electrum/gui/qml/qechannelopener.py | 3 +-- electrum/gui/qml/qedaemon.py | 3 +-- electrum/gui/qml/qeinvoice.py | 1 - electrum/gui/qml/qeswaphelper.py | 10 +------ electrum/gui/qml/qewallet.py | 2 +- 7 files changed, 34 insertions(+), 51 deletions(-) diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py index 9aef53f0b..49c68542a 100644 --- a/electrum/gui/qml/auth.py +++ b/electrum/gui/qml/auth.py @@ -4,32 +4,28 @@ from electrum.logging import get_logger -def auth_protect(func=None, reject=None, method='pin'): - if func is None: - return partial(auth_protect, reject=reject, method=method) +def auth_protect(message='', method='pin', reject=None): - @wraps(func) - def wrapper(self, *args, **kwargs): - _logger = get_logger(__name__) - _logger.debug(f'{str(self)}.{func.__name__}') - if hasattr(self, '__auth_fcall'): - _logger.debug('object already has a pending authed function call') - raise Exception('object already has a pending authed function call') - setattr(self, '__auth_fcall', (func,args,kwargs,reject)) - getattr(self, 'authRequired').emit(method) + def decorator(func=None): + if func is None: + return partial(auth_protect, reject=reject, method=method) - return wrapper + @wraps(func) + def wrapper(self, *args, **kwargs): + _logger = get_logger(__name__) + _logger.debug(f'{str(self)}.{func.__name__}') + if hasattr(self, '__auth_fcall'): + _logger.debug('object already has a pending authed function call') + raise Exception('object already has a pending authed function call') + setattr(self, '__auth_fcall', (func,args,kwargs,reject)) + getattr(self, 'authRequired').emit(method, message) + + return wrapper + return decorator class AuthMixin: _auth_logger = get_logger(__name__) - - authRequired = pyqtSignal([str],arguments=['method']) - - auth_message = '' - _authMixinMessageChanged = pyqtSignal() - @pyqtProperty(str, notify=_authMixinMessageChanged) - def authMessage(self): - return self.auth_message + authRequired = pyqtSignal([str, str], arguments=['method', 'authMessage']) @pyqtSlot() def authProceed(self): @@ -44,7 +40,6 @@ def authProceed(self): raise e finally: delattr(self,'__auth_fcall') - self.auth_message = '' @pyqtSlot() def authCancel(self): @@ -64,4 +59,3 @@ def authCancel(self): raise e finally: delattr(self, '__auth_fcall') - self.auth_message = '' diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 3d385cc70..cbece771b 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -372,7 +372,7 @@ ApplicationWindow id: _swaphelper wallet: Daemon.currentWallet onAuthRequired: { - app.handleAuthRequired(_swaphelper, method) + app.handleAuthRequired(_swaphelper, method, authMessage) } onError: { var dialog = app.messageDialog.createObject(app, { text: message }) @@ -477,8 +477,8 @@ ApplicationWindow var dialog = app.messageDialog.createObject(app, {'text': error}) dialog.open() } - function onAuthRequired(method) { - handleAuthRequired(Daemon, method) + function onAuthRequired(method, authMessage) { + handleAuthRequired(Daemon, method, authMessage) } function onLoadingChanged() { if (!Daemon.loading) @@ -509,8 +509,8 @@ ApplicationWindow Connections { target: Daemon.currentWallet - function onAuthRequired(method) { - handleAuthRequired(Daemon.currentWallet, method) + function onAuthRequired(method, authMessage) { + handleAuthRequired(Daemon.currentWallet, method, authMessage) } // TODO: add to notification queue instead of barging through function onPaymentSucceeded(key) { @@ -523,12 +523,12 @@ ApplicationWindow Connections { target: Config - function onAuthRequired(method) { - handleAuthRequired(Config, method) + function onAuthRequired(method, authMessage) { + handleAuthRequired(Config, method, authMessage) } } - function handleAuthRequired(qtobject, method) { + function handleAuthRequired(qtobject, method, authMessage) { console.log('auth using method ' + method) if (method == 'wallet') { if (Daemon.currentWallet.verify_password('')) { @@ -551,12 +551,12 @@ ApplicationWindow } else if (method == 'pin') { if (Config.pinCode == '') { // no PIN configured - handleAuthConfirmationOnly(qtobject) + handleAuthConfirmationOnly(qtobject, authMessage) } else { var dialog = app.pinDialog.createObject(app, { mode: 'check', pincode: Config.pinCode, - authMessage: qtobject.authMessage + authMessage: authMessage }) dialog.accepted.connect(function() { qtobject.authProceed() @@ -573,12 +573,12 @@ ApplicationWindow } } - function handleAuthConfirmationOnly(qtobject) { - if (!qtobject.authMessage) { + function handleAuthConfirmationOnly(qtobject, authMessage) { + if (!authMessage) { qtobject.authProceed() return } - var dialog = app.messageDialog.createObject(app, {text: qtobject.authMessage, yesno: true}) + var dialog = app.messageDialog.createObject(app, {text: authMessage, yesno: true}) dialog.accepted.connect(function() { qtobject.authProceed() }) diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 9f97553ae..578929ecc 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -165,7 +165,6 @@ def open_channel(self, confirm_backup_conflict=False): node_id=self._node_pubkey, fee_est=None) - self.auth_message = _('Open Lightning channel?') acpt = lambda tx: self.do_open_channel(tx, self._connect_str_resolved, self._wallet.password) self._finalizer = QETxFinalizer(self, make_tx=mktx, accept=acpt) @@ -174,7 +173,7 @@ def open_channel(self, confirm_backup_conflict=False): self._finalizer.wallet = self._wallet self.finalizerChanged.emit() - @auth_protect + @auth_protect(message=_('Open Lichtning channel?')) def do_open_channel(self, funding_tx, conn_str, password): """ conn_str: a connection string that extract_nodeid can parse, i.e. cannot be a trampoline name diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 25876a918..10645328d 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -261,10 +261,9 @@ def checkThenDeleteWallet(self, wallet, confirm_requests=False, confirm_balance= self.walletDeleteError.emit('balance', _('There are still coins present in this wallet. Really delete?')) return - self.auth_message = _('Really delete this wallet?') self.delete_wallet(wallet) - @auth_protect + @auth_protect(message=_('Really delete this wallet?')) def delete_wallet(self, wallet): path = standardize_path(wallet.wallet.storage.path) self._logger.debug('deleting wallet with path %s' % path) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index be5e16a63..dc0225c7e 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -383,7 +383,6 @@ def pay_lightning_invoice(self): # TODO: is update amount_msat for overrideAmount sufficient? self._effectiveInvoice.amount_msat = self.amountOverride.satsInt * 1000 - self._wallet.auth_message = _('Pay Lightning Invoice?') self._wallet.pay_lightning_invoice(self._effectiveInvoice) def get_max_spendable_onchain(self): diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 38423ee05..590e78b67 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -423,17 +423,9 @@ def executeSwap(self): if not self._wallet.wallet.network: self.error.emit(_("You are offline.")) return - - if self.isReverse: - self.auth_message = _('Do you want to do a reverse submarine swap?') - else: - self.auth_message = _('Do you want to do a submarine swap? ' - 'You will need to wait for the swap transaction to confirm.' - ) - self._do_execute_swap() - @auth_protect + @auth_protect(message=_('Confirm Lightning swap?')) def _do_execute_swap(self): if self.isReverse: lightning_amount = self._send_amount diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index a6e6ca346..792a536a4 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -584,7 +584,7 @@ def save_tx(self, tx: 'PartialTransaction'): def ln_auth_rejected(self): self.paymentAuthRejected.emit() - @auth_protect(reject='ln_auth_rejected') + @auth_protect(message=_('Pay lightning invoice?'), reject='ln_auth_rejected') def pay_lightning_invoice(self, invoice: 'QEInvoice'): amount_msat = invoice.get_amount_msat() From 6ac3f84095f1b0c3d501a45c25633e2e3ab33a40 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Apr 2023 16:56:57 +0000 Subject: [PATCH 0726/1143] wallet/lnworker/etc: add sanity checks to start_network methods related: https://github.com/spesmilo/electrum/issues/8301 --- electrum/address_synchronizer.py | 1 + electrum/lnworker.py | 1 + electrum/submarine_swaps.py | 1 + electrum/wallet.py | 9 +++++---- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index cea1ed0f3..3e490f8f4 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -182,6 +182,7 @@ def load_unverified_transactions(self): self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height) def start_network(self, network: Optional['Network']) -> None: + assert self.network is None, "already started" self.network = network if self.network is not None: self.synchronizer = Synchronizer(self) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 37aa6c98a..ffc56c74f 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -338,6 +338,7 @@ def num_peers(self) -> int: def start_network(self, network: 'Network'): assert network + assert self.network is None, "already started" self.network = network self.config = network.config self._add_peers_from_config() diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index d1c653ca3..b0af03fd4 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -170,6 +170,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): def start_network(self, *, network: 'Network', lnwatcher: 'LNWalletWatcher'): assert network assert lnwatcher + assert self.network is None, "already started" self.network = network self.lnwatcher = lnwatcher for k, swap in self.swaps.items(): diff --git a/electrum/wallet.py b/electrum/wallet.py index ec75dab95..302726f4b 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -440,7 +440,7 @@ def init_lightning(self, *, password) -> None: self.db.put('lightning_privkey2', ln_xprv) self.lnworker = LNWallet(self, ln_xprv) if self.network: - self.start_network_lightning() + self._start_network_lightning() async def stop(self): """Stop all networking and save DB to disk.""" @@ -531,15 +531,16 @@ def clear_history(self): self.adb.clear_history() self.save_db() - def start_network(self, network): + def start_network(self, network: 'Network'): + assert self.network is None, "already started" self.network = network if network: asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop) self.adb.start_network(network) if self.lnworker: - self.start_network_lightning() + self._start_network_lightning() - def start_network_lightning(self): + def _start_network_lightning(self): assert self.lnworker assert self.lnworker.network is None, 'lnworker network already initialized' self.lnworker.start_network(self.network) From b42bb84b5cc5eee812148155ceac3d9b4d2ae238 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Apr 2023 19:04:09 +0200 Subject: [PATCH 0727/1143] fix typo --- electrum/gui/qml/qechannelopener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 578929ecc..16433258d 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -173,7 +173,7 @@ def open_channel(self, confirm_backup_conflict=False): self._finalizer.wallet = self._wallet self.finalizerChanged.emit() - @auth_protect(message=_('Open Lichtning channel?')) + @auth_protect(message=_('Open Lightning channel?')) def do_open_channel(self, funding_tx, conn_str, password): """ conn_str: a connection string that extract_nodeid can parse, i.e. cannot be a trampoline name From e3544f260ba261606b2db0cc73c7fb51c628d62a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Apr 2023 18:26:29 +0000 Subject: [PATCH 0728/1143] i18n: log set_language --- electrum/i18n.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/electrum/i18n.py b/electrum/i18n.py index a98ea1d39..8aadbdae8 100644 --- a/electrum/i18n.py +++ b/electrum/i18n.py @@ -23,9 +23,14 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os +from typing import Optional import gettext +from .logging import get_logger + + +_logger = get_logger(__name__) LOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale') language = gettext.translation('electrum', LOCALE_DIR, fallback=True) @@ -45,7 +50,8 @@ def _(x: str) -> str: return language.gettext(x) -def set_language(x): +def set_language(x: Optional[str]) -> None: + _logger.info(f"setting language to {x!r}") global language if x: language = gettext.translation('electrum', LOCALE_DIR, fallback=True, languages=[x]) From 27ce8ba241f88f058aa408a308d3542187af5ea6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Apr 2023 19:19:13 +0000 Subject: [PATCH 0729/1143] i18n: log initial default language set based on OS locale --- electrum/i18n.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/electrum/i18n.py b/electrum/i18n.py index 8aadbdae8..23bdda1c9 100644 --- a/electrum/i18n.py +++ b/electrum/i18n.py @@ -32,7 +32,17 @@ _logger = get_logger(__name__) LOCALE_DIR = os.path.join(os.path.dirname(__file__), 'locale') + +# set initial default language, based on OS-locale +# FIXME some module-level strings might get translated using this language, before +# any user-provided custom language (in config) can get set. language = gettext.translation('electrum', LOCALE_DIR, fallback=True) +try: + _lang = language.info().get('language', None) +except Exception as e: + _logger.info(f"gettext setting initial language to ?? (error: {e!r})") +else: + _logger.info(f"gettext setting initial language to {_lang!r}") # note: do not use old-style (%) formatting inside translations, From 5c83327eb00a64f2839260fb31613201220e7872 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Apr 2023 19:52:54 +0000 Subject: [PATCH 0730/1143] qml: small change to some qsTr() strings to reuse existing translations not all ":" suffixes are removed, only the ones where this allows reusing translations --- electrum/gui/qml/components/Channels.qml | 4 ++-- electrum/gui/qml/components/NetworkOverview.qml | 10 +++++----- .../gui/qml/components/controls/BalanceSummary.qml | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index 7a67256be..b2a8f5628 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -40,7 +40,7 @@ Pane { } Label { - text: qsTr('You can send:') + text: qsTr('You can send') + ':' color: Material.accentColor } @@ -49,7 +49,7 @@ Pane { } Label { - text: qsTr('You can receive:') + text: qsTr('You can receive') + ':' color: Material.accentColor } diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index f1339dc1f..41fdc0984 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -37,21 +37,21 @@ Pane { text: qsTr('On-chain') } Label { - text: qsTr('Network:'); + text: qsTr('Network') + ':' color: Material.accentColor } Label { text: Network.networkName } Label { - text: qsTr('Status:'); + text: qsTr('Status') + ':' color: Material.accentColor } Label { text: Network.status } Label { - text: qsTr('Server:'); + text: qsTr('Server') + ':' color: Material.accentColor } Label { @@ -165,7 +165,7 @@ Pane { } Label { - text: Config.useGossip ? qsTr('Gossip:') : qsTr('Trampoline:') + text: (Config.useGossip ? qsTr('Gossip') : qsTr('Trampoline')) + ':' color: Material.accentColor } ColumnLayout { @@ -201,7 +201,7 @@ Pane { } Label { - text: qsTr('Proxy:'); + text: qsTr('Proxy') + ':' color: Material.accentColor } Label { diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index e02ef92fe..15e3ace0d 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -35,7 +35,7 @@ Item { Label { font.pixelSize: constants.fontSizeXLarge - text: qsTr('Balance:') + text: qsTr('Balance') + ':' color: Material.accentColor } @@ -79,7 +79,7 @@ Item { source: '../../../icons/lightning.png' } Label { - text: qsTr('Lightning:') + text: qsTr('Lightning') + ':' font.pixelSize: constants.fontSizeSmall color: Material.accentColor } @@ -106,7 +106,7 @@ Item { source: '../../../icons/bitcoin.png' } Label { - text: qsTr('On-chain:') + text: qsTr('On-chain') + ':' font.pixelSize: constants.fontSizeSmall color: Material.accentColor } From a6c36b8588796f5295b793da321c4a24950d6c29 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Apr 2023 02:02:07 +0000 Subject: [PATCH 0731/1143] regtests: test_watchtower started failing due to newly exposed bug local_watchtower.adb.start_network was getting called twice. follow-up 6ac3f84095f1b0c3d501a45c25633e2e3ab33a40 ``` 20230418T014725.636141Z | ERROR | __main__ | Traceback (most recent call last): File "/home/user/wspace/electrum/./run_electrum", line 435, in main handle_cmd( File "/home/user/wspace/electrum/./run_electrum", line 469, in handle_cmd d = daemon.Daemon(config, fd) File "/home/user/wspace/electrum/electrum/util.py", line 462, in return lambda *args, **kw_args: do_profile(args, kw_args) File "/home/user/wspace/electrum/electrum/util.py", line 458, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/daemon.py", line 404, in __init__ self.network = Network(config, daemon=self) File "/home/user/wspace/electrum/electrum/network.py", line 348, in __init__ self.local_watchtower.adb.start_network(self) File "/home/user/wspace/electrum/electrum/address_synchronizer.py", line 185, in start_network assert self.network is None, "already started" AssertionError: already started ``` --- electrum/lnwatcher.py | 2 +- electrum/network.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index deeb46492..020407878 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -324,7 +324,7 @@ class WatchTower(LNWatcher): LOGGING_SHORTCUT = 'W' - def __init__(self, network): + def __init__(self, network: 'Network'): adb = AddressSynchronizer(WalletDB({}, manual_upgrades=False), network.config, name=self.diagnostic_name()) adb.start_network(network) LNWatcher.__init__(self, adb, network) diff --git a/electrum/network.py b/electrum/network.py index b288a5578..e77836a95 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -345,7 +345,6 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): if self.config.get('run_watchtower', False): from . import lnwatcher self.local_watchtower = lnwatcher.WatchTower(self) - self.local_watchtower.adb.start_network(self) asyncio.ensure_future(self.local_watchtower.start_watching()) def has_internet_connection(self) -> bool: From dde609872dfd95339719ba54e330a3dc5282aa84 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 10:16:58 +0200 Subject: [PATCH 0732/1143] follow-up a03f4769caa482ed45614e1ea523703f6e6082cb It is annoying that this kind of bug (missing parameter) is silent. --- electrum/gui/qml/components/OpenChannelDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 8fa5585ba..78f3ca195 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -211,7 +211,7 @@ ElDialog { id: channelopener wallet: Daemon.currentWallet onAuthRequired: { - app.handleAuthRequired(channelopener, method) + app.handleAuthRequired(channelopener, method, authMessage) } onValidationError: { if (code == 'invalid_nodeid') { From 86aca1e24e120713a3e80a0753905ff5ca85198c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 10:20:49 +0200 Subject: [PATCH 0733/1143] qml: change title of ConfirmTxDialog From a user perspectuve, what they are asked to do is select the mining fee. --- electrum/gui/qml/components/ConfirmTxDialog.qml | 2 +- electrum/gui/qml/components/OpenChannelDialog.qml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 5910bc1cf..e0a630cb4 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -17,7 +17,7 @@ ElDialog { property alias amountLabelText: amountLabel.text property alias sendButtonText: sendButton.text - title: qsTr('Confirm Transaction') + title: qsTr('Transaction Fee') // copy these to finalizer onAddressChanged: finalizer.address = address diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 78f3ca195..a14a3da2c 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -200,7 +200,6 @@ ElDialog { Component { id: confirmOpenChannelDialog ConfirmTxDialog { - title: qsTr('Confirm Open Channel') amountLabelText: qsTr('Channel capacity') sendButtonText: qsTr('Open Channel') finalizer: channelopener.finalizer From f513ba465429a92daedec8848f2ad009e18a0189 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 10:32:07 +0200 Subject: [PATCH 0734/1143] messageDialog: change title to Question for yesno dialogs --- electrum/gui/qml/components/MessageDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 285270254..1fcc74f30 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -7,7 +7,7 @@ import "controls" ElDialog { id: dialog - title: qsTr("Message") + title: yesno ? qsTr("Question") : qsTr("Message") iconSource: yesno ? Qt.resolvedUrl('../../icons/question.png') : Qt.resolvedUrl('../../icons/info.png') From 6bdda957c0b6e9b5479694c689330cd51bd0a874 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 12:00:14 +0200 Subject: [PATCH 0735/1143] qml: reformulate swap success message --- electrum/gui/qml/qeswaphelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 590e78b67..b5ce15aa3 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -400,7 +400,7 @@ def swap_task(): _('Success!'), _('The funding transaction has been detected.'), _('Your claiming transaction will be broadcast when the funding transaction is confirmed.'), - _('You may broadcast it before that manually, but this is not trustless.'), + _('You may choose to broadcast it earlier, although that would not be trustless.'), ]) self.state = QESwapHelper.State.Success else: From 9b98d762ee21cae24719a45ae78ddd4d9a0c4680 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 12:49:05 +0200 Subject: [PATCH 0736/1143] update locale submodule --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 14bd5cb70..1131e5258 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 14bd5cb703cc99b87c70346656ca729f8e8a2df4 +Subproject commit 1131e525808c36d2b8300463e129830984049ec7 From 03d64aa665adff0d381382f25ebaaef27291ec52 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 12:56:45 +0200 Subject: [PATCH 0737/1143] qt: use same icon as qml for swap dialog --- electrum/gui/qt/channels_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index 2df20b104..d81bb9d56 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -360,7 +360,7 @@ def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') self.can_send_label = toolbar.itemAt(0).widget() menu.addAction(_('Rebalance channels'), lambda: self.on_rebalance()) - menu.addAction(_('Submarine swap'), lambda: self.main_window.run_swap_dialog()) + menu.addAction(read_QIcon('update.png'), _('Submarine swap'), lambda: self.main_window.run_swap_dialog()) menu.addSeparator() menu.addAction(_("Import channel backup"), lambda: self.main_window.do_process_from_text_channel_backup()) self.new_channel_button = EnterButton(_('New Channel'), self.main_window.new_channel_dialog) From f68d91e988a205aa544b025b7a99f21d6a4f224c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 18 Apr 2023 13:02:52 +0200 Subject: [PATCH 0738/1143] update release notes (qml) --- RELEASE-NOTES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6da49eedc..c98f3d50b 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,5 +1,11 @@ # Release 4.4 (not released yet) * New Android app, using QML instead of Kivy + - Using Qt 5.15.7, PyQt 5.15.9 + - This release still on python3.8 + - Feature parity with Kivy + - Note: two topbar menus; tap wallet name for wallet menu, tap + network orb for application menu + - Note: long-press Receive/Send for list of payment requests/invoices * Qt GUI improvements - New onchain tx creation flow - Advanced options have been moved to toolbars, where their effect From bea41d2098d9a377bb2071868c281ee0d0320811 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 18 Apr 2023 13:05:31 +0200 Subject: [PATCH 0739/1143] release notes (qml) --- RELEASE-NOTES | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index c98f3d50b..555eb291c 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -3,6 +3,7 @@ - Using Qt 5.15.7, PyQt 5.15.9 - This release still on python3.8 - Feature parity with Kivy + - Android Back button used throughout, for cancel/close/back - Note: two topbar menus; tap wallet name for wallet menu, tap network orb for application menu - Note: long-press Receive/Send for list of payment requests/invoices From 5e29b945612b0cfea1fea0a47edd2d01f0d4964d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 14:07:41 +0200 Subject: [PATCH 0740/1143] qml MessageDialog: split messages into title and header message. hide header if it is empty. --- electrum/gui/qml/components/ChannelBackups.qml | 2 +- electrum/gui/qml/components/ChannelDetails.qml | 5 ++--- electrum/gui/qml/components/Channels.qml | 2 +- electrum/gui/qml/components/MessageDialog.qml | 3 ++- electrum/gui/qml/components/OpenChannelDialog.qml | 2 +- electrum/gui/qml/components/Preferences.qml | 6 ++++-- electrum/gui/qml/components/TxDetails.qml | 13 ++++++++----- electrum/gui/qml/components/WalletDetails.qml | 8 ++++---- electrum/gui/qml/components/WalletMainView.qml | 4 ++-- electrum/gui/qml/components/main.qml | 6 +++--- 10 files changed, 28 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qml/components/ChannelBackups.qml b/electrum/gui/qml/components/ChannelBackups.qml index 496509bee..73003a77a 100644 --- a/electrum/gui/qml/components/ChannelBackups.qml +++ b/electrum/gui/qml/components/ChannelBackups.qml @@ -93,7 +93,7 @@ Pane { Connections { target: Daemon.currentWallet function onImportChannelBackupFailed(message) { - var dialog = app.messageDialog.createObject(root, { text: message }) + var dialog = app.messageDialog.createObject(root, { title: qstr('Error'), text: message }) dialog.open() } } diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 99965410f..d1f11c090 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -283,9 +283,8 @@ Pane { visible: channeldetails.canDelete onClicked: { var dialog = app.messageDialog.createObject(root, { - text: channeldetails.isBackup - ? qsTr('Are you sure you want to delete this channel backup?') - : qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), + title: qsTr('Are you sure?'), + text: channeldetails.isBackup ? '' : qsTr('This will purge associated transactions from your wallet history.'), yesno: true }) dialog.accepted.connect(function() { diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index b2a8f5628..dbe3d7888 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -163,7 +163,7 @@ Pane { Connections { target: Daemon.currentWallet function onImportChannelBackupFailed(message) { - var dialog = app.messageDialog.createObject(root, { text: message }) + var dialog = app.messageDialog.createObject(root, { title: qsTr('Error'), text: message }) dialog.open() } } diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 1fcc74f30..06a6f0a0f 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -24,6 +24,7 @@ ElDialog { ColumnLayout { ColumnLayout { + visible: text Layout.margins: constants.paddingMedium Layout.alignment: Qt.AlignHCenter TextArea { @@ -39,7 +40,7 @@ ElDialog { } ButtonContainer { - Layout.fillWidth: true + Layout.preferredWidth: dialog.parent.width * 2/3 FlatButton { Layout.fillWidth: true diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index a14a3da2c..279e11792 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -214,7 +214,7 @@ ElDialog { } onValidationError: { if (code == 'invalid_nodeid') { - var dialog = app.messageDialog.createObject(app, { 'text': message }) + var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), 'text': message }) dialog.open() } } diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index c7ffe8cf3..c2cba8dd7 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -256,7 +256,8 @@ Pane { if (activeFocus) { if (!checked) { var dialog = app.messageDialog.createObject(app, { - text: qsTr('Using plain gossip mode is not recommended on mobile. Are you sure?'), + title: qsTr('Are you sure?'), + text: qsTr('Electrum will have to download the Lightning Network graph, which is not recommended on mobile.'), yesno: true }) dialog.accepted.connect(function() { @@ -291,7 +292,8 @@ Pane { if (activeFocus) { if (!checked) { var dialog = app.messageDialog.createObject(app, { - text: qsTr('Are you sure? This option allows you to recover your lightning funds if you lose your device, or if you uninstall this app while lightning channels are active. Do not disable it unless you know how to recover channels from backups.'), + title: qsTr('Are you sure?'), + text: qsTr('This option allows you to recover your lightning funds if you lose your device, or if you uninstall this app while lightning channels are active. Do not disable it unless you know how to recover channels from backups.'), yesno: true }) dialog.accepted.connect(function() { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 723d5e395..1435cf370 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -414,8 +414,8 @@ Pane { if (txid != txdetails.txid) return var dialog = app.messageDialog.createObject(app, { - text: qsTr('Transaction added to wallet history.') + '\n\n' + - qsTr('Note: this is an offline transaction, if you want the network to see it, you need to broadcast it.') + title: qsTr('Transaction added to wallet history.'), + text: qsTr('Note: this is an offline transaction, if you want the network to see it, you need to broadcast it.') }) dialog.open() root.close() @@ -446,7 +446,8 @@ Pane { txdetails.sign_and_broadcast() } else { var dialog = app.messageDialog.createObject(app, { - text: qsTr('Transaction fee updated.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') + title: qsTr('Transaction fee updated.'), + text: qsTr('You still need to sign and broadcast this transaction.') }) dialog.open() } @@ -472,7 +473,8 @@ Pane { txdetails.sign_and_broadcast() } else { var dialog = app.messageDialog.createObject(app, { - text: qsTr('CPFP fee bump transaction created.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') + title: qsTr('CPFP fee bump transaction created.'), + text: qsTr('You still need to sign and broadcast this transaction.') }) dialog.open() } @@ -497,7 +499,8 @@ Pane { txdetails.sign_and_broadcast() } else { var dialog = app.messageDialog.createObject(app, { - text: qsTr('Cancel transaction created.') + '\n\n' + qsTr('You still need to sign and broadcast this transaction.') + title: qsTr('Cancel transaction created.'), + text: qsTr('You still need to sign and broadcast this transaction.') }) dialog.open() } diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index f33fdf640..6d32a06e0 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -17,7 +17,7 @@ Pane { function enableLightning() { var dialog = app.messageDialog.createObject(rootItem, - {'text': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) + {'title': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) dialog.accepted.connect(function() { Daemon.currentWallet.enableLightning() }) @@ -466,19 +466,19 @@ Pane { } function onWalletDeleteError(code, message) { if (code == 'unpaid_requests') { - var dialog = app.messageDialog.createObject(app, {text: message, yesno: true }) + var dialog = app.messageDialog.createObject(app, {title: qsTr('Error'), text: message, yesno: true }) dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet, true) }) dialog.open() } else if (code == 'balance') { - var dialog = app.messageDialog.createObject(app, {text: message, yesno: true }) + var dialog = app.messageDialog.createObject(app, {title: qsTr('Error'), text: message, yesno: true }) dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet, true, true) }) dialog.open() } else { - var dialog = app.messageDialog.createObject(app, {text: message }) + var dialog = app.messageDialog.createObject(app, {title: qsTr('Error'), text: message }) dialog.open() } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 8769c35e3..0940365b9 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -252,7 +252,7 @@ Item { dialog.open() } onLnurlError: { - var dialog = app.messageDialog.createObject(app, { text: message }) + var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), text: message }) dialog.open() } } @@ -370,7 +370,7 @@ Item { } onChannelBackupFound: { var dialog = app.messageDialog.createObject(app, { - text: qsTr('Import Channel backup?'), + title: qsTr('Import Channel backup?'), yesno: true }) dialog.accepted.connect(function() { diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index cbece771b..15eb7ed0f 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -375,7 +375,7 @@ ApplicationWindow app.handleAuthRequired(_swaphelper, method, authMessage) } onError: { - var dialog = app.messageDialog.createObject(app, { text: message }) + var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), text: message }) dialog.open() } } @@ -452,7 +452,7 @@ ApplicationWindow mainStackView.clear() } else { var dialog = app.messageDialog.createObject(app, { - text: qsTr('Close Electrum?'), + title: qsTr('Close Electrum?'), yesno: true }) dialog.accepted.connect(function() { @@ -578,7 +578,7 @@ ApplicationWindow qtobject.authProceed() return } - var dialog = app.messageDialog.createObject(app, {text: authMessage, yesno: true}) + var dialog = app.messageDialog.createObject(app, {title: authMessage, yesno: true}) dialog.accepted.connect(function() { qtobject.authProceed() }) From 9dbf354bf24f3c36ddd1a99bc14a4e57fa2aacbd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 14:30:24 +0200 Subject: [PATCH 0741/1143] follow-up a03f4769caa482ed45614e1ea523703f6e6082cb fixes TypeError: auth_protect..decorator() got an unexpected keyword argument 'broadcast' --- electrum/gui/qml/qewallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 792a536a4..fd3aee41f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -485,7 +485,7 @@ def enableLightning(self): self.isLightningChanged.emit() self.dataChanged.emit() - @auth_protect + @auth_protect() def sign(self, tx, *, broadcast: bool = False): sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast), self.on_sign_failed) From 6733665dacf961e46804b6a555b8ca5935b571f5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 18 Apr 2023 14:47:01 +0200 Subject: [PATCH 0742/1143] qml: simplify path_protect decorator --- electrum/gui/qml/auth.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qml/auth.py b/electrum/gui/qml/auth.py index 49c68542a..f9896efcf 100644 --- a/electrum/gui/qml/auth.py +++ b/electrum/gui/qml/auth.py @@ -4,24 +4,21 @@ from electrum.logging import get_logger -def auth_protect(message='', method='pin', reject=None): - - def decorator(func=None): - if func is None: - return partial(auth_protect, reject=reject, method=method) - - @wraps(func) - def wrapper(self, *args, **kwargs): - _logger = get_logger(__name__) - _logger.debug(f'{str(self)}.{func.__name__}') - if hasattr(self, '__auth_fcall'): - _logger.debug('object already has a pending authed function call') - raise Exception('object already has a pending authed function call') - setattr(self, '__auth_fcall', (func,args,kwargs,reject)) - getattr(self, 'authRequired').emit(method, message) - - return wrapper - return decorator +def auth_protect(func=None, reject=None, method='pin', message=''): + if func is None: + return partial(auth_protect, reject=reject, method=method, message=message) + + @wraps(func) + def wrapper(self, *args, **kwargs): + _logger = get_logger(__name__) + _logger.debug(f'{str(self)}.{func.__name__}') + if hasattr(self, '__auth_fcall'): + _logger.debug('object already has a pending authed function call') + raise Exception('object already has a pending authed function call') + setattr(self, '__auth_fcall', (func, args, kwargs, reject)) + getattr(self, 'authRequired').emit(method, message) + + return wrapper class AuthMixin: _auth_logger = get_logger(__name__) From 43b3e074790a133c01d09096b741ea70c59e164a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 18 Apr 2023 14:51:19 +0200 Subject: [PATCH 0743/1143] Revert "qml: don't determine channel state on gui string, use state enum instead" This reverts commit 73f89d516aff9579e4b3b7ad34c0fff2b707595d. --- electrum/gui/qml/components/ChannelDetails.qml | 2 +- electrum/gui/qml/components/controls/ChannelDelegate.qml | 2 +- electrum/gui/qml/qechanneldetails.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index d1f11c090..e614f0246 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -72,7 +72,7 @@ Pane { Label { text: channeldetails.state - color: channeldetails.stateCode == ChannelDetails.Open + color: channeldetails.state == 'OPEN' ? constants.colorChannelOpen : Material.foreground } diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index af1fcce84..bf54d0a76 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -79,7 +79,7 @@ ItemDelegate { font.pixelSize: constants.fontSizeMedium color: _closed ? constants.mutedForeground - : model.state_code == ChannelDetails.Open + : model.state == 'OPEN' ? constants.colorChannelOpen : Material.foreground } diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index af25a7784..4706f6a47 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -17,7 +17,6 @@ class QEChannelDetails(QObject, QtEventListener): _logger = get_logger(__name__) class State: # subset, only ones we currently need in UI - Open = ChannelState.OPEN Closed = ChannelState.CLOSED Redeemed = ChannelState.REDEEMED From ca40b37ec5844397ae501bc3c7453ea7c0c89d30 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Apr 2023 12:59:09 +0000 Subject: [PATCH 0744/1143] qml: show "tx fee rate" in TxDetails, like in other guis --- electrum/gui/qml/components/TxDetails.qml | 12 ++++++++++++ electrum/gui/qml/qetxdetails.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 1435cf370..d4d54e641 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -117,6 +117,18 @@ Pane { } } + Label { + visible: txdetails.feeRateStr != "" + text: qsTr('Transaction fee rate') + color: Material.accentColor + } + + Label { + Layout.fillWidth: true + visible: txdetails.feeRateStr != "" + text: txdetails.feeRateStr + } + Label { Layout.fillWidth: true text: qsTr('Status') diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 866aea3d9..5897eb3a3 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -37,6 +37,7 @@ def __init__(self, parent=None): self._amount = QEAmount() self._lnamount = QEAmount() self._fee = QEAmount() + self._feerate_str = '' self._inputs = [] self._outputs = [] @@ -145,6 +146,10 @@ def lnAmount(self): def fee(self): return self._fee + @pyqtProperty(str, notify=detailsChanged) + def feeRateStr(self): + return self._feerate_str + @pyqtProperty('QVariantList', notify=detailsChanged) def inputs(self): return self._inputs @@ -259,6 +264,12 @@ def update(self): self._status = txinfo.status self._fee.satsInt = txinfo.fee + self._feerate_str = "" + if txinfo.fee is not None: + size = self._tx.estimated_size() + fee_per_kb = txinfo.fee / size * 1000 + self._feerate_str = self._wallet.wallet.config.format_fee_rate(fee_per_kb) + self._is_mined = False if not txinfo.tx_mined_status else txinfo.tx_mined_status.height > 0 if self._is_mined: self.update_mined_status(txinfo.tx_mined_status) From be2f156a4baf69cbe3741036017ebfdbe9a307f1 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 15:09:57 +0200 Subject: [PATCH 0745/1143] qml swaphelper: show errors in userinfo --- electrum/gui/qml/qeswaphelper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index b5ce15aa3..c27ecf2dc 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -372,6 +372,7 @@ def swap_task(): except Exception as e: try: # swaphelper might be destroyed at this point self.state = QESwapHelper.State.Failed + self.userinfo = _('Error') + ': ' + str(e) self._logger.error(str(e)) except RuntimeError: pass @@ -410,8 +411,8 @@ def swap_task(): pass except Exception as e: try: # swaphelper might be destroyed at this point - self.userinfo = _('Swap failed!') self.state = QESwapHelper.State.Failed + self.userinfo = _('Error') + ': ' + str(e) self._logger.error(str(e)) except RuntimeError: pass From ee61f99c22cf2f58add2d03bcaa2007d6a5c9f9f Mon Sep 17 00:00:00 2001 From: Victor Forgeoux <100780559+vforgeoux-ledger@users.noreply.github.com> Date: Tue, 18 Apr 2023 15:11:49 +0200 Subject: [PATCH 0746/1143] Add support for Ledger Stax (#8308) * Add support for Ledger Stax --- contrib/udev/20-hw1.rules | 2 ++ electrum/plugins/ledger/ledger.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/contrib/udev/20-hw1.rules b/contrib/udev/20-hw1.rules index cba158d12..2931ffe10 100644 --- a/contrib/udev/20-hw1.rules +++ b/contrib/udev/20-hw1.rules @@ -12,3 +12,5 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0003|3000|3001|30 SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0004|4000|4001|4002|4003|4004|4005|4006|4007|4008|4009|400a|400b|400c|400d|400e|400f|4010|4011|4012|4013|4014|4015|4016|4017|4018|4019|401a|401b|401c|401d|401e|401f", TAG+="uaccess", TAG+="udev-acl" # Nano S Plus SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0005|5000|5001|5002|5003|5004|5005|5006|5007|5008|5009|500a|500b|500c|500d|500e|500f|5010|5011|5012|5013|5014|5015|5016|5017|5018|5019|501a|501b|501c|501d|501e|501f", TAG+="uaccess", TAG+="udev-acl" +# Stax +SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", ATTRS{idProduct}=="0006|6000|6001|6002|6003|6004|6005|6006|6007|6008|6009|600a|600b|600c|600d|600e|600f|6010|6011|6012|6013|6014|6015|6016|6017|6018|6019|601a|601b|601c|601d|601e|601f", TAG+="uaccess", TAG+="udev-acl" diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 9ccf950f4..78b281668 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1322,7 +1322,7 @@ class LedgerPlugin(HW_PluginBase): (0x2c97, 0x0001), # Nano-S (0x2c97, 0x0004), # Nano-X (0x2c97, 0x0005), # Nano-S Plus - (0x2c97, 0x0006), # RFU + (0x2c97, 0x0006), # Stax (0x2c97, 0x0007), # RFU (0x2c97, 0x0008), # RFU (0x2c97, 0x0009), # RFU @@ -1332,6 +1332,7 @@ class LedgerPlugin(HW_PluginBase): 0x10: "Ledger Nano S", 0x40: "Ledger Nano X", 0x50: "Ledger Nano S Plus", + 0x60: "Ledger Stax", } SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') @@ -1380,6 +1381,8 @@ def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]: return True, "Ledger Nano X" if product_key == (0x2c97, 0x0005): return True, "Ledger Nano S Plus" + if product_key == (0x2c97, 0x0006): + return True, "Ledger Stax" return True, None # modern product_keys if product_key[0] == 0x2c97: From 36db6673a5ea0b43a5bf9b330208335789bb387d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 18 Apr 2023 15:30:10 +0200 Subject: [PATCH 0747/1143] update release notes for version 4.4.0 --- RELEASE-NOTES | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 555eb291c..c7c30ee1c 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,4 +1,5 @@ -# Release 4.4 (not released yet) +# Release 4.4.0 (April 18, 2023) + * New Android app, using QML instead of Kivy - Using Qt 5.15.7, PyQt 5.15.9 - This release still on python3.8 @@ -8,15 +9,17 @@ network orb for application menu - Note: long-press Receive/Send for list of payment requests/invoices * Qt GUI improvements - - New onchain tx creation flow - - Advanced options have been moved to toolbars, where their effect + - New onchain transaction creation flow, with configurable preview + - Various options have been moved to toolbars, where their effect can be more directly observed. * Privacy features: - lightning: support for option scid_alias. - - UTXO privacy analysis that displays the parents of a coin (Qt - GUI). - - Option to fully spend a selection of UTXOs into a new channel or - submarine swap (Qt GUI). + - Qt GUI: UTXO privacy analysis: this dialog displays all the + wallet transactions that are either parent of a UTXO, or can be + related to it through address reuse (Note that in the case of + address reuse, it does not display children transactions.) + - Coins tab: New menu that lets users easily spend a selection + of UTXOs into a new channel, or into a submarine swap (Qt GUI). * Internal: - Lightning invoices are regenerated everytime routing hints are deprecated due to liquidity changes. From 16fcfe34a7bc272187390363e35931c4f6ad5f21 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 18 Apr 2023 16:09:50 +0000 Subject: [PATCH 0748/1143] build: rm "contrib" from debian apt sources lists --- contrib/android/apt.sources.list | 4 ++-- contrib/build-linux/appimage/apt.sources.list | 4 ++-- contrib/build-wine/apt.sources.list | 4 ++-- contrib/freeze_containers_distro.sh | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contrib/android/apt.sources.list b/contrib/android/apt.sources.list index 93da282e5..e8c300942 100644 --- a/contrib/android/apt.sources.list +++ b/contrib/android/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib -deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib \ No newline at end of file +deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main +deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main \ No newline at end of file diff --git a/contrib/build-linux/appimage/apt.sources.list b/contrib/build-linux/appimage/apt.sources.list index 50b885a85..975ddfd5b 100644 --- a/contrib/build-linux/appimage/apt.sources.list +++ b/contrib/build-linux/appimage/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ buster main contrib -deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ buster main contrib \ No newline at end of file +deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ buster main +deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ buster main \ No newline at end of file diff --git a/contrib/build-wine/apt.sources.list b/contrib/build-wine/apt.sources.list index 93da282e5..e8c300942 100644 --- a/contrib/build-wine/apt.sources.list +++ b/contrib/build-wine/apt.sources.list @@ -1,2 +1,2 @@ -deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib -deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main contrib \ No newline at end of file +deb https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main +deb-src https://snapshot.debian.org/archive/debian/20230317T205011Z/ bullseye main \ No newline at end of file diff --git a/contrib/freeze_containers_distro.sh b/contrib/freeze_containers_distro.sh index 0da3a9969..5566ca491 100755 --- a/contrib/freeze_containers_distro.sh +++ b/contrib/freeze_containers_distro.sh @@ -32,15 +32,15 @@ wget -O /dev/null ${DEBIAN_SNAPSHOT} 2>/dev/null echo "Valid!" # build-linux -echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main contrib" >$contrib/build-linux/appimage/apt.sources.list -echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main contrib" >>$contrib/build-linux/appimage/apt.sources.list +echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main" >$contrib/build-linux/appimage/apt.sources.list +echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main" >>$contrib/build-linux/appimage/apt.sources.list # build-wine -echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main contrib" >$contrib/build-wine/apt.sources.list -echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main contrib" >>$contrib/build-wine/apt.sources.list +echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main" >$contrib/build-wine/apt.sources.list +echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main" >>$contrib/build-wine/apt.sources.list # android -echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main contrib" >$contrib/android/apt.sources.list -echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main contrib" >>$contrib/android/apt.sources.list +echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main" >$contrib/android/apt.sources.list +echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main" >>$contrib/android/apt.sources.list echo "updated APT sources to ${DEBIAN_SNAPSHOT}" From f15abd7cbd55c82833ad621754d3420895414f75 Mon Sep 17 00:00:00 2001 From: l-pt <88390968+l-pt@users.noreply.github.com> Date: Wed, 19 Apr 2023 12:12:18 +0000 Subject: [PATCH 0749/1143] remove PATH manipulation from .desktop file (#8309) * remove PATH manipulation from .desktop file * Add note for user-local intallations --- electrum.desktop | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum.desktop b/electrum.desktop index 1593591f7..4bb197272 100644 --- a/electrum.desktop +++ b/electrum.desktop @@ -1,9 +1,10 @@ # If you want Electrum to appear in a Linux app launcher ("start menu"), install this by doing: # sudo desktop-file-install electrum.desktop +# Note: This assumes $HOME/.local/bin is in your $PATH [Desktop Entry] Comment=Lightweight Bitcoin Client -Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum %u" +Exec=electrum %u GenericName[en_US]=Bitcoin Wallet GenericName=Bitcoin Wallet Icon=electrum @@ -18,5 +19,5 @@ MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning; Actions=Testnet; [Desktop Action Testnet] -Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum --testnet %u" +Exec=electrum --testnet %u Name=Testnet mode From e617dd07a076d8bc9bacc6c9e8292a7644d5e010 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 16:08:24 +0000 Subject: [PATCH 0750/1143] qt send tab: fix payto_contacts closes https://github.com/spesmilo/electrum/issues/8313 --- electrum/gui/qt/paytoedit.py | 4 ++++ electrum/gui/qt/send_tab.py | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index af2ebfd61..b43354ba4 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -121,6 +121,7 @@ def __init__(self, send_tab: 'SendTab'): self.setText = self.editor.setText self.setEnabled = self.editor.setEnabled self.setReadOnly = self.editor.setReadOnly + self.setFocus = self.editor.setFocus # button handlers self.on_qr_from_camera_input_btn = partial( self.input_qr_from_camera, @@ -150,10 +151,13 @@ def editor(self): return self.text_edit if self.is_paytomany() else self.line_edit def set_paytomany(self, b): + has_focus = self.editor.hasFocus() self._is_paytomany = b self.line_edit.setVisible(not b) self.text_edit.setVisible(b) self.send_tab.paytomany_menu.setChecked(b) + if has_focus: + self.editor.setFocus() def toggle_paytomany(self): self.set_paytomany(not self._is_paytomany) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 3412a2f1c..cbfa7687a 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -793,12 +793,11 @@ def toggle_paytomany(self): def payto_contacts(self, labels): paytos = [self.window.get_contact_payto(label) for label in labels] self.window.show_send_tab() + self.payto_e.do_clear() if len(paytos) == 1: self.payto_e.setText(paytos[0]) self.amount_e.setFocus() else: + self.payto_e.setFocus() text = "\n".join([payto + ", 0" for payto in paytos]) self.payto_e.setText(text) - self.payto_e.setFocus() - - From bba8a272e78ae8961dffd37060801bf28b934dd9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 16:18:57 +0000 Subject: [PATCH 0751/1143] qt send tab: rm unused variable and add explanation --- electrum/gui/qt/send_tab.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index cbfa7687a..eafa8f61b 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -254,7 +254,8 @@ def pay_onchain_dialog( output_value = '!' if is_max else sum(output_values) conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value) if conf_dlg.not_enough_funds: - confirmed_only = self.config.get('confirmed_only', False) + # note: use confirmed_only=False here, regardless of config setting, + # as the user needs to get to ConfirmTxDialog to change the config setting if not conf_dlg.can_pay_assuming_zero_fees(confirmed_only=False): text = self.get_text_not_enough_funds_mentioning_frozen() self.show_message(text) From e24f837fbfc9f3e0fe027fefc8c481f8eb98987d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 16:25:39 +0000 Subject: [PATCH 0752/1143] qt send tab: rm incorrect bare raise follow-up bc3946d2f4e00abdfc8bcd2980ea4a467a2e9756 not sure what the raise was trying to do; note that relevant exception handling is done at: https://github.com/spesmilo/electrum/blob/bba8a272e78ae8961dffd37060801bf28b934dd9/electrum/gui/qt/main_window.py#L1213-L1217 although note the TODO in main_window.on_error: would be nice to propagate some of the exceptions to the crash reporter closes https://github.com/spesmilo/electrum/issues/8312 --- electrum/gui/qt/send_tab.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index eafa8f61b..1429c8494 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -272,8 +272,6 @@ def pay_onchain_dialog( def sign_done(success): if success: self.window.broadcast_or_show(tx) - else: - raise self.window.sign_tx( tx, callback=sign_done, From 89b75f95d0f87532a312a3ac9b84070fea304542 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 16:29:54 +0000 Subject: [PATCH 0753/1143] wallet.set_broadcasting: fix incorrect type-hint and rename arg --- electrum/gui/qml/qewallet.py | 10 +++++----- electrum/gui/qt/send_tab.py | 6 +++--- electrum/wallet.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index fd3aee41f..025e3ad82 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -542,23 +542,23 @@ def broadcast(self, tx): assert tx.is_complete() def broadcast_thread(): - self.wallet.set_broadcasting(tx, PR_BROADCASTING) + self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCASTING) try: self._logger.info('running broadcast in thread') self.wallet.network.run_from_another_thread(self.wallet.network.broadcast_transaction(tx)) except TxBroadcastError as e: self._logger.error(repr(e)) self.broadcastFailed.emit(tx.txid(), '', e.get_message_for_gui()) - self.wallet.set_broadcasting(tx, None) + self.wallet.set_broadcasting(tx, broadcasting_status=None) except BestEffortRequestFailed as e: self._logger.error(repr(e)) self.broadcastFailed.emit(tx.txid(), '', repr(e)) - self.wallet.set_broadcasting(tx, None) + self.wallet.set_broadcasting(tx, broadcasting_status=None) else: self._logger.info('broadcast success') self.broadcastSucceeded.emit(tx.txid()) - self.historyModel.requestRefresh.emit() # via qt thread - self.wallet.set_broadcasting(tx, PR_BROADCAST) + self.historyModel.requestRefresh.emit() # via qt thread + self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCAST) threading.Thread(target=broadcast_thread, daemon=True).start() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 1429c8494..c8276c82d 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -760,7 +760,7 @@ def broadcast_thread(): # Capture current TL window; override might be removed on return parent = self.window.top_level_window(lambda win: isinstance(win, MessageBoxMixin)) - self.wallet.set_broadcasting(tx, PR_BROADCASTING) + self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCASTING) def broadcast_done(result): # GUI thread @@ -769,11 +769,11 @@ def broadcast_done(result): if success: parent.show_message(_('Payment sent.') + '\n' + msg) self.invoice_list.update() - self.wallet.set_broadcasting(tx, PR_BROADCAST) + self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCAST) else: msg = msg or '' parent.show_error(msg) - self.wallet.set_broadcasting(tx, None) + self.wallet.set_broadcasting(tx, broadcasting_status=None) WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.window.on_error) diff --git a/electrum/wallet.py b/electrum/wallet.py index 302726f4b..e57bd80f4 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2549,13 +2549,13 @@ def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None: util.trigger_callback('request_status', self, request.get_id(), status) self._update_onchain_invoice_paid_detection(invoice_keys) - def set_broadcasting(self, tx: Transaction, b: bool): + def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]): request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx) for key in invoice_keys: invoice = self._invoices.get(key) if not invoice: continue - invoice._broadcasting_status = b + invoice._broadcasting_status = broadcasting_status status = self.get_invoice_status(invoice) util.trigger_callback('invoice_status', self, key, status) From 7b475f58dbccce12218f3de73d7d63b189d0d09f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 19 Apr 2023 21:11:56 +0200 Subject: [PATCH 0754/1143] qml: show unrelated tx also in top InfoTextArea --- electrum/gui/qml/components/TxDetails.qml | 32 ++++++++--------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index d4d54e641..eb58dfb58 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -55,27 +55,17 @@ Pane { Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel || txdetails.canRemove - text: txdetails.canRemove - ? qsTr('This transaction is local to your wallet. It has not been published yet.') - : qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel - ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') - : qsTr('You can bump its fee to speed up its confirmation')) - } - - RowLayout { - Layout.fillWidth: true - Layout.columnSpan: 2 - visible: txdetails.isUnrelated - Image { - source: '../../icons/warning.png' - Layout.preferredWidth: constants.iconSizeSmall - Layout.preferredHeight: constants.iconSizeSmall - } - Label { - text: qsTr('Transaction is unrelated to this wallet') - color: Material.accentColor - } + visible: txdetails.canBump || txdetails.canCpfp || txdetails.canCancel || txdetails.canRemove || txdetails.isUnrelated + text: txdetails.isUnrelated + ? qsTr('Transaction is unrelated to this wallet') + : txdetails.canRemove + ? qsTr('This transaction is local to your wallet. It has not been published yet.') + : qsTr('This transaction is still unconfirmed.') + '\n' + (txdetails.canCancel + ? qsTr('You can bump its fee to speed up its confirmation, or cancel this transaction') + : qsTr('You can bump its fee to speed up its confirmation')) + iconStyle: txdetails.isUnrelated + ? InfoTextArea.IconStyle.Warn + : InfoTextArea.IconStyle.Info } Label { From dd93ebda4d6692ff66543723289b81173e917032 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 21:55:07 +0000 Subject: [PATCH 0755/1143] wallet: _bump_fee_through_decreasing_payment: handle too high fee_rate fixes https://github.com/spesmilo/electrum/issues/8316 --- electrum/tests/test_wallet_vertical.py | 42 +++++++++++++++++++++++++- electrum/wallet.py | 2 ++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 8b1dedbe4..aeeeeb92a 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -12,7 +12,7 @@ from electrum import util from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, - restore_wallet_from_text, Abstract_Wallet) + restore_wallet_from_text, Abstract_Wallet, CannotBumpFee) from electrum.util import ( bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException) @@ -1100,6 +1100,9 @@ def __enter__(self): await self._bump_fee_p2wpkh_decrease_payment_batch( simulate_moving_txs=simulate_moving_txs, config=config) + with TmpConfig() as config: + with self.subTest(msg="_bump_fee_p2wpkh_insane_high_target_fee", simulate_moving_txs=simulate_moving_txs): + await self._bump_fee_p2wpkh_insane_high_target_fee(config=config) async def _bump_fee_p2pkh_when_there_is_a_change_address(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean', @@ -1291,6 +1294,43 @@ async def _bump_fee_p2wpkh_decrease_payment_batch(self, *, simulate_moving_txs, wallet.adb.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) self.assertEqual((0, 18700, 0), wallet.get_balance()) + async def _bump_fee_p2wpkh_insane_high_target_fee(self, *, config): + wallet = self.create_standard_wallet_from_seed('leader company camera enlist crash sleep insane aware anger hole hammer label', + config=config) + + # bootstrap wallet + funding_tx = Transaction('020000000001022ea8f7940c2e4bca2f34f21ba15a5c8d5e3c93d9c6deb17983412feefa0f1f6d0100000000fdffffff9d4ba5ab41951d506a7fa8272ef999ce3df166fe28f6f885aa791f012a0924cf0000000000fdffffff027485010000000000160014f80e86af4246960a24cd21c275a8e8842973fbcaa0860100000000001600149c6b743752604b98d30f1a5d27a5d5ce8919f4400247304402203bf6dd875a775f356d4bb8c4e295a2cd506338c100767518f2b31fb85db71c1302204dc4ebca5584fc1cc08bd7f7171135d1b67ca6c8812c3723cd332eccaa7b848101210360bdbd16d9ef390fd3e804c421e6f30e6b065ac314f4d2b9a80d2f0682ad1431024730440220126b442d7988c5883ca17c2429f51ce770e3a57895524c8dfe07b539e483019e02200b50feed4f42f0035c9a9ddd044820607281e45e29e41a29233c2b8be6080bac01210245d47d08915816a5ecc934cff1b17e00071ca06172f51d632ba95392e8aad4fdd38a1d00') + funding_txid = funding_tx.txid() + self.assertEqual('dd0bf0d1563cd588b4c93cc1a9623c051ddb1c4f4581cf8ef43cfd27f031f246', funding_txid) + wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + orig_rbf_tx = Transaction('0200000000010146f231f027fd3cf48ecf81454f1cdb1d053c62a9c13cc9b488d53c56d1f00bdd0100000000fdffffff02c8af000000000000160014999a95482213a896c72a251b6cc9f3d137b0a45850c3000000000000160014ea76d391236726af7d7a9c10abe600129154eb5a02473044022076d298537b524a926a8fadad0e9ded5868c8f4cf29246048f76f00eb4afa56310220739ad9e0417e97ce03fad98a454b4977972c2805cef37bfa822c6d6c56737c870121024196fb7b766ac987a08b69a5e108feae8513b7e72bc9e47899e27b36100f2af4d48a1d00') + orig_rbf_txid = orig_rbf_tx.txid() + self.assertEqual('db2f77709a4a04417b3a45838c21470877fe7c182a4f81005a21ce1315c6a5e6', orig_rbf_txid) + wallet.adb.receive_tx_callback(orig_rbf_txid, orig_rbf_tx, TX_HEIGHT_UNCONFIRMED) + + with self.assertRaises(CannotBumpFee): + tx = wallet.bump_fee( + tx=tx_from_any(orig_rbf_tx.serialize()), + new_fee_rate=99999, + decrease_payment=True, + ) + with self.assertRaises(CannotBumpFee): + tx = wallet.bump_fee( + tx=tx_from_any(orig_rbf_tx.serialize()), + new_fee_rate=99999, + decrease_payment=False, + ) + + tx = wallet.bump_fee( + tx=tx_from_any(orig_rbf_tx.serialize()), + new_fee_rate=60, + decrease_payment=True, + ) + tx.locktime = 1936085 + tx.version = 2 + self.assertEqual('6b03c00f47cb145ffb632c3ce54dece29b9a980949ef5c574321f7fc83fa2238', tx.txid()) + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') async def test_cpfp_p2pkh(self, mock_save_db): wallet = self.create_standard_wallet_from_seed('fold object utility erase deputy output stadium feed stereo usage modify bean') diff --git a/electrum/wallet.py b/electrum/wallet.py index e57bd80f4..5c729e1a3 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2091,6 +2091,8 @@ def _bump_fee_through_decreasing_payment( break out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey.hex()) for (idx, out) in s if idx not in del_out_idxs) + if out_size_total == 0: # no outputs left to decrease + raise CannotBumpFee(_('Could not find suitable outputs')) for idx, out in s: out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey.hex()) delta = int(math.ceil(delta_total * out_size / out_size_total)) From cbb4c3ceb2771c33de1633644d77601ee18a11f7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 22:25:02 +0000 Subject: [PATCH 0756/1143] slip39: rewrite some strings for better localisation related: https://github.com/spesmilo/electrum/issues/8314 --- electrum/slip39.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/slip39.py b/electrum/slip39.py index 6ab1d776c..ab07fb486 100644 --- a/electrum/slip39.py +++ b/electrum/slip39.py @@ -297,11 +297,11 @@ def process_mnemonics(mnemonics: List[str]) -> Tuple[bool, str]: common_params = shares[0].common_parameters() for share in shares: if share.common_parameters() != common_params: - error_text = _("Share") + ' #%d ' % share.index + _("is not part of the current set.") + error_text = _("Share #{} is not part of the current set.").format(share.index) return None, _ERROR_STYLE % error_text for other in groups[share.group_index]: if share.member_index == other.member_index: - error_text = _("Share") + ' #%d ' % share.index + _("is a duplicate of share") + ' #%d.' % other.index + error_text = _("Share #{} is a duplicate of share #{}.").format(share.index, other.index) return None, _ERROR_STYLE % error_text groups[share.group_index].add(share) @@ -319,7 +319,8 @@ def process_mnemonics(mnemonics: List[str]) -> Tuple[bool, str]: group_count = shares[0].group_count status = '' if group_count > 1: - status += _('Completed') + ' %d ' % groups_completed + _('of') + ' %d ' % group_threshold + _('groups needed:
') + status += _('Completed {} of {} groups needed').format(f"{groups_completed}", f"{group_threshold}") + status += ":
" for group_index in range(group_count): group_prefix = _make_group_prefix(identifier, iteration_exponent, group_index, group_threshold, group_count) @@ -368,11 +369,11 @@ def _make_group_prefix(identifier, iteration_exponent, group_index, group_thresh def _group_status(group: Set[Share], group_prefix) -> str: len(group) if not group: - return _EMPTY + '0 ' + _('shares from group') + ' ' + group_prefix + '.
' + return _EMPTY + _('{} shares from group {}').format('0 ', f'{group_prefix}') + f'.
' else: share = next(iter(group)) icon = _FINISHED if len(group) >= share.member_threshold else _INPROGRESS - return icon + '%d ' % len(group) + _('of') + ' %d ' % share.member_threshold + _('shares needed from group') + ' %s.
' % group_prefix + return icon + _('{} of {} shares needed from group {}').format(f'{len(group)}', f'{share.member_threshold}', f'{group_prefix}') + f'.
' """ From 42ec0e4e9d8741fe9f9f1a3f5f42bddc0d208826 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 19 Apr 2023 22:31:08 +0000 Subject: [PATCH 0757/1143] slip39: fix incorrect type hint --- electrum/slip39.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/slip39.py b/electrum/slip39.py index ab07fb486..6a889d476 100644 --- a/electrum/slip39.py +++ b/electrum/slip39.py @@ -278,7 +278,7 @@ def get_wordlist() -> Wordlist: return wordlist -def process_mnemonics(mnemonics: List[str]) -> Tuple[bool, str]: +def process_mnemonics(mnemonics: List[str]) -> Tuple[Optional[EncryptedSeed], str]: # Collect valid shares. shares = [] for i, mnemonic in enumerate(mnemonics): From a2063f8f48d95dc91dfe061baed8535b7a8a4b84 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 00:15:27 +0000 Subject: [PATCH 0758/1143] qt tx dialog: rm dead code --- electrum/gui/qt/transaction_dialog.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index e860e425c..5fb1f2060 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -936,15 +936,6 @@ def set_title(self): txid = self.tx.txid() or "" self.setWindowTitle(_("Transaction") + ' ' + txid) - def can_finalize(self) -> bool: - return False - - def on_finalize(self): - pass # overridden in subclass - - def update_fee_fields(self): - pass # overridden in subclass - def maybe_fetch_txin_data(self): """Download missing input data from the network, asynchronously. Note: we fetch the prev txs, which allows calculating the fee and showing "input addresses". From ce8f564fb48d2aa0d22a1d6162ed165e4ea9f4f3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 00:36:40 +0000 Subject: [PATCH 0759/1143] qt bump fee: disable targeting an abs fee. only allow setting feerate wallet.bump_fee() only allows targeting a feerate. Prior to this commit, _BaseRBFDialog(TxEditor) allowed setting either a feerate or an abs fee. When setting an abs fee, TxEditor.update_fee_fields() tries to adjust the feerate accordingly, and then via side-effecting, wallet.bump_fee() will get called with the derived feerate. This seems really buggy atm. I think it is best to disable setting abs fees, and if we want to enable it later, targeting needs to be implemented in wallet.bump_fee() - just like how it works in ConfirmTxDialog(TxEditor) and wallet.make_unsigned_transaction(). --- electrum/gui/qt/rbf_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index 045413e9b..97c34cabc 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -49,6 +49,7 @@ def __init__( title=title, make_tx=self.rbf_func) + self.fee_e.setFrozen(True) # disallow setting absolute fee for now, as wallet.bump_fee can only target feerate new_fee_rate = self.old_fee_rate + max(1, self.old_fee_rate // 20) self.feerate_e.setAmount(new_fee_rate) self.update() From fd6e34bf5f2742cec0b3481a3a7695b5c45a1c63 Mon Sep 17 00:00:00 2001 From: Emanuel Haupt Date: Thu, 20 Apr 2023 08:50:44 +0200 Subject: [PATCH 0760/1143] fix: Fix libsecp256k1 loader on FreeBSD FreeBSD installs libsecp256k1 as: ``` /usr/local/lib/libsecp256k1.so.2.0.1 /usr/local/lib/libsecp256k1.so -> libsecp256k1.so.2.0.1 /usr/local/lib/libsecp256k1.so.2 -> libsecp256k1.so.2.0.1 ``` --- electrum/ecc_fast.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index fe6e1090e..fcd13c64d 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -43,6 +43,8 @@ def load_library(): libnames = ['libsecp256k1-1.dll', 'libsecp256k1-0.dll', ] elif 'ANDROID_DATA' in os.environ: libnames = ['libsecp256k1.so', ] + elif 'freebsd' in sys.platform: + libnames = ['libsecp256k1.so', ] else: # desktop Linux and similar libnames = ['libsecp256k1.so.1', 'libsecp256k1.so.0', ] library_paths = [] From 1649f9993eb73fc8d038305f90cbb6aba66a4334 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 20 Apr 2023 10:27:30 +0200 Subject: [PATCH 0761/1143] qml: limit wallet name label widths so they get wrapped/elided. fixes #8317 --- electrum/gui/qml/components/Wallets.qml | 3 ++- electrum/gui/qml/components/main.qml | 1 + electrum/gui/qml/components/wizard/WCWalletPassword.qml | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 5b6d3b76a..4a8949a37 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -81,10 +81,11 @@ Pane { } Label { + Layout.fillWidth: true font.pixelSize: constants.fontSizeLarge text: model.name + elide: Label.ElideRight color: model.active ? Material.foreground : Qt.darker(Material.foreground, 1.20) - Layout.fillWidth: true } Tag { diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 15eb7ed0f..09c63b7bf 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -129,6 +129,7 @@ ApplicationWindow } RowLayout { + width: parent.width Item { Layout.preferredWidth: constants.paddingXLarge diff --git a/electrum/gui/qml/components/wizard/WCWalletPassword.qml b/electrum/gui/qml/components/wizard/WCWalletPassword.qml index 78e3903f1..2e41d1309 100644 --- a/electrum/gui/qml/components/wizard/WCWalletPassword.qml +++ b/electrum/gui/qml/components/wizard/WCWalletPassword.qml @@ -13,10 +13,14 @@ WizardComponent { } ColumnLayout { + width: parent.width + Label { + Layout.fillWidth: true text: Daemon.singlePasswordEnabled ? qsTr('Enter password') : qsTr('Enter password for %1').arg(wizard_data['wallet_name']) + wrapMode: Text.Wrap } PasswordField { id: password1 From 323aa842791fe4f424e9a7d1cfd296fa612ba7ad Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 20 Apr 2023 10:37:49 +0200 Subject: [PATCH 0762/1143] qml: only allow wallet menu if on wallet specific page --- electrum/gui/qml/components/main.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 09c63b7bf..c1695ce5b 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -122,6 +122,7 @@ ApplicationWindow MouseArea { anchors.fill: parent + enabled: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) onClicked: { stack.getRoot().menu.open() // open wallet-menu stack.getRoot().menu.y = toolbar.height From 3b7fa89e442ea9b4952868bce0c0ec05267777ae Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 20 Apr 2023 11:00:18 +0200 Subject: [PATCH 0763/1143] wizard: p2sh multisig is 'standard' in backend wallet --- electrum/wizard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index c8c59b5d7..410e70d29 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -365,7 +365,10 @@ def create_storage(self, path, data): self._logger.debug('creating keystore from bip39 seed') root_seed = keystore.bip39_to_seed(data['seed'], data['seed_extra_words']) derivation = normalize_bip32_derivation(data['derivation_path']) - script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard' + if data['wallet_type'] == 'multisig': + script = data['script_type'] if data['script_type'] != 'p2sh' else 'standard' + else: + script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard' k = keystore.from_bip43_rootseed(root_seed, derivation, xtype=script) elif is_any_2fa_seed_type(data['seed_type']): self._logger.debug('creating keystore from 2fa seed') From 54ab3673baa2bc3e894bf08f2ec6f726e04ba983 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 20 Apr 2023 11:02:46 +0200 Subject: [PATCH 0764/1143] qml: TxDetails don't show or allow edit of label for unrelated tx --- electrum/gui/qml/components/TxDetails.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index eb58dfb58..8531b2175 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -166,6 +166,7 @@ Pane { Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall + visible: !txdetails.isUnrelated text: qsTr('Label') color: Material.accentColor } @@ -178,6 +179,8 @@ Pane { Layout.columnSpan: 2 Layout.fillWidth: true + visible: !txdetails.isUnrelated + RowLayout { width: parent.width Label { From 1a4e48e2d4799579d1f5fd7ba39e0f27537ed185 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 20 Apr 2023 12:07:05 +0200 Subject: [PATCH 0765/1143] qml: fix MessageDialog layout --- electrum/gui/qml/components/MessageDialog.qml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/MessageDialog.qml b/electrum/gui/qml/components/MessageDialog.qml index 06a6f0a0f..91f3721e2 100644 --- a/electrum/gui/qml/components/MessageDialog.qml +++ b/electrum/gui/qml/components/MessageDialog.qml @@ -22,14 +22,20 @@ ElDialog { padding: 0 + width: rootLayout.width + ColumnLayout { + id: rootLayout + width: dialog.parent.width * 2/3 + ColumnLayout { visible: text Layout.margins: constants.paddingMedium - Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + TextArea { id: message - Layout.preferredWidth: dialog.parent.width * 2/3 + Layout.fillWidth: true readOnly: true wrapMode: TextInput.WordWrap textFormat: richText ? TextEdit.RichText : TextEdit.PlainText @@ -40,7 +46,7 @@ ElDialog { } ButtonContainer { - Layout.preferredWidth: dialog.parent.width * 2/3 + Layout.fillWidth: true FlatButton { Layout.fillWidth: true From b0b4f39b405f3cdbf036e3ec892ccedc3c971866 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 14:31:44 +0000 Subject: [PATCH 0766/1143] i18n: add "context" param to _(), and use it from qml fixes https://github.com/spesmilo/electrum/issues/8323 from issue: > Currently, translatable strings from QML are assigned a `context` > by `lupdate`, which is then also used by the conversion to `gettext`. > This `context` must be used when translating such a string. This results in > strings that are unique to QML to not be translated, due to a missing > `context` parameter which we do not take into account in electrum. --- electrum/gui/qml/__init__.py | 2 +- electrum/i18n.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index a09539c5b..f475a58d5 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -42,7 +42,7 @@ def __init__(self, parent=None): super().__init__(parent) def translate(self, context, source_text, disambiguation, n): - return _(source_text) + return _(source_text, context=context) class ElectrumGui(BaseElectrumGui, Logger): diff --git a/electrum/i18n.py b/electrum/i18n.py index 23bdda1c9..9f85fe3d1 100644 --- a/electrum/i18n.py +++ b/electrum/i18n.py @@ -53,11 +53,22 @@ # note: f-strings cannot be translated! see https://stackoverflow.com/q/49797658 # So this does not work: _(f"My name: {name}") # instead use .format: _("My name: {}").format(name) -def _(x: str) -> str: - if x == "": +def _(msg: str, *, context=None) -> str: + if msg == "": return "" # empty string must not be translated. see #7158 global language - return language.gettext(x) + if context: + contexts = [context] + if context[-1] != "|": # try with both "|" suffix and without + contexts.append(context + "|") + else: + contexts.append(context[:-1]) + for ctx in contexts: + out = language.pgettext(ctx, msg) + if out != msg: # found non-trivial translation + return out + # else try without context + return language.gettext(msg) def set_language(x: Optional[str]) -> None: From fa04ff005b98b5b44ba53f034477fce5b103fbe9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 14:57:37 +0000 Subject: [PATCH 0767/1143] contrib: fix build_locale.sh to work with relative paths I think all scripts that call this file already used abs paths, but manual callers tend to use relative paths. --- contrib/build_locale.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/contrib/build_locale.sh b/contrib/build_locale.sh index 425066754..a50f0eca4 100755 --- a/contrib/build_locale.sh +++ b/contrib/build_locale.sh @@ -8,16 +8,20 @@ if [[ ! -d "$1" || -z "$2" ]]; then exit 1 fi +# convert $1 and $2 to abs paths +SRC_DIR="$(realpath "$1" 2> /dev/null || grealpath "$1")" +DST_DIR="$(realpath "$2" 2> /dev/null || grealpath "$2")" + if ! which msgfmt > /dev/null 2>&1; then echo "Please install gettext" exit 1 fi -cd "$1" -mkdir -p "$2" +cd "$SRC_DIR" +mkdir -p "$DST_DIR" for i in *; do - dir="$2/$i/LC_MESSAGES" + dir="$DST_DIR/$i/LC_MESSAGES" mkdir -p "$dir" (msgfmt --output-file="$dir/electrum.mo" "$i/electrum.po" || true) done From 22205dccb1ba069b5efbc36eae11f52e6437b101 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 15:06:37 +0000 Subject: [PATCH 0768/1143] qt ChannelsList: disable toolbar menu if `not wallet.has_lightning()` closes https://github.com/spesmilo/electrum/issues/8321 --- electrum/gui/qt/channels_list.py | 3 +++ electrum/gui/qt/main_window.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index d81bb9d56..e3fc3d416 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -363,6 +363,9 @@ def create_toolbar(self, config): menu.addAction(read_QIcon('update.png'), _('Submarine swap'), lambda: self.main_window.run_swap_dialog()) menu.addSeparator() menu.addAction(_("Import channel backup"), lambda: self.main_window.do_process_from_text_channel_backup()) + # only enable menu if has LN. Or we could selectively enable menu items? + # and maybe add item "main_window.init_lightning_dialog()" when applicable + menu.setEnabled(self.wallet.has_lightning()) self.new_channel_button = EnterButton(_('New Channel'), self.main_window.new_channel_dialog) self.new_channel_button.setEnabled(self.wallet.has_lightning()) toolbar.insertWidget(2, self.new_channel_button) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 4eeb4aebc..67fd252fd 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1112,6 +1112,9 @@ def run_swap_dialog(self, is_reverse=None, recv_amount_sat=None, channels=None): if not self.network: self.show_error(_("You are offline.")) return + if not self.wallet.lnworker: + self.show_error(_('Lightning is disabled')) + return if not self.wallet.lnworker.num_sats_can_send() and not self.wallet.lnworker.num_sats_can_receive(): self.show_error(_("You do not have liquidity in your active channels.")) return @@ -2130,6 +2133,9 @@ def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransacti def import_channel_backup(self, encrypted: str): if not self.question('Import channel backup?'): return + if not self.wallet.lnworker: + self.show_error(_('Lightning is disabled')) + return try: self.wallet.lnworker.import_channel_backup(encrypted) except Exception as e: From be159b5b9558f483e31558032f6d0ea99a1fdfd8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 15:17:36 +0000 Subject: [PATCH 0769/1143] bugfix: assert walrus (":=") combo side-eff. breaks w/ asserts disabled ``` $ python3 -O Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> assert (x := 2) >>> x Traceback (most recent call last): File "", line 1, in NameError: name 'x' is not defined ``` pity. it looked to be a neat and concise pattern. --- electrum/plugins/bitbox02/bitbox02.py | 3 ++- electrum/plugins/digitalbitbox/digitalbitbox.py | 3 ++- electrum/plugins/jade/jade.py | 3 ++- electrum/plugins/keepkey/keepkey.py | 6 ++++-- electrum/plugins/safe_t/safe_t.py | 6 ++++-- electrum/plugins/trezor/trezor.py | 6 ++++-- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index ccba7c126..fcd14a241 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -444,7 +444,8 @@ def sign_transaction( } ) - assert (desc := txin.script_descriptor) + desc = txin.script_descriptor + assert desc if tx_script_type is None: tx_script_type = desc.to_legacy_electrum_script_type() elif tx_script_type != desc.to_legacy_electrum_script_type(): diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 1f774c62d..2701d6ca6 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -528,7 +528,8 @@ def sign_transaction(self, tx, password): if txin.is_coinbase_input(): self.give_error("Coinbase not supported") # should never happen - assert (desc := txin.script_descriptor) + desc = txin.script_descriptor + assert desc if desc.to_legacy_electrum_script_type() != 'p2pkh': p2pkhTransaction = False diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 1493de8c2..348ebf556 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -280,7 +280,8 @@ def sign_transaction(self, tx, password): change = [None] * len(tx.outputs()) for index, txout in enumerate(tx.outputs()): if txout.is_mine and txout.is_change: - assert (desc := txout.script_descriptor) + desc = txout.script_descriptor + assert desc if is_multisig: # Multisig - wallet details must be registered on Jade hw multisig_name = _register_multisig_wallet(wallet, self, txout.address) diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py index 573819ec6..aa09aeb97 100644 --- a/electrum/plugins/keepkey/keepkey.py +++ b/electrum/plugins/keepkey/keepkey.py @@ -371,7 +371,8 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeySto assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - assert (desc := txin.script_descriptor) + desc = txin.script_descriptor + assert desc if multi := desc.get_simple_multisig(): multisig = self._make_multisig(multi) else: @@ -417,7 +418,8 @@ def _make_multisig(self, desc: descriptor.MultisigDescriptor): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'): def create_output_by_derivation(): - assert (desc := txout.script_descriptor) + desc = txout.script_descriptor + assert desc script_type = self.get_keepkey_output_script_type(desc.to_legacy_electrum_script_type()) if multi := desc.get_simple_multisig(): multisig = self._make_multisig(multi) diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py index 750171fc0..850c00588 100644 --- a/electrum/plugins/safe_t/safe_t.py +++ b/electrum/plugins/safe_t/safe_t.py @@ -341,7 +341,8 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - assert (desc := txin.script_descriptor) + desc = txin.script_descriptor + assert desc if multi := desc.get_simple_multisig(): multisig = self._make_multisig(multi) else: @@ -387,7 +388,8 @@ def _make_multisig(self, desc: descriptor.MultisigDescriptor): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'): def create_output_by_derivation(): - assert (desc := txout.script_descriptor) + desc = txout.script_descriptor + assert desc script_type = self.get_safet_output_script_type(desc.to_legacy_electrum_script_type()) if multi := desc.get_simple_multisig(): multisig = self._make_multisig(multi) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 6170ea2d3..39571b760 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -412,7 +412,8 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - assert (desc := txin.script_descriptor) + desc = txin.script_descriptor + assert desc if multi := desc.get_simple_multisig(): txinputtype.multisig = self._make_multisig(multi) txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type()) @@ -444,7 +445,8 @@ def _make_multisig(self, desc: descriptor.MultisigDescriptor): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'): def create_output_by_derivation(): - assert (desc := txout.script_descriptor) + desc = txout.script_descriptor + assert desc script_type = self.get_trezor_output_script_type(desc.to_legacy_electrum_script_type()) if multi := desc.get_simple_multisig(): multisig = self._make_multisig(multi) From 8f576e50a494eddcc49908ba4891f351cb6a2f9b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 17:48:03 +0000 Subject: [PATCH 0770/1143] lnurl: add some error-handling/response-validation --- electrum/lnurl.py | 65 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/electrum/lnurl.py b/electrum/lnurl.py index bbc76ba1b..a4d376354 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -13,11 +13,15 @@ from electrum.segwit_addr import bech32_decode, Encoding, convertbits from electrum.lnaddr import LnDecodeException from electrum.network import Network +from electrum.logging import get_logger if TYPE_CHECKING: from collections.abc import Coroutine +_logger = get_logger(__name__) + + class LNURLError(Exception): pass @@ -52,21 +56,19 @@ class LNURL6Data(NamedTuple): async def _request_lnurl(url: str) -> dict: """Requests payment data from a lnurl.""" try: - response = await Network.async_send_http_on_proxy("get", url, timeout=10) - response = json.loads(response) + response_raw = await Network.async_send_http_on_proxy("get", url, timeout=10) except asyncio.TimeoutError as e: raise LNURLError("Server did not reply in time.") from e except aiohttp.client_exceptions.ClientError as e: raise LNURLError(f"Client error: {e}") from e + try: + response = json.loads(response_raw) except json.JSONDecodeError: raise LNURLError(f"Invalid response from server") - # TODO: handling of specific client errors - if "metadata" in response: - response["metadata"] = json.loads(response["metadata"]) status = response.get("status") if status and status == "ERROR": - raise LNURLError(f"LNURL request encountered an error: {response['reason']}") + raise LNURLError(f"LNURL request encountered an error: {response.get('reason', '')}") return response @@ -75,17 +77,38 @@ async def request_lnurl(url: str) -> LNURL6Data: tag = lnurl_dict.get('tag') if tag != 'payRequest': # only LNURL6 is handled atm raise LNURLError(f"Unknown subtype of lnurl. tag={tag}") - metadata = lnurl_dict.get('metadata') + # parse lnurl6 "metadata" metadata_plaintext = "" - for m in metadata: - if m[0] == 'text/plain': - metadata_plaintext = str(m[1]) + try: + metadata_raw = lnurl_dict["metadata"] + metadata = json.loads(metadata_raw) + for m in metadata: + if m[0] == 'text/plain': + metadata_plaintext = str(m[1]) + except Exception as e: + raise LNURLError(f"Missing or malformed 'metadata' field in lnurl6 response. exc: {e!r}") from e + # parse lnurl6 "callback" + try: + callback_url = lnurl_dict['callback'] + except KeyError as e: + raise LNURLError(f"Missing 'callback' field in lnurl6 response.") from e + # parse lnurl6 "minSendable"/"maxSendable" + try: + max_sendable_sat = int(lnurl_dict['maxSendable']) // 1000 + min_sendable_sat = int(lnurl_dict['minSendable']) // 1000 + except Exception as e: + raise LNURLError(f"Missing or malformed 'minSendable'/'maxSendable' field in lnurl6 response. {e=!r}") from e + # parse lnurl6 "commentAllowed" (optional, described in lnurl-12) + try: + comment_allowed = int(lnurl_dict['commentAllowed']) if 'commentAllowed' in lnurl_dict else 0 + except Exception as e: + raise LNURLError(f"Malformed 'commentAllowed' field in lnurl6 response. {e=!r}") from e data = LNURL6Data( - callback_url=lnurl_dict['callback'], - max_sendable_sat=int(lnurl_dict['maxSendable']) // 1000, - min_sendable_sat=int(lnurl_dict['minSendable']) // 1000, + callback_url=callback_url, + max_sendable_sat=max_sendable_sat, + min_sendable_sat=min_sendable_sat, metadata_plaintext=metadata_plaintext, - comment_allowed=int(lnurl_dict['commentAllowed']) if 'commentAllowed' in lnurl_dict else 0 + comment_allowed=comment_allowed, ) return data @@ -93,14 +116,20 @@ async def request_lnurl(url: str) -> LNURL6Data: async def callback_lnurl(url: str, params: dict) -> dict: """Requests an invoice from a lnurl supporting server.""" try: - response = await Network.async_send_http_on_proxy("get", url, params=params) + response_raw = await Network.async_send_http_on_proxy("get", url, params=params) + except asyncio.TimeoutError as e: + raise LNURLError("Server did not reply in time.") from e except aiohttp.client_exceptions.ClientError as e: raise LNURLError(f"Client error: {e}") from e - # TODO: handling of specific errors - response = json.loads(response) + try: + response = json.loads(response_raw) + except json.JSONDecodeError: + raise LNURLError(f"Invalid response from server") + status = response.get("status") if status and status == "ERROR": - raise LNURLError(f"LNURL request encountered an error: {response['reason']}") + raise LNURLError(f"LNURL request encountered an error: {response.get('reason', '')}") + # TODO: handling of specific errors (validate fields, e.g. for lnurl6) return response From 1b5c7d46d72963ffe7fc345c91d87d9eaab47251 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 18:08:42 +0000 Subject: [PATCH 0771/1143] lnurl: forbid paying to "http://" lnurls (enforce https or .onion) In theory merchants should only use safeish non-mitm-able schemes, but let's add this sanity check for peace of mind. --- electrum/lnurl.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/electrum/lnurl.py b/electrum/lnurl.py index a4d376354..27d020cb1 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -6,6 +6,7 @@ import json from typing import Callable, Optional, NamedTuple, Any, TYPE_CHECKING import re +import urllib.parse import aiohttp.client_exceptions from aiohttp import ClientResponse @@ -44,6 +45,15 @@ def decode_lnurl(lnurl: str) -> str: return url +def _is_url_safe_enough_for_lnurl(url: str) -> bool: + u = urllib.parse.urlparse(url) + if u.scheme.lower() == "https": + return True + if u.netloc.endswith(".onion"): + return True + return False + + class LNURL6Data(NamedTuple): callback_url: str max_sendable_sat: int @@ -55,6 +65,8 @@ class LNURL6Data(NamedTuple): async def _request_lnurl(url: str) -> dict: """Requests payment data from a lnurl.""" + if not _is_url_safe_enough_for_lnurl(url): + raise LNURLError(f"This lnurl looks unsafe. It must use 'https://' or '.onion' (found: {url[:10]}...)") try: response_raw = await Network.async_send_http_on_proxy("get", url, timeout=10) except asyncio.TimeoutError as e: @@ -92,6 +104,8 @@ async def request_lnurl(url: str) -> LNURL6Data: callback_url = lnurl_dict['callback'] except KeyError as e: raise LNURLError(f"Missing 'callback' field in lnurl6 response.") from e + if not _is_url_safe_enough_for_lnurl(callback_url): + raise LNURLError(f"This lnurl callback_url looks unsafe. It must use 'https://' or '.onion' (found: {callback_url[:10]}...)") # parse lnurl6 "minSendable"/"maxSendable" try: max_sendable_sat = int(lnurl_dict['maxSendable']) // 1000 @@ -115,6 +129,8 @@ async def request_lnurl(url: str) -> LNURL6Data: async def callback_lnurl(url: str, params: dict) -> dict: """Requests an invoice from a lnurl supporting server.""" + if not _is_url_safe_enough_for_lnurl(url): + raise LNURLError(f"This lnurl looks unsafe. It must use 'https://' or '.onion' (found: {url[:10]}...)") try: response_raw = await Network.async_send_http_on_proxy("get", url, params=params) except asyncio.TimeoutError as e: From 1aa14e749ac99bc8aeb3881700b549856e883f95 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 20 Apr 2023 18:38:32 +0200 Subject: [PATCH 0772/1143] qml: first part of partially signing tx while not having txid yet --- electrum/gui/qml/components/TxDetails.qml | 6 +-- electrum/gui/qml/qetxdetails.py | 25 +++++------- electrum/gui/qml/qetxfinalizer.py | 27 ++++--------- electrum/gui/qml/qewallet.py | 47 +++++++++++++++-------- 4 files changed, 51 insertions(+), 54 deletions(-) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 8531b2175..abc8bd1d0 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -448,7 +448,7 @@ Pane { onAccepted: { root.rawtx = rbffeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign_and_broadcast() + txdetails.signAndBroadcast() } else { var dialog = app.messageDialog.createObject(app, { title: qsTr('Transaction fee updated.'), @@ -475,7 +475,7 @@ Pane { // replaces parent tx with cpfp tx root.rawtx = cpfpfeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign_and_broadcast() + txdetails.signAndBroadcast() } else { var dialog = app.messageDialog.createObject(app, { title: qsTr('CPFP fee bump transaction created.'), @@ -501,7 +501,7 @@ Pane { onAccepted: { root.rawtx = txcanceller.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { - txdetails.sign_and_broadcast() + txdetails.signAndBroadcast() } else { var dialog = app.messageDialog.createObject(app, { title: qsTr('Cancel transaction created.'), diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 5897eb3a3..596d06b47 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -310,7 +310,7 @@ def update_mined_status(self, tx_mined_info: TxMinedInfo): self._short_id = tx_mined_info.short_id() or "" @pyqtSlot() - def sign_and_broadcast(self): + def signAndBroadcast(self): self._sign(broadcast=True) @pyqtSlot() @@ -320,20 +320,24 @@ def sign(self): def _sign(self, broadcast): # TODO: connecting/disconnecting signal handlers here is hmm try: - self._wallet.transactionSigned.disconnect(self.onSigned) - self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) if broadcast: + self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) except: pass - self._wallet.transactionSigned.connect(self.onSigned) - self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded) + if broadcast: + self._wallet.broadcastSucceeded.connect(self.onBroadcastSucceeded) self._wallet.broadcastFailed.connect(self.onBroadcastFailed) - self._wallet.sign(self._tx, broadcast=broadcast) + + self._wallet.sign(self._tx, broadcast=broadcast, on_success=self.on_signed_tx) # side-effect: signing updates self._tx # we rely on this for broadcast + def on_signed_tx(self, tx: Transaction): + self._logger.debug('on_signed_tx') + self.update() + @pyqtSlot() def broadcast(self): assert self._tx.is_complete() @@ -349,15 +353,6 @@ def broadcast(self): self._wallet.broadcast(self._tx) - @pyqtSlot(str) - def onSigned(self, txid): - if txid != self._txid: - return - - self._logger.debug('onSigned') - self._wallet.transactionSigned.disconnect(self.onSigned) - self.update() - @pyqtSlot(str) def onBroadcastSucceeded(self, txid): if txid != self._txid: diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 593d3bf56..17b952d0e 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -5,7 +5,7 @@ from electrum.logging import get_logger from electrum.i18n import _ -from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction from electrum.util import NotEnoughFunds, profiler from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP from electrum.network import NetworkException @@ -368,31 +368,18 @@ def signAndSave(self): self._logger.error('no valid tx') return - # TODO: f_accept handler not used - # if self.f_accept: - # self.f_accept(self._tx) - # return - - try: - self._wallet.transactionSigned.disconnect(self.onSigned) - except: - pass - self._wallet.transactionSigned.connect(self.onSigned) - self._wallet.sign(self._tx) - - @pyqtSlot(str) - def onSigned(self, txid): - if txid != self._tx.txid(): - return - - self._logger.debug('onSigned') - self._wallet.transactionSigned.disconnect(self.onSigned) + self._wallet.sign(self._tx, broadcast=False, on_success=self.on_signed_tx) + def on_signed_tx(self, tx: Transaction): + self._logger.debug('on_signed_tx') if not self._wallet.save_tx(self._tx): self._logger.error('Could not save tx') else: + # FIXME: don't rely on txid. (non-segwit tx don't have a txid + # until tx is complete, and can't save to backend without it). self.finishedSave.emit(self._tx.txid()) + # mixin for watching an existing TX based on its txid for verified event # requires self._wallet to contain a QEWallet instance # exposes txid qt property diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 025e3ad82..bd30127ef 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -2,7 +2,7 @@ import queue import threading import time -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple, Callable from functools import partial from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, QMetaObject, Qt @@ -12,7 +12,7 @@ from electrum.invoices import InvoiceError, PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_BROADCASTING, PR_BROADCAST from electrum.logging import get_logger from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.transaction import PartialTxOutput, PartialTransaction +from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction from electrum.util import parse_max_spend, InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop from electrum.plugin import run_hook from electrum.wallet import Multisig_Wallet @@ -62,7 +62,8 @@ def getInstanceFor(cls, wallet): paymentSucceeded = pyqtSignal([str], arguments=['key']) paymentFailed = pyqtSignal([str,str], arguments=['key','reason']) requestNewPassword = pyqtSignal() - transactionSigned = pyqtSignal([str], arguments=['txid']) + signSucceeded = pyqtSignal([str], arguments=['txid']) + signFailed = pyqtSignal([str], arguments=['message']) broadcastSucceeded = pyqtSignal([str], arguments=['txid']) broadcastFailed = pyqtSignal([str,str,str], arguments=['txid','code','reason']) saveTxSuccess = pyqtSignal([str], arguments=['txid']) @@ -486,28 +487,37 @@ def enableLightning(self): self.dataChanged.emit() @auth_protect() - def sign(self, tx, *, broadcast: bool = False): - sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast), - self.on_sign_failed) + def sign(self, tx, *, broadcast: bool = False, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[], None] = None): + sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, on_success, broadcast), partial(self.on_sign_failed, on_failure)) if sign_hook: - self.do_sign(tx, False) - self._logger.debug('plugin needs to sign tx too') - sign_hook(tx) - return + signSuccess = self.do_sign(tx, False) + if signSuccess: + self._logger.debug('plugin needs to sign tx too') + sign_hook(tx) + return + else: + signSuccess = self.do_sign(tx, broadcast) - self.do_sign(tx, broadcast) + if signSuccess: + if on_success: on_success(tx) + else: + if on_failure: on_failure() def do_sign(self, tx, broadcast): - tx = self.wallet.sign_transaction(tx, self.password) + try: + tx = self.wallet.sign_transaction(tx, self.password) + except BaseException as e: + self._logger.error(f'{e!r}') + self.signFailed.emit(str(e)) if tx is None: self._logger.info('did not sign') - return + return False txid = tx.txid() self._logger.debug(f'do_sign(), txid={txid}') - self.transactionSigned.emit(txid) + self.signSucceeded.emit(txid) if not tx.is_complete(): self._logger.debug('tx not complete') @@ -519,14 +529,19 @@ def do_sign(self, tx, broadcast): # not broadcasted, so refresh history here self.historyModel.init_model(True) + return True + # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok - def on_sign_complete(self, broadcast, tx): + def on_sign_complete(self, broadcast, cb: Callable[[Transaction], None] = None, tx: Transaction = None): self.otpSuccess.emit() + if cb: cb(tx) if broadcast: self.broadcast(tx) - def on_sign_failed(self, error): + # this assumes a 2fa wallet, but there are no other tc_sign_wrapper hooks, so that's ok + def on_sign_failed(self, cb: Callable[[], None] = None, error: str = None): self.otpFailed.emit('error', error) + if cb: cb() def request_otp(self, on_submit): self._otp_on_submit = on_submit From 75e65c5cc70fb72af86b24a23e49bca88955cb9a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 20 Apr 2023 22:26:45 +0000 Subject: [PATCH 0773/1143] qml: virtual keyboard: make margins smaller, for larger buttons Looking at different system-wide keyboards on different phones, these new smaller margin sizes should still be sufficient; and this lets the buttons be larger. --- .../qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml index a016f25a4..506f0ff53 100644 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml +++ b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml @@ -56,10 +56,10 @@ KeyboardStyle { keyboardDesignWidth: 2560 keyboardDesignHeight: 1440 - keyboardRelativeLeftMargin: 114 / keyboardDesignWidth - keyboardRelativeRightMargin: 114 / keyboardDesignWidth - keyboardRelativeTopMargin: 13 / keyboardDesignHeight - keyboardRelativeBottomMargin: 86 / keyboardDesignHeight + keyboardRelativeLeftMargin: 32 / keyboardDesignWidth + keyboardRelativeRightMargin: 32 / keyboardDesignWidth + keyboardRelativeTopMargin: 10 / keyboardDesignHeight + keyboardRelativeBottomMargin: 28 / keyboardDesignHeight keyboardBackground: Rectangle { color: constants.colorAlpha(Material.accentColor, 0.5) //mutedForeground //'red' //"black" From b9ec04f13af90351fc5bbfd9006c4ec8cdfaac0d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 13:19:23 +0200 Subject: [PATCH 0774/1143] qml: make txdetails less reliant on txid --- .../gui/qml/components/CpfpBumpFeeDialog.qml | 1 - .../gui/qml/components/RbfBumpFeeDialog.qml | 1 - .../gui/qml/components/RbfCancelDialog.qml | 1 - electrum/gui/qml/components/TxDetails.qml | 25 ++++++++++++------- electrum/gui/qml/qetxdetails.py | 17 ++++++------- electrum/gui/qml/qewallet.py | 8 +++--- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index 94811b27d..d0a17cbcd 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -10,7 +10,6 @@ import "controls" ElDialog { id: dialog - required property string txid required property QtObject cpfpfeebumper title: qsTr('Bump Fee') diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index e613b46d6..cdffa49c3 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -10,7 +10,6 @@ import "controls" ElDialog { id: dialog - required property string txid required property QtObject rbffeebumper title: qsTr('Bump Fee') diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index b6727e96c..6a821a941 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -10,7 +10,6 @@ import "controls" ElDialog { id: dialog - required property string txid required property QtObject txcanceller title: qsTr('Cancel Transaction') diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index abc8bd1d0..624f5aaab 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -15,7 +15,6 @@ Pane { property string txid property string rawtx - property alias label: txdetails.label signal detailsChanged @@ -311,9 +310,9 @@ Pane { visible: txdetails.canBump || txdetails.canCpfp onClicked: { if (txdetails.canBump) { - var dialog = rbfBumpFeeDialog.createObject(root, { txid: root.txid }) + var dialog = rbfBumpFeeDialog.createObject(root, { txid: txdetails.txid }) } else { - var dialog = cpfpBumpFeeDialog.createObject(root, { txid: root.txid }) + var dialog = cpfpBumpFeeDialog.createObject(root, { txid: txdetails.txid }) } dialog.open() } @@ -326,7 +325,7 @@ Pane { text: qsTr('Cancel Tx') visible: txdetails.canCancel onClicked: { - var dialog = rbfCancelDialog.createObject(root, { txid: root.txid }) + var dialog = rbfCancelDialog.createObject(root, { txid: txdetails.txid }) dialog.open() } } @@ -400,8 +399,6 @@ Pane { TxDetails { id: txdetails wallet: Daemon.currentWallet - txid: root.txid - rawtx: root.rawtx onLabelChanged: root.detailsChanged() onConfirmRemoveLocalTx: { var dialog = app.messageDialog.createObject(app, { text: message, yesno: true }) @@ -411,6 +408,13 @@ Pane { }) dialog.open() } + Component.onCompleted: { + if (root.txid) { + txdetails.txid = root.txid + } else if (root.rawtx) { + txdetails.rawtx = root.rawtx + } + } } Connections { @@ -440,13 +444,14 @@ Pane { id: rbfBumpFeeDialog RbfBumpFeeDialog { id: dialog + required property string txid rbffeebumper: TxRbfFeeBumper { id: rbffeebumper wallet: Daemon.currentWallet txid: dialog.txid } onAccepted: { - root.rawtx = rbffeebumper.getNewTx() + txdetails.rawtx = rbffeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.signAndBroadcast() } else { @@ -465,6 +470,7 @@ Pane { id: cpfpBumpFeeDialog CpfpBumpFeeDialog { id: dialog + required property string txid cpfpfeebumper: TxCpfpFeeBumper { id: cpfpfeebumper wallet: Daemon.currentWallet @@ -473,7 +479,7 @@ Pane { onAccepted: { // replaces parent tx with cpfp tx - root.rawtx = cpfpfeebumper.getNewTx() + txdetails.rawtx = cpfpfeebumper.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.signAndBroadcast() } else { @@ -492,6 +498,7 @@ Pane { id: rbfCancelDialog RbfCancelDialog { id: dialog + required property string txid txcanceller: TxCanceller { id: txcanceller wallet: Daemon.currentWallet @@ -499,7 +506,7 @@ Pane { } onAccepted: { - root.rawtx = txcanceller.getNewTx() + txdetails.rawtx = txcanceller.getNewTx() if (txdetails.wallet.canSignWithoutCosigner) { txdetails.signAndBroadcast() } else { diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 596d06b47..3a9bda646 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -95,10 +95,10 @@ def txid(self): @txid.setter def txid(self, txid: str): if self._txid != txid: - self._logger.debug('txid set -> %s' % txid) + self._logger.debug(f'txid set -> {txid}') self._txid = txid self.txidChanged.emit() - self.update() + self.update(from_txid=True) @pyqtProperty(str, notify=detailsChanged) def rawtx(self): @@ -107,13 +107,14 @@ def rawtx(self): @rawtx.setter def rawtx(self, rawtx: str): if self._rawtx != rawtx: - self._logger.debug('rawtx set -> %s' % rawtx) + self._logger.debug(f'rawtx set -> {rawtx}') self._rawtx = rawtx if not rawtx: return try: self._tx = tx_from_any(rawtx, deserialize=True) - self.txid = self._tx.txid() # triggers update() + self._txid = self._tx.txid() + self.update() except Exception as e: self._tx = None self._logger.error(repr(e)) @@ -226,12 +227,10 @@ def isComplete(self): def isFinal(self): return self._is_final - def update(self): - if self._wallet is None: - self._logger.error('wallet undefined') - return + def update(self, from_txid: bool = False): + assert self._wallet - if not self._rawtx: + if from_txid: self._tx = self._wallet.wallet.db.get_transaction(self._txid) assert self._tx is not None diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index bd30127ef..7b5b030e0 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -490,15 +490,15 @@ def enableLightning(self): def sign(self, tx, *, broadcast: bool = False, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[], None] = None): sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, on_success, broadcast), partial(self.on_sign_failed, on_failure)) if sign_hook: - signSuccess = self.do_sign(tx, False) - if signSuccess: + success = self.do_sign(tx, False) + if success: self._logger.debug('plugin needs to sign tx too') sign_hook(tx) return else: - signSuccess = self.do_sign(tx, broadcast) + success = self.do_sign(tx, broadcast) - if signSuccess: + if success: if on_success: on_success(tx) else: if on_failure: on_failure() From 03d9000e79a514454aaa14252604faec1e7be7ad Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 14:50:08 +0200 Subject: [PATCH 0775/1143] qml: fix a few texts that should wrap --- electrum/gui/qml/components/wizard/WCCosignerKeystore.qml | 4 +++- electrum/gui/qml/components/wizard/WCHaveMasterKey.qml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml index 94e5895b4..6e43676a8 100644 --- a/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml +++ b/electrum/gui/qml/components/wizard/WCCosignerKeystore.qml @@ -65,7 +65,7 @@ WizardComponent { } Rectangle { - Layout.preferredWidth: parent.width + Layout.fillWidth: true Layout.preferredHeight: 1 Layout.topMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingLarge @@ -74,7 +74,9 @@ WizardComponent { } Label { + Layout.fillWidth: true text: qsTr('Add cosigner #%1 of %2 to your multi-sig wallet').arg(cosigner).arg(participants) + wrapMode: Text.Wrap } RadioButton { ButtonGroup.group: keystoregroup diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index 558615573..09cb67347 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -89,7 +89,7 @@ WizardComponent { } Rectangle { - Layout.preferredWidth: parent.width + Layout.fillWidth: true Layout.preferredHeight: 1 Layout.topMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingLarge @@ -103,9 +103,11 @@ WizardComponent { } Label { + Layout.fillWidth: true text: cosigner ? qsTr('Enter cosigner master public key') : qsTr('Create keystore from a master key') + wrapMode: Text.Wrap } RowLayout { From 3cec6cdcfb87dd1331f6904a08e890b2d8cb6bb2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 15:09:33 +0200 Subject: [PATCH 0776/1143] qml: second part of partially signing tx while not having txid yet --- .../gui/qml/components/WalletMainView.qml | 31 ++++++++----- electrum/gui/qml/qetxfinalizer.py | 44 +++++++++++++------ 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 0940365b9..0a7d3de38 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -51,10 +51,6 @@ Item { } } - function showExportByTxid(txid, helptext) { - showExport(Daemon.currentWallet.getSerializedTx(txid), helptext) - } - function showExport(data, helptext) { var dialog = exportTxDialog.createObject(app, { text: data[0], @@ -334,7 +330,7 @@ Item { if (Daemon.currentWallet.isWatchOnly) { dialog.finalizer.save() } else { - dialog.finalizer.signAndSave() + dialog.finalizer.sign() } } else { dialog.finalizer.signAndSend() @@ -430,13 +426,24 @@ Item { finalizer: TxFinalizer { wallet: Daemon.currentWallet canRbf: true - onFinishedSave: { - if (wallet.isWatchOnly) { - // tx was saved. Show QR for signer(s) - showExportByTxid(txid, qsTr('Transaction created. Present this QR code to the signing device')) - } else { - // tx was (partially) signed and saved. Show QR for co-signers or online wallet - showExportByTxid(txid, qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer')) + onFinished: { + if (!complete) { + var msg + if (wallet.isWatchOnly) { + // tx created in watchonly wallet. Show QR for signer(s) + if (wallet.isMultisig) { + msg = qsTr('Transaction created. Present this QR code to one of the co-cigners or signing devices') + } else { + msg = qsTr('Transaction created. Present this QR code to the signing device') + } + } else { + if (signed) { + msg = qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer') + } else { + msg = qsTr('Transaction created but not signed by this wallet yet. Sign the transaction and present this QR code to the next co-signer') + } + } + showExport(getSerializedTx(), msg) } _confirmPaymentDialog.destroy() } diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 17b952d0e..cbab6f75e 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -1,5 +1,6 @@ from decimal import Decimal from typing import Optional +from functools import partial from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject @@ -220,7 +221,7 @@ def update_outputs_from_tx(self, tx): class QETxFinalizer(TxFeeSlider): _logger = get_logger(__name__) - finishedSave = pyqtSignal([str], arguments=['txid']) + finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete']) def __init__(self, parent=None, *, make_tx=None, accept=None): super().__init__(parent) @@ -343,12 +344,16 @@ def update(self): @pyqtSlot() def save(self): - if not self._valid or not self._tx: - self._logger.debug('no valid tx') + if not self._valid or not self._tx or not self._tx.txid(): + self._logger.debug('no valid tx or no txid') return + saved = False if self._wallet.save_tx(self._tx): - self.finishedSave.emit(self._tx.txid()) + saved = True + # self.finishedSave.emit(self._tx.txid()) + + self.finished.emit(False, saved) @pyqtSlot() def signAndSend(self): @@ -360,25 +365,36 @@ def signAndSend(self): self.f_accept(self._tx) return - self._wallet.sign(self._tx, broadcast=True) + self._wallet.sign(self._tx, + broadcast=True, + on_success=partial(self.on_signed_tx, False) + ) @pyqtSlot() - def signAndSave(self): + def sign(self): if not self._valid or not self._tx: self._logger.error('no valid tx') return - self._wallet.sign(self._tx, broadcast=False, on_success=self.on_signed_tx) + self._wallet.sign(self._tx, + broadcast=False, + on_success=partial(self.on_signed_tx, True) + ) - def on_signed_tx(self, tx: Transaction): + def on_signed_tx(self, save: bool, tx: Transaction): self._logger.debug('on_signed_tx') - if not self._wallet.save_tx(self._tx): - self._logger.error('Could not save tx') - else: - # FIXME: don't rely on txid. (non-segwit tx don't have a txid - # until tx is complete, and can't save to backend without it). - self.finishedSave.emit(self._tx.txid()) + saved = False + if save and self._tx.txid(): + if self._wallet.save_tx(self._tx): + saved = True + else: + self._logger.error('Could not save tx') + self.finished.emit(True, saved, tx.is_complete()) + @pyqtSlot(result='QVariantList') + def getSerializedTx(self): + txqr = self._tx.to_qr_data() + return [str(self._tx), txqr[0], txqr[1]] # mixin for watching an existing TX based on its txid for verified event # requires self._wallet to contain a QEWallet instance From 784fc27cb9383c19d3e4e5f8ff7e55b5edbc0312 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 14:05:21 +0000 Subject: [PATCH 0777/1143] libsecp256k1: add runtime support for 0.3.x this replaces https://github.com/spesmilo/electrum/pull/8320 see https://github.com/bitcoin-core/secp256k1/blob/f6bef03c0a2c826227708dbeaecf1dbc702a2567/CHANGELOG.md I am not yet sure how this will look like going forward, but unless there will be lots of libsecp256k1 releases with ~invisible harmless ABI changes, I think conceptually this is the right approach. --- electrum/ecc_fast.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index fcd13c64d..ba75498f2 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -37,16 +37,23 @@ class LibModuleMissing(Exception): pass def load_library(): + # note: for a mapping between bitcoin-core/secp256k1 git tags and .so.V libtool version numbers, + # see https://github.com/bitcoin-core/secp256k1/pull/1055#issuecomment-1227505189 + tested_libversions = [2, 1, 0, ] # try latest version first + libnames = [] if sys.platform == 'darwin': - libnames = ['libsecp256k1.1.dylib', 'libsecp256k1.0.dylib', ] + for v in tested_libversions: + libnames.append(f"libsecp256k1.{v}.dylib") elif sys.platform in ('windows', 'win32'): - libnames = ['libsecp256k1-1.dll', 'libsecp256k1-0.dll', ] + for v in tested_libversions: + libnames.append(f"libsecp256k1-{v}.dll") elif 'ANDROID_DATA' in os.environ: - libnames = ['libsecp256k1.so', ] - elif 'freebsd' in sys.platform: - libnames = ['libsecp256k1.so', ] + libnames = ['libsecp256k1.so', ] # don't care about version number. we built w/e is available. else: # desktop Linux and similar - libnames = ['libsecp256k1.so.1', 'libsecp256k1.so.0', ] + for v in tested_libversions: + libnames.append(f"libsecp256k1.so.{v}") + # maybe we could fall back to trying "any" version? maybe guarded with an env var? + #libnames.append(f"libsecp256k1.so") library_paths = [] for libname in libnames: # try local files in repo dir first library_paths.append(os.path.join(os.path.dirname(__file__), libname)) From 2a2b683d2334adc74dcf426d07aa6a923473f723 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 14:13:32 +0000 Subject: [PATCH 0778/1143] bump libsecp256k1 version --- contrib/android/p4a_recipes/libsecp256k1/__init__.py | 4 ++-- contrib/make_libsecp256k1.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/android/p4a_recipes/libsecp256k1/__init__.py b/contrib/android/p4a_recipes/libsecp256k1/__init__.py index a1b969f7d..a639fcf89 100644 --- a/contrib/android/p4a_recipes/libsecp256k1/__init__.py +++ b/contrib/android/p4a_recipes/libsecp256k1/__init__.py @@ -6,9 +6,9 @@ class LibSecp256k1RecipePinned(LibSecp256k1Recipe): - version = "21ffe4b22a9683cf24ae0763359e401d1284cc7a" + version = "346a053d4c442e08191f075c3932d03140579d47" url = "https://github.com/bitcoin-core/secp256k1/archive/{version}.zip" - sha512sum = "51832bfc6825690d5b71a5426aacce8981163ca1a56a235394aa86e742d105f5e2b331971433a21b8842ee338cbd7877dcbae5605fa01a9e6f4a73171b93f3e9" + sha512sum = "d6232bd8fb29395984b15633bee582e7588ade0ec1c7bea5b2cab766b1ff657672b804e078656e0ce4067071140b0552d12ce3c01866231b212f3c65908b85aa" recipe = LibSecp256k1RecipePinned() diff --git a/contrib/make_libsecp256k1.sh b/contrib/make_libsecp256k1.sh index 7400e9313..11cee4078 100755 --- a/contrib/make_libsecp256k1.sh +++ b/contrib/make_libsecp256k1.sh @@ -14,8 +14,8 @@ # sudo apt-get install gcc-multilib g++-multilib # $ AUTOCONF_FLAGS="--host=i686-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32" ./contrib/make_libsecp256k1.sh -LIBSECP_VERSION="21ffe4b22a9683cf24ae0763359e401d1284cc7a" -# ^ tag "v0.2.0" +LIBSECP_VERSION="346a053d4c442e08191f075c3932d03140579d47" +# ^ tag "v0.3.1" set -e From bd897b095514c04624e75cd8e233f997934d08f1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 16:17:44 +0200 Subject: [PATCH 0779/1143] qml: translate --- electrum/gui/qml/components/Addresses.qml | 2 +- .../controls/HistoryItemDelegate.qml | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index 3008c9612..76fa768e2 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -90,7 +90,7 @@ Pane { Label { id: labelLabel font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall - text: model.label != '' ? model.label : '' + text: model.label != '' ? model.label : qsTr('') opacity: model.label != '' ? 1.0 : 0.8 elide: Text.ElideRight maximumLineCount: 2 diff --git a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml index 66599ffe9..28e26cfcc 100644 --- a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml +++ b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml @@ -49,13 +49,13 @@ Item { Image { readonly property variant tx_icons : [ - "../../../icons/unconfirmed.png", - "../../../icons/clock1.png", - "../../../icons/clock2.png", - "../../../icons/clock3.png", - "../../../icons/clock4.png", - "../../../icons/clock5.png", - "../../../icons/confirmed_bw.png" + '../../../icons/unconfirmed.png', + '../../../icons/clock1.png', + '../../../icons/clock2.png', + '../../../icons/clock3.png', + '../../../icons/clock4.png', + '../../../icons/clock5.png', + '../../../icons/confirmed_bw.png' ] Layout.preferredWidth: constants.iconSizeLarge @@ -63,7 +63,7 @@ Item { Layout.alignment: Qt.AlignVCenter Layout.rowSpan: 2 source: model.lightning - ? "../../../icons/lightning.png" + ? '../../../icons/lightning.png' : model.complete && model.section != 'local' ? tx_icons[Math.min(6,model.confirmations)] : '../../../icons/offline_tx.png' @@ -72,7 +72,7 @@ Item { Label { Layout.fillWidth: true font.pixelSize: model.label !== '' ? constants.fontSizeLarge : constants.fontSizeMedium - text: model.label !== '' ? model.label : '' + text: model.label !== '' ? model.label : qsTr('') color: model.label !== '' ? Material.foreground : constants.mutedForeground wrapMode: Text.Wrap maximumLineCount: 2 @@ -119,11 +119,10 @@ Item { Rectangle { visible: delegate.ListView.section == delegate.ListView.nextSection - // Layout.fillWidth: true Layout.preferredWidth: parent.width * 2/3 Layout.alignment: Qt.AlignHCenter Layout.preferredHeight: constants.paddingTiny - color: Material.background //Qt.rgba(0,0,0,0.10) + color: Material.background } } From ae8501e5bef8999fe5415c1d80b3f9697600c166 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 14:35:37 +0000 Subject: [PATCH 0780/1143] qml: small fix in qetxfinalizer.py follow-up 3cec6cdcfb87dd1331f6904a08e890b2d8cb6bb2 --- electrum/gui/qml/qetxfinalizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index cbab6f75e..659b4386b 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -353,7 +353,7 @@ def save(self): saved = True # self.finishedSave.emit(self._tx.txid()) - self.finished.emit(False, saved) + self.finished.emit(False, saved, self._tx.is_complete()) @pyqtSlot() def signAndSend(self): From 2f8ab8027b665e71afd6ad6fac1ffcb6ceee723d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 16:36:45 +0200 Subject: [PATCH 0781/1143] qml: split off AddressDelegate and handle imported addresses more gracefully --- .../gui/qml/components/AddressDetails.qml | 2 + electrum/gui/qml/components/Addresses.qml | 87 +---------------- .../components/controls/AddressDelegate.qml | 95 +++++++++++++++++++ electrum/gui/qml/qeaddresslistmodel.py | 14 ++- 4 files changed, 107 insertions(+), 91 deletions(-) create mode 100644 electrum/gui/qml/components/controls/AddressDelegate.qml diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index cb58e668c..6cbe6e848 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -200,11 +200,13 @@ Pane { } Label { + visible: addressdetails.derivationPath text: qsTr('Derivation path') color: Material.accentColor } Label { + visible: addressdetails.derivationPath text: addressdetails.derivationPath } diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index 76fa768e2..f56aadddd 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -31,14 +31,7 @@ Pane { section.criteria: ViewSection.FullString section.delegate: sectionDelegate - delegate: ItemDelegate { - id: delegate - width: ListView.view.width - height: delegateLayout.height - highlighted: ListView.isCurrentItem - - font.pixelSize: constants.fontSizeMedium // set default font size for child controls - + delegate: AddressDelegate { onClicked: { var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {'address': model.address}) page.addressDetailsChanged.connect(function() { @@ -46,84 +39,6 @@ Pane { listview.model.update_address(model.address) }) } - - ColumnLayout { - id: delegateLayout - width: parent.width - spacing: 0 - - GridLayout { - columns: 2 - Layout.topMargin: constants.paddingSmall - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge - - Label { - id: indexLabel - font.bold: true - text: '#' + ('00'+model.iaddr).slice(-2) - Layout.fillWidth: true - } - Label { - font.family: FixedFont - text: model.address - elide: Text.ElideMiddle - Layout.fillWidth: true - } - - Rectangle { - id: useIndicator - Layout.preferredWidth: constants.iconSizeMedium - Layout.preferredHeight: constants.iconSizeMedium - color: model.held - ? constants.colorAddressFrozen - : model.numtx > 0 - ? model.balance.satsInt == 0 - ? constants.colorAddressUsed - : constants.colorAddressUsedWithBalance - : model.type == 'receive' - ? constants.colorAddressExternal - : constants.colorAddressInternal - } - - RowLayout { - Label { - id: labelLabel - font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall - text: model.label != '' ? model.label : qsTr('') - opacity: model.label != '' ? 1.0 : 0.8 - elide: Text.ElideRight - maximumLineCount: 2 - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - Label { - font.family: FixedFont - text: Config.formatSats(model.balance, false) - visible: model.balance.satsInt != 0 - } - Label { - color: Material.accentColor - text: Config.baseUnit + ',' - visible: model.balance.satsInt != 0 - } - Label { - text: model.numtx - visible: model.numtx > 0 - } - Label { - color: Material.accentColor - text: qsTr('tx') - visible: model.numtx > 0 - } - } - } - - Item { - Layout.preferredWidth: 1 - Layout.preferredHeight: constants.paddingSmall - } - } } ScrollIndicator.vertical: ScrollIndicator { } diff --git a/electrum/gui/qml/components/controls/AddressDelegate.qml b/electrum/gui/qml/components/controls/AddressDelegate.qml new file mode 100644 index 000000000..f81171537 --- /dev/null +++ b/electrum/gui/qml/components/controls/AddressDelegate.qml @@ -0,0 +1,95 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.0 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +ItemDelegate { + id: delegate + width: ListView.view.width + height: delegateLayout.height + highlighted: ListView.isCurrentItem + + font.pixelSize: constants.fontSizeMedium // set default font size for child controls + + ColumnLayout { + id: delegateLayout + width: parent.width + spacing: 0 + + GridLayout { + columns: 2 + Layout.topMargin: constants.paddingSmall + Layout.leftMargin: constants.paddingLarge + Layout.rightMargin: constants.paddingLarge + + Label { + id: indexLabel + font.bold: true + text: model.iaddr < 10 + ? '#' + ('0'+model.iaddr).slice(-2) + : '#' + model.iaddr + Layout.fillWidth: true + } + Label { + font.family: FixedFont + text: model.address + elide: Text.ElideMiddle + Layout.fillWidth: true + } + + Rectangle { + id: useIndicator + Layout.preferredWidth: constants.iconSizeMedium + Layout.preferredHeight: constants.iconSizeMedium + color: model.held + ? constants.colorAddressFrozen + : model.numtx > 0 + ? model.balance.satsInt == 0 + ? constants.colorAddressUsed + : constants.colorAddressUsedWithBalance + : model.type == 'change' + ? constants.colorAddressInternal + : constants.colorAddressExternal + } + + RowLayout { + Label { + id: labelLabel + font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall + text: model.label != '' ? model.label : qsTr('') + opacity: model.label != '' ? 1.0 : 0.8 + elide: Text.ElideRight + maximumLineCount: 2 + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + Label { + font.family: FixedFont + text: Config.formatSats(model.balance, false) + visible: model.balance.satsInt != 0 + } + Label { + color: Material.accentColor + text: Config.baseUnit + ',' + visible: model.balance.satsInt != 0 + } + Label { + text: model.numtx + visible: model.numtx > 0 + } + Label { + color: Material.accentColor + text: qsTr('tx') + visible: model.numtx > 0 + } + } + } + + Item { + Layout.preferredWidth: 1 + Layout.preferredHeight: constants.paddingSmall + } + } +} diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py index ba17ae5a9..29c2cb900 100644 --- a/electrum/gui/qml/qeaddresslistmodel.py +++ b/electrum/gui/qml/qeaddresslistmodel.py @@ -73,7 +73,7 @@ def init_model(self): return r_addresses = self.wallet.get_receiving_addresses() - c_addresses = self.wallet.get_change_addresses() + 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): @@ -84,10 +84,14 @@ def insert_row(atype, alist, address, iaddr): self.clear() self.beginInsertRows(QModelIndex(), 0, n_addresses - 1) - for i, address in enumerate(r_addresses): - insert_row('receive', self.receive_addresses, address, i) - for i, address in enumerate(c_addresses): - insert_row('change', self.change_addresses, address, i) + if self.wallet.wallet_type != 'imported': + for i, address in enumerate(r_addresses): + insert_row('receive', self.receive_addresses, address, i) + for i, address in enumerate(c_addresses): + insert_row('change', self.change_addresses, address, i) + else: + for i, address in enumerate(r_addresses): + insert_row('imported', self.receive_addresses, address, i) self.endInsertRows() self._dirty = False From cede16a522dcb686f78d3c4369aba803305ddc87 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 14:42:51 +0000 Subject: [PATCH 0782/1143] libsecp256k1: update hardcoded .so lib name in binaries follow-up 2a2b683d2334adc74dcf426d07aa6a923473f723 TODO: maybe we should drop the version number in the lib name we bundle... --- contrib/build-linux/appimage/make_appimage.sh | 2 +- contrib/build-wine/deterministic.spec | 2 +- contrib/build-wine/make_win.sh | 2 +- contrib/osx/make_osx.sh | 2 +- contrib/osx/osx.spec | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/build-linux/appimage/make_appimage.sh b/contrib/build-linux/appimage/make_appimage.sh index 5db025f8b..ec8c9aeb3 100755 --- a/contrib/build-linux/appimage/make_appimage.sh +++ b/contrib/build-linux/appimage/make_appimage.sh @@ -78,7 +78,7 @@ info "installing python." ) -if [ -f "$DLL_TARGET_DIR/libsecp256k1.so.1" ]; then +if [ -f "$DLL_TARGET_DIR/libsecp256k1.so.2" ]; then info "libsecp256k1 already built, skipping" else "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index e62c38414..c133de4e6 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -31,7 +31,7 @@ binaries = [] # Workaround for "Retro Look": binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]] -binaries += [('C:/tmp/libsecp256k1-1.dll', '.')] +binaries += [('C:/tmp/libsecp256k1-2.dll', '.')] binaries += [('C:/tmp/libusb-1.0.dll', '.')] binaries += [('C:/tmp/libzbar-0.dll', '.')] diff --git a/contrib/build-wine/make_win.sh b/contrib/build-wine/make_win.sh index 5d9423c34..6cda8dcab 100755 --- a/contrib/build-wine/make_win.sh +++ b/contrib/build-wine/make_win.sh @@ -43,7 +43,7 @@ rm "$here"/dist/* -rf mkdir -p "$CACHEDIR" "$DLL_TARGET_DIR" "$PIP_CACHE_DIR" -if [ -f "$DLL_TARGET_DIR/libsecp256k1-1.dll" ]; then +if [ -f "$DLL_TARGET_DIR/libsecp256k1-2.dll" ]; then info "libsecp256k1 already built, skipping" else "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index 615e6dbe7..3f5f0e03d 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -180,7 +180,7 @@ info "generating locale" ) || fail "failed generating locale" -if [ ! -f "$DLL_TARGET_DIR/libsecp256k1.1.dylib" ]; then +if [ ! -f "$DLL_TARGET_DIR/libsecp256k1.2.dylib" ]; then info "Building libsecp256k1 dylib..." "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" else diff --git a/contrib/osx/osx.spec b/contrib/osx/osx.spec index 7b983eea6..b4fb53df9 100644 --- a/contrib/osx/osx.spec +++ b/contrib/osx/osx.spec @@ -50,7 +50,7 @@ datas += collect_data_files('bitbox02') # Add libusb so Trezor and Safe-T mini will work binaries = [(electrum + "electrum/libusb-1.0.dylib", ".")] -binaries += [(electrum + "electrum/libsecp256k1.1.dylib", ".")] +binaries += [(electrum + "electrum/libsecp256k1.2.dylib", ".")] binaries += [(electrum + "electrum/libzbar.0.dylib", ".")] # Workaround for "Retro Look": From c3a418d4dad9cd16c413a286b294d2711f9b1462 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 17:04:21 +0200 Subject: [PATCH 0783/1143] qml: address list heading translatable --- electrum/gui/qml/components/Addresses.qml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index f56aadddd..935b5710d 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -54,13 +54,20 @@ Pane { height: childrenRect.height required property string section + property string section_label: section == 'receive' + ? qsTr('receive addresses') + : section == 'change' + ? qsTr('change addresses') + : section == 'imported' + ? qsTr('imported addresses') + : section + ' ' + qsTr('addresses') ColumnLayout { width: parent.width Heading { Layout.leftMargin: constants.paddingLarge Layout.rightMargin: constants.paddingLarge - text: root.section + ' ' + qsTr('addresses') + text: root.section_label } } } From e9d5e5737ec77bc5fdaab328e3c1fdd0f8bb3c74 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 21 Apr 2023 17:04:59 +0200 Subject: [PATCH 0784/1143] qml: update UI after import key/address, add icon to address/key import dialog --- electrum/gui/qml/components/ImportAddressesKeysDialog.qml | 1 + electrum/gui/qml/qewallet.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml index 703543221..a01e4ae2c 100644 --- a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml +++ b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml @@ -102,6 +102,7 @@ ElDialog { FlatButton { Layout.fillWidth: true + icon.source: '../../icons/add.png' text: qsTr('Import') enabled: valid onClicked: doAccept() diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 7b5b030e0..d88f9817e 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -704,10 +704,12 @@ def set_password(self, password): @pyqtSlot(str) def importAddresses(self, addresslist): self.wallet.import_addresses(addresslist.split()) + self.dataChanged.emit() @pyqtSlot(str) def importPrivateKeys(self, keyslist): self.wallet.import_private_keys(keyslist.split(), self.password) + self.dataChanged.emit() @pyqtSlot(str) def importChannelBackup(self, backup_str): From ca3f48d22e2ce94cbd9a3fb031e542114ffcc6b9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 15:44:24 +0000 Subject: [PATCH 0785/1143] qml: remove buggy "Replace-by-Fee" checkbox from RbfCancelDialog follow-up 02dce339cc00f00333539f10408888ef8b3c5066 --- electrum/gui/qml/components/RbfCancelDialog.qml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 6a821a941..be2fb067b 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -141,17 +141,6 @@ ElDialog { } } - CheckBox { - id: final_cb - text: qsTr('Replace-by-Fee') - Layout.columnSpan: 2 - checked: txcanceller.rbf - onCheckedChanged: { - if (activeFocus) - txcanceller.rbf = checked - } - } - Label { Layout.columnSpan: 2 Layout.fillWidth: true From 2be71c2dcc34f499930975487b85a72d7958bfe8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 16:38:58 +0000 Subject: [PATCH 0786/1143] windows README: update reference to libsecp256k1-0.dll to incl newer related: https://github.com/spesmilo/electrum/pull/8185 --- .cirrus.yml | 4 ++-- contrib/build-wine/README_windows.md | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 34cb5e60f..d68194dc8 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -41,7 +41,7 @@ task: - git tag install_script: - apt-get update - - apt-get -y install libsecp256k1-0 + - apt-get -y install libsecp256k1-dev - pip install -r $ELECTRUM_REQUIREMENTS_CI tox_script: - export PYTHONASYNCIODEBUG @@ -101,7 +101,7 @@ task: populate_script: mkdir -p /tmp/bitcoind install_script: - apt-get update - - apt-get -y install libsecp256k1-0 curl jq bc + - apt-get -y install libsecp256k1-dev curl jq bc - pip3 install .[tests] # install e-x some commits after 1.16.0 tag, where it uses same aiorpcx as electrum - pip3 install git+https://github.com/spesmilo/electrumx.git@c8d2cc0d5cf9e549a90ca876d85fed9a90b8c4ed diff --git a/contrib/build-wine/README_windows.md b/contrib/build-wine/README_windows.md index 82c0efca9..a34cce1f9 100644 --- a/contrib/build-wine/README_windows.md +++ b/contrib/build-wine/README_windows.md @@ -23,7 +23,8 @@ Run install (this should install most dependencies): [libsecp256k1](https://github.com/bitcoin-core/secp256k1) is a required dependency. This is a C library, which you need to compile yourself. -Electrum needs a dll, named `libsecp256k1-0.dll`, placed into the inner `electrum/` folder. +Electrum needs a dll, named `libsecp256k1-0.dll` (or newer `libsecp256k1-*.dll`), +placed into the inner `electrum/` folder. For Unix-like systems, the (`contrib/make_libsecp256k1.sh`) script does this for you, however it does not work on Windows. @@ -53,7 +54,7 @@ Alternatively, MSYS2 and MinGW-w64 can be used directly on Windows, as follows. (note: this is a bit cumbersome, see [issue #5976](https://github.com/spesmilo/electrum/issues/5976) for discussion) -### 3. Run electrum: +### 3. Run electrum: ``` > python3 ./run_electrum From 7d2260595220014ad862eaea76f7dd6cdf4ede8a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 16:36:48 +0000 Subject: [PATCH 0787/1143] README: refer to `libsecp256k1-dev` instead of `libsecp256k1-0` We don't actually need the development headers, instead using this as a hack to be agnostic to the version scheme and pull in the latest. related: https://github.com/spesmilo/electrum/pull/8185 https://github.com/spesmilo/electrum/pull/8320 https://github.com/spesmilo/electrum/issues/8328#issuecomment-1518061250 debian 11 (stable) only has libsecp256k1-0 debian 12 (testing) atm only has libsecp256k1-1 ubuntu 23.04 only has libsecp256k1-1 I expect libsecp256k1-2 might soon get packaged too, now that upstream secp released v0.3.0. So what do we tell users to install? well, turns out most distros have libsecp256k1-dev, which just pulls in the latest secp. Caveat: if there is a new secp release that actually gets packaged on a distro before we can react, then this new instruction will not work. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 683c4ce8e..3e10d8282 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ but not everything. The following sections describe how to run from source, but is a TL;DR: ``` -$ sudo apt-get install libsecp256k1-0 +$ sudo apt-get install libsecp256k1-dev $ python3 -m pip install --user ".[gui,crypto]" ``` @@ -37,7 +37,7 @@ For elliptic curve operations, [libsecp256k1](https://github.com/bitcoin-core/secp256k1) is a required dependency: ``` -$ sudo apt-get install libsecp256k1-0 +$ sudo apt-get install libsecp256k1-dev ``` Alternatively, when running from a cloned repository, a script is provided to build From 2ec4758a1253a9937bff8f60251bb0b0e0b9cd0c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 17:14:49 +0000 Subject: [PATCH 0788/1143] qml: follow-up fix for offline-signing pre-segwit tx follow-up 3cec6cdcfb87dd1331f6904a08e890b2d8cb6bb2 --- electrum/gui/qml/components/WalletMainView.qml | 2 +- electrum/gui/qml/qetxfinalizer.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 0a7d3de38..4dec7ca0a 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -328,7 +328,7 @@ Item { dialog.accepted.connect(function() { if (!canComplete) { if (Daemon.currentWallet.isWatchOnly) { - dialog.finalizer.save() + dialog.finalizer.saveOrShow() } else { dialog.finalizer.sign() } diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 659b4386b..36e129dda 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -343,15 +343,15 @@ def update(self): self.validChanged.emit() @pyqtSlot() - def save(self): - if not self._valid or not self._tx or not self._tx.txid(): - self._logger.debug('no valid tx or no txid') + def saveOrShow(self): + if not self._valid or not self._tx: + self._logger.debug('no valid tx') return saved = False - if self._wallet.save_tx(self._tx): - saved = True - # self.finishedSave.emit(self._tx.txid()) + if self._tx.txid(): + if self._wallet.save_tx(self._tx): + saved = True self.finished.emit(False, saved, self._tx.is_complete()) From 75a9a4fce9ac8261289f075466ded6a671a8d31a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 21 Apr 2023 17:39:11 +0000 Subject: [PATCH 0789/1143] qml TxDetails: txid must be updated after rawtx is changed refresh bug when using bump_fee/dscancel/etc --- electrum/gui/qml/qetxdetails.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 3a9bda646..ee6aa28d0 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -114,6 +114,7 @@ def rawtx(self, rawtx: str): try: self._tx = tx_from_any(rawtx, deserialize=True) self._txid = self._tx.txid() + self.txidChanged.emit() self.update() except Exception as e: self._tx = None From 7383cdc474bdab113a042f2b119cbb344f2f7410 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 22 Apr 2023 11:40:36 +0200 Subject: [PATCH 0790/1143] qml: don't crash when tx not found on histogram event. closes #8332 --- electrum/gui/qml/qetransactionlistmodel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index e9ab92526..ea9ae213f 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -253,7 +253,8 @@ def on_event_fee_histogram(self, histogram): continue txid = tx_item['txid'] tx = self.wallet.db.get_transaction(txid) - assert tx is not None + if not tx: + continue txinfo = self.wallet.get_tx_info(tx) status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status) tx_item['date'] = status_str From 0d536b83ba4d196a7af0aa3ee070b76a74d6d5ef Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 22 Apr 2023 12:18:02 +0200 Subject: [PATCH 0791/1143] qml: ignore update() when wallet not set yet. closes #8330 --- electrum/gui/qml/qetxfinalizer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 36e129dda..ad567333b 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -302,6 +302,10 @@ def make_tx(self, amount): return tx def update(self): + if not self._wallet: + self._logger.debug('wallet not set, ignoring update()') + return + try: # make unsigned transaction tx = self.make_tx(amount = '!' if self._amount.isMax else self._amount.satsInt) From f787d6eede9ab9bdf6ce8d45b2d140d1d85b29fd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 22 Apr 2023 12:50:14 +0200 Subject: [PATCH 0792/1143] qt piechart: show a full circle if there is only one item in the list --- electrum/gui/qt/balance_dialog.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/balance_dialog.py b/electrum/gui/qt/balance_dialog.py index acf3489ae..4bbd29738 100644 --- a/electrum/gui/qt/balance_dialog.py +++ b/electrum/gui/qt/balance_dialog.py @@ -68,10 +68,15 @@ def paintEvent(self, event): alpha = 0 s = 0 for name, color, amount in self._list: - delta = int(16 * 360 * amount/total) qp.setBrush(color) - qp.drawPie(self.R, alpha, delta) - alpha += delta + if amount == 0: + continue + elif amount == total: + qp.drawEllipse(self.R) + else: + delta = int(16 * 360 * amount/total) + qp.drawPie(self.R, alpha, delta) + alpha += delta qp.end() class PieChartWidget(QWidget, PieChartObject): From fcf836bc94401c4773ad09a99da8cca62645bf46 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 22 Apr 2023 15:03:39 +0200 Subject: [PATCH 0793/1143] lnworker: schedule_force_closing is not async --- electrum/lnworker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index ffc56c74f..a7173528d 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1013,7 +1013,7 @@ async def handle_onchain_state(self, chan: Channel): if (chan.get_state() in (ChannelState.OPEN, ChannelState.SHUTDOWN) and chan.should_be_closed_due_to_expiring_htlcs(self.network.get_local_height())): self.logger.info(f"force-closing due to expiring htlcs") - await self.schedule_force_closing(chan.channel_id) + self.schedule_force_closing(chan.channel_id) elif chan.get_state() == ChannelState.FUNDED: peer = self._peers.get(chan.node_id) From 0fcf423fbe1e8b47b9700d5ff3bd2d79ce3bf0b0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 22 Apr 2023 17:20:47 +0200 Subject: [PATCH 0794/1143] Revert "lnworker: schedule_force_closing is not async" This reverts commit fcf836bc94401c4773ad09a99da8cca62645bf46. --- electrum/lnworker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index a7173528d..ffc56c74f 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1013,7 +1013,7 @@ async def handle_onchain_state(self, chan: Channel): if (chan.get_state() in (ChannelState.OPEN, ChannelState.SHUTDOWN) and chan.should_be_closed_due_to_expiring_htlcs(self.network.get_local_height())): self.logger.info(f"force-closing due to expiring htlcs") - self.schedule_force_closing(chan.channel_id) + await self.schedule_force_closing(chan.channel_id) elif chan.get_state() == ChannelState.FUNDED: peer = self._peers.get(chan.node_id) From 6b0db411ae6a71673d561d0b3d9049347c82d22d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 00:06:45 +0000 Subject: [PATCH 0795/1143] wallet: save_db immediately after init_lightning() generates keys Had a crash shortly after enabling lightning and the LN keys were lost... Though note that opening a channel triggers wallet.save_db(), so I think nothing of real value is at risk without this change. --- electrum/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/wallet.py b/electrum/wallet.py index 5c729e1a3..1afccd715 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -439,6 +439,7 @@ def init_lightning(self, *, password) -> None: ln_xprv = node.to_xprv() self.db.put('lightning_privkey2', ln_xprv) self.lnworker = LNWallet(self, ln_xprv) + self.save_db() if self.network: self._start_network_lightning() From ad5891672997499ab7aea13eb8d7267458610ce7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 00:13:22 +0000 Subject: [PATCH 0796/1143] wizard.py: (trivial) fix type hint and an f-string --- electrum/wizard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 410e70d29..f0045d3b2 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -13,7 +13,7 @@ class WizardViewState(NamedTuple): - view: str + view: Optional[str] wizard_data: Dict[str, Any] params: Dict[str, Any] @@ -124,7 +124,7 @@ def is_last_view(self, view, wizard_data): self._logger.debug(f'view "{view}" last: {l}') return l else: - raise Exception('last handler for view {view} is not callable nor a bool literal') + raise Exception(f'last handler for view {view} is not callable nor a bool literal') def finished(self, wizard_data): self._logger.debug('finished.') From 1a2d4494eb77868cf2c07c9398eb76a7e3a6bb50 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 15:10:38 +0000 Subject: [PATCH 0797/1143] qt: fix sweeping closes https://github.com/spesmilo/electrum/issues/8340 regression from 2f6d60c715e3c2637a5fe491d375a493ced43bba --- electrum/gui/qt/main_window.py | 12 ++++++++---- electrum/gui/qt/send_tab.py | 12 ++++++++---- electrum/gui/qt/transaction_dialog.py | 15 +++++++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 67fd252fd..c8d888495 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1050,8 +1050,8 @@ def show_channel_details(self, chan): from .channel_details import ChannelDetailsDialog ChannelDetailsDialog(self, chan).show() - def show_transaction(self, tx: Transaction): - show_transaction(tx, parent=self) + def show_transaction(self, tx: Transaction, *, external_keypairs=None): + show_transaction(tx, parent=self, external_keypairs=external_keypairs) def show_lightning_transaction(self, tx_item): from .lightning_tx_dialog import LightningTxDialog @@ -1174,7 +1174,7 @@ def on_event_payment_failed(self, wallet, key, reason): invoice = self.wallet.get_invoice(key) if invoice and invoice.is_lightning() and invoice.get_address(): if self.question(_('Payment failed') + '\n\n' + reason + '\n\n'+ 'Fallback to onchain payment?'): - self.send_tab.pay_onchain_dialog(self.get_coins(), invoice.get_outputs()) + self.send_tab.pay_onchain_dialog(invoice.get_outputs()) else: self.show_error(_('Payment failed') + '\n\n' + reason) @@ -2387,6 +2387,9 @@ def export_contacts(self): def sweep_key_dialog(self): + if not self.network: + self.show_error(_("You are offline.")) + return d = WindowModalDialog(self, title=_('Sweep private keys')) d.setMinimumSize(600, 300) vbox = QVBoxLayout(d) @@ -2450,7 +2453,8 @@ def on_success(result): coins, keypairs = result outputs = [PartialTxOutput.from_address_and_value(addr, value='!')] self.warn_if_watching_only() - self.send_tab.pay_onchain_dialog(coins, outputs, external_keypairs=keypairs) + self.send_tab.pay_onchain_dialog( + outputs, external_keypairs=keypairs, get_coins=lambda *args, **kwargs: coins) def on_failure(exc_info): self.on_error(exc_info) msg = _('Preparing sweep transaction...') diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index c8276c82d..dffa15d31 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -4,7 +4,7 @@ import asyncio from decimal import Decimal -from typing import Optional, TYPE_CHECKING, Sequence, List +from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Any from urllib.parse import urlparse from PyQt5.QtCore import pyqtSignal, QPoint @@ -238,14 +238,18 @@ def pay_onchain_dialog( self, outputs: List[PartialTxOutput], *, nonlocal_only=False, - external_keypairs=None) -> None: + external_keypairs=None, + get_coins: Callable[..., Sequence[PartialTxInput]] = None, + ) -> None: # trustedcoin requires this if run_hook('abort_send', self): return is_sweep = bool(external_keypairs) # we call get_coins inside make_tx, so that inputs can be changed dynamically + if get_coins is None: + get_coins = self.window.get_coins make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( - coins=self.window.get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only), + coins=get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only), outputs=outputs, fee=fee_est, is_sweep=is_sweep) @@ -266,7 +270,7 @@ def pay_onchain_dialog( return is_preview = conf_dlg.is_preview if is_preview: - self.window.show_transaction(tx) + self.window.show_transaction(tx, external_keypairs=external_keypairs) return self.save_pending_invoice() def sign_done(success): diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 5fb1f2060..c29de2cbb 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -372,10 +372,15 @@ def on_context_menu_for_outputs(self, pos: QPoint): menu.exec_(global_pos) - -def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved=False): +def show_transaction( + tx: Transaction, + *, + parent: 'ElectrumWindow', + prompt_if_unsaved: bool = False, + external_keypairs=None, +): try: - d = TxDialog(tx, parent=parent, prompt_if_unsaved=prompt_if_unsaved) + d = TxDialog(tx, parent=parent, prompt_if_unsaved=prompt_if_unsaved, external_keypairs=external_keypairs) except SerializationError as e: _logger.exception('unable to deserialize the transaction') parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) @@ -383,13 +388,11 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_uns d.show() - - class TxDialog(QDialog, MessageBoxMixin): throttled_update_sig = pyqtSignal() # emit from thread to do update in main thread - def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved, external_keypairs=None): + def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved: bool, external_keypairs=None): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' From ea864cd5c974c46e2fd34dd1b4c9b9863cc5e50e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 16:00:42 +0000 Subject: [PATCH 0798/1143] qml: TxListModel: don't rely on wallet.db.get_transaction() finding tx tx might get removed from wallet after wallet.get_full_history() but before the model is populated closes https://github.com/spesmilo/electrum/issues/8339 --- electrum/gui/qml/qetransactionlistmodel.py | 33 ++++++++++++++++------ electrum/gui/qt/history_list.py | 12 ++++---- electrum/util.py | 9 ++++++ 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index ea9ae213f..24af49ede 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Any from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex @@ -97,9 +97,9 @@ def clear(self): self.tx_history = [] self.endResetModel() - def tx_to_model(self, tx): - #self._logger.debug(str(tx)) - item = tx + def tx_to_model(self, tx_item): + #self._logger.debug(str(tx_item)) + item = tx_item item['key'] = item['txid'] if 'txid' in item else item['payment_hash'] @@ -118,15 +118,19 @@ def tx_to_model(self, tx): if 'txid' in item: tx = self.wallet.db.get_transaction(item['txid']) - assert tx is not None - item['complete'] = tx.is_complete() + if tx: + item['complete'] = tx.is_complete() + else: # due to races, tx might have already been removed from history + item['complete'] = False # newly arriving txs, or (partially/fully signed) local txs have no (block) timestamp # FIXME just use wallet.get_tx_status, and change that as needed if not item['timestamp']: # onchain: local or mempool or unverified txs - txinfo = self.wallet.get_tx_info(tx) - item['section'] = 'mempool' if item['complete'] and not txinfo.can_broadcast else 'local' - status, status_str = self.wallet.get_tx_status(item['txid'], txinfo.tx_mined_status) + txid = item['txid'] + assert txid + tx_mined_info = self._tx_mined_info_from_tx_item(tx_item) + item['section'] = 'local' if tx_mined_info.is_local_like() else 'mempool' + status, status_str = self.wallet.get_tx_status(txid, tx_mined_info=tx_mined_info) item['date'] = status_str else: # lightning or already mined (and SPV-ed) onchain txs item['section'] = self.get_section_by_timestamp(item['timestamp']) @@ -162,6 +166,17 @@ def format_date_by_section(self, section, date): section = 'older' return date.strftime(dfmt[section]) + @staticmethod + def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: + # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qt-gui + tx_mined_info = TxMinedInfo( + height=tx_item['height'], + conf=tx_item['confirmations'], + timestamp=tx_item['timestamp'], + wanted_height=tx_item.get('wanted_height', None), + ) + return tx_mined_info + # initial model data @pyqtSlot() @pyqtSlot(bool) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 2770091c8..927659aa5 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -28,7 +28,7 @@ import time import datetime from datetime import date -from typing import TYPE_CHECKING, Tuple, Dict +from typing import TYPE_CHECKING, Tuple, Dict, Any import threading import enum from decimal import Decimal @@ -128,7 +128,7 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria try: status, status_str = self.model.tx_status_cache[tx_hash] except KeyError: - tx_mined_info = self.model.tx_mined_info_from_tx_item(tx_item) + tx_mined_info = self.model._tx_mined_info_from_tx_item(tx_item) status, status_str = window.wallet.get_tx_status(tx_hash, tx_mined_info) if role == ROLE_SORT_ORDER: @@ -353,7 +353,7 @@ def refresh(self, reason: str): self.tx_status_cache.clear() for txid, tx_item in self.transactions.items(): if not tx_item.get('lightning', False): - tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + tx_mined_info = self._tx_mined_info_from_tx_item(tx_item) self.tx_status_cache[txid] = self.window.wallet.get_tx_status(txid, tx_mined_info) # update counter num_tx = len(self.transactions) @@ -404,7 +404,7 @@ def on_fee_histogram(self): for tx_hash, tx_item in list(self.transactions.items()): if tx_item.get('lightning'): continue - tx_mined_info = self.tx_mined_info_from_tx_item(tx_item) + tx_mined_info = self._tx_mined_info_from_tx_item(tx_item) if tx_mined_info.conf > 0: # note: we could actually break here if we wanted to rely on the order of txns in self.transactions continue @@ -441,8 +441,8 @@ def flags(self, idx: QModelIndex) -> int: return super().flags(idx) | int(extra_flags) @staticmethod - def tx_mined_info_from_tx_item(tx_item): - # FIXME a bit hackish to have to reconstruct the TxMinedInfo... + def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: + # FIXME a bit hackish to have to reconstruct the TxMinedInfo... same thing in qml-gui tx_mined_info = TxMinedInfo( height=tx_item['height'], conf=tx_item['confirmations'], diff --git a/electrum/util.py b/electrum/util.py index adf502293..a8e8dd648 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1340,6 +1340,15 @@ def short_id(self) -> Optional[str]: return f"{self.height}x{self.txpos}" return None + def is_local_like(self) -> bool: + """Returns whether the tx is local-like (LOCAL/FUTURE).""" + from .address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT + if self.height > 0: + return False + if self.height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): + return False + return True + class ShortID(bytes): From 6b75d5f134049c0510ed0f64a6f34f7772d68665 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 16:13:03 +0000 Subject: [PATCH 0799/1143] qt SwapDialog: handle sm.max_amount_forward_swap() being None closes https://github.com/spesmilo/electrum/issues/8341 --- electrum/gui/qt/swap_dialog.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 5966e029e..4f52756c6 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton @@ -170,7 +170,7 @@ def uncheck_max(self): self.max_button.setChecked(False) self.update() - def _spend_max_forward_swap(self, tx): + def _spend_max_forward_swap(self, tx: Optional[PartialTransaction]) -> None: if tx: amount = tx.output_value_for_address(ln_dummy_address()) self.send_amount_e.setAmount(amount) @@ -178,7 +178,7 @@ def _spend_max_forward_swap(self, tx): self.send_amount_e.setAmount(None) self.max_button.setChecked(False) - def _spend_max_reverse_swap(self): + def _spend_max_reverse_swap(self) -> None: amount = min(self.lnworker.num_sats_can_send(), self.swap_manager.get_max_amount()) self.send_amount_e.setAmount(amount) @@ -267,7 +267,7 @@ def run(self): self.window.protect(self.do_normal_swap, (lightning_amount, onchain_amount)) return True - def update_tx(self): + def update_tx(self) -> None: if self.is_reverse: self.update_fee(None) return @@ -280,7 +280,7 @@ def update_tx(self): tx = self._create_tx(onchain_amount) self.update_fee(tx) - def _create_tx(self, onchain_amount): + def _create_tx(self, onchain_amount: Union[int, str, None]) -> Optional[PartialTransaction]: if self.is_reverse: return if onchain_amount is None: @@ -289,6 +289,8 @@ def _create_tx(self, onchain_amount): if onchain_amount == '!': max_amount = sum(c.value_sats() for c in coins) max_swap_amount = self.swap_manager.max_amount_forward_swap() + if max_swap_amount is None: + return None if max_amount > max_swap_amount: onchain_amount = max_swap_amount outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)] From 7907eb1f8660777e41bf27d524e556047aefb419 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 16:36:32 +0000 Subject: [PATCH 0800/1143] qml/qeinvoice.py: turn _bip21 field into local var --- electrum/gui/qml/qeinvoice.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index dc0225c7e..fbd31db76 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -442,7 +442,6 @@ def isLnurlPay(self): def clear(self): self.recipient = '' self.setInvoiceType(QEInvoice.Type.Invalid) - self._bip21 = None self._lnurlData = None self.canSave = False self.canPay = False @@ -497,15 +496,15 @@ def validateRecipient(self, recipient): maybe_lightning_invoice = recipient try: - self._bip21 = parse_URI(recipient, lambda pr: self._bip70PrResolvedSignal.emit(pr)) - if self._bip21: - if 'r' in self._bip21 or ('name' in self._bip21 and 'sig' in self._bip21): # TODO set flag in util? + bip21 = parse_URI(recipient, lambda pr: self._bip70PrResolvedSignal.emit(pr)) + if bip21: + if 'r' in bip21 or ('name' in bip21 and 'sig' in bip21): # TODO set flag in util? # let callback handle state return if ':' not in recipient: # address only # create bare invoice - outputs = [PartialTxOutput.from_address_and_value(self._bip21['address'], 0)] + outputs = [PartialTxOutput.from_address_and_value(bip21['address'], 0)] invoice = self.create_onchain_invoice(outputs, None, None, None) self._logger.debug(repr(invoice)) self.setValidOnchainInvoice(invoice) @@ -513,10 +512,10 @@ def validateRecipient(self, recipient): return else: # fallback lightning invoice? - if 'lightning' in self._bip21: - maybe_lightning_invoice = self._bip21['lightning'] + if 'lightning' in bip21: + maybe_lightning_invoice = bip21['lightning'] except InvalidBitcoinURI as e: - self._bip21 = None + bip21 = None lninvoice = None maybe_lightning_invoice = maybe_extract_lightning_payment_identifier(maybe_lightning_invoice) @@ -538,14 +537,14 @@ def validateRecipient(self, recipient): return self._logger.exception(repr(e)) - if not lninvoice and not self._bip21: + if not lninvoice and not bip21: self.validationError.emit('unknown',_('Unknown invoice')) self.clear() return if lninvoice: if not self._wallet.wallet.has_lightning(): - if not self._bip21: + if not bip21: if lninvoice.get_address(): self.setValidLightningInvoice(lninvoice) self.validationSuccess.emit() @@ -553,7 +552,7 @@ def validateRecipient(self, recipient): self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.')) else: self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') - self.setValidOnchainInvoice(self._bip21['address']) + self.setValidOnchainInvoice(bip21['address']) else: self.setValidLightningInvoice(lninvoice) if not self._wallet.wallet.lnworker.channels: @@ -562,14 +561,14 @@ def validateRecipient(self, recipient): self.validationSuccess.emit() else: self._logger.debug('flow without LN but having bip21 uri') - if 'amount' not in self._bip21: + if 'amount' not in bip21: amount = 0 else: - amount = self._bip21['amount'] - outputs = [PartialTxOutput.from_address_and_value(self._bip21['address'], amount)] + amount = bip21['amount'] + outputs = [PartialTxOutput.from_address_and_value(bip21['address'], amount)] self._logger.debug(outputs) - message = self._bip21['message'] if 'message' in self._bip21 else '' - invoice = self.create_onchain_invoice(outputs, message, None, self._bip21) + message = bip21['message'] if 'message' in bip21 else '' + invoice = self.create_onchain_invoice(outputs, message, None, bip21) self._logger.debug(repr(invoice)) self.setValidOnchainInvoice(invoice) self.validationSuccess.emit() From a8623f63bbc31989dd18f1b57540685a235fb2b1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 16:42:08 +0000 Subject: [PATCH 0801/1143] qml: fix send "flow with LN but not LN enabled AND having bip21 uri" closes https://github.com/spesmilo/electrum/issues/8334 --- electrum/gui/qml/qeinvoice.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index fbd31db76..3b40ee596 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -1,5 +1,5 @@ import threading -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Dict, Any import asyncio from urllib.parse import urlparse @@ -552,7 +552,7 @@ def validateRecipient(self, recipient): self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.')) else: self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') - self.setValidOnchainInvoice(bip21['address']) + self._validateRecipient_bip21_onchain(bip21) else: self.setValidLightningInvoice(lninvoice) if not self._wallet.wallet.lnworker.channels: @@ -561,17 +561,20 @@ def validateRecipient(self, recipient): self.validationSuccess.emit() else: self._logger.debug('flow without LN but having bip21 uri') - if 'amount' not in bip21: - amount = 0 - else: - amount = bip21['amount'] - outputs = [PartialTxOutput.from_address_and_value(bip21['address'], amount)] - self._logger.debug(outputs) - message = bip21['message'] if 'message' in bip21 else '' - invoice = self.create_onchain_invoice(outputs, message, None, bip21) - self._logger.debug(repr(invoice)) - self.setValidOnchainInvoice(invoice) - self.validationSuccess.emit() + self._validateRecipient_bip21_onchain(bip21) + + def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None: + if 'amount' not in bip21: + amount = 0 + else: + amount = bip21['amount'] + outputs = [PartialTxOutput.from_address_and_value(bip21['address'], amount)] + self._logger.debug(outputs) + message = bip21['message'] if 'message' in bip21 else '' + invoice = self.create_onchain_invoice(outputs, message, None, bip21) + self._logger.debug(repr(invoice)) + self.setValidOnchainInvoice(invoice) + self.validationSuccess.emit() def resolve_lnurl(self, lnurl): self._logger.debug('resolve_lnurl') From 417423ecd7ccab199db9327a626b56219a3e3276 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 24 Apr 2023 00:58:41 +0000 Subject: [PATCH 0802/1143] qt: PayToEdit: fix input_qr_from_camera closes https://github.com/spesmilo/electrum/issues/8342 probably regression from 1f4cedf56a342ed4b1e1e5dea69e037dd1f8083f --- electrum/gui/qt/paytoedit.py | 1 + electrum/gui/qt/qrreader/__init__.py | 1 + electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py | 6 +++--- electrum/gui/qt/util.py | 5 ++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index b43354ba4..964a729da 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -129,6 +129,7 @@ def __init__(self, send_tab: 'SendTab'): allow_multi=False, show_error=self.win.show_error, setText=self._on_input_btn, + parent=self.win, ) self.on_qr_from_screenshot_input_btn = partial( self.input_qr_from_screenshot, diff --git a/electrum/gui/qt/qrreader/__init__.py b/electrum/gui/qt/qrreader/__init__.py index 8e39f21f3..9dd7ada95 100644 --- a/electrum/gui/qt/qrreader/__init__.py +++ b/electrum/gui/qt/qrreader/__init__.py @@ -49,6 +49,7 @@ def scan_qrcode( callback: Callable[[bool, str, Optional[str]], None], ) -> None: """Scans QR code using camera.""" + assert parent is None or isinstance(parent, QWidget), f"parent should be a QWidget, not {parent!r}" if sys.platform == 'darwin' or sys.platform in ('windows', 'win32'): _scan_qrcode_using_qtmultimedia(parent=parent, config=config, callback=callback) else: # desktop Linux and similar diff --git a/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py index 54ad1a1c9..9b25e1e76 100644 --- a/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py +++ b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py @@ -27,10 +27,10 @@ import math import sys import os -from typing import List +from typing import List, Optional from PyQt5.QtMultimedia import QCameraInfo, QCamera, QCameraViewfinderSettings -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QLabel +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QLabel, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QSize, QRect, Qt, pyqtSignal, PYQT_VERSION @@ -68,7 +68,7 @@ class QrReaderCameraDialog(Logger, MessageBoxMixin, QDialog): qr_finished = pyqtSignal(bool, str, object) - def __init__(self, parent, *, config: SimpleConfig): + def __init__(self, parent: Optional[QWidget], *, config: SimpleConfig): ''' Note: make sure parent is a "top_level_window()" as per MessageBoxMixin API else bad things can happen on macOS. ''' QDialog.__init__(self, parent=parent) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 2e400e158..32f06398a 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -547,6 +547,7 @@ def input_qr_from_camera( allow_multi: bool = False, show_error: Callable[[str], None], setText: Callable[[str], None] = None, + parent: QWidget = None, ) -> None: if setText is None: setText = self.setText @@ -564,7 +565,9 @@ def cb(success: bool, error: str, data): setText(new_text) from .qrreader import scan_qrcode - scan_qrcode(parent=self, config=config, callback=cb) + if parent is None: + parent = self if isinstance(self, QWidget) else None + scan_qrcode(parent=parent, config=config, callback=cb) def input_qr_from_screenshot( self, From fd9a90f3b6129da4539a093e2e625685e97fe2d0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 00:50:29 +0000 Subject: [PATCH 0803/1143] wizard.py: fix clearing stack between wizards try finishing a wizard and then launching a new one: the new one kept building on top of the stack of the prev wizard --- electrum/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index f0045d3b2..975eeed95 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -130,7 +130,7 @@ def finished(self, wizard_data): self._logger.debug('finished.') def reset(self): - self.stack = [] + self._stack = [] self._current = WizardViewState(None, {}, {}) def log_stack(self, _stack): From e9aad6896ee224e0bedae5d7fb1ec1ceb59c19ee Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 00:53:40 +0000 Subject: [PATCH 0804/1143] wizard.py: change stack to be per-instance seems less error-prone --- electrum/wizard.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index 975eeed95..e9eed61ce 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -28,10 +28,11 @@ class AbstractWizard: _logger = get_logger(__name__) - navmap = {} + def __init__(self): + self.navmap = {} - _current = WizardViewState(None, {}, {}) - _stack = [] # type: List[WizardViewState] + self._current = WizardViewState(None, {}, {}) + self._stack = [] # type: List[WizardViewState] def navmap_merge(self, additional_navmap): # NOTE: only merges one level deep. Deeper dict levels will overwrite @@ -165,6 +166,7 @@ class NewWalletWizard(AbstractWizard): _logger = get_logger(__name__) def __init__(self, daemon): + AbstractWizard.__init__(self) self.navmap = { 'wallet_name': { 'next': 'wallet_type' @@ -438,6 +440,7 @@ class ServerConnectWizard(AbstractWizard): _logger = get_logger(__name__) def __init__(self, daemon): + AbstractWizard.__init__(self) self.navmap = { 'autoconnect': { 'next': 'server_config', From b429992e77071b7a01b5212cf0347ea96eec87b0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 00:56:12 +0000 Subject: [PATCH 0805/1143] wizard.py: don't use mutable default args --- electrum/wizard.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/wizard.py b/electrum/wizard.py index e9eed61ce..d46908fc6 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -228,7 +228,9 @@ def __init__(self, daemon): } self._daemon = daemon - def start(self, initial_data = {}): + def start(self, initial_data=None): + if initial_data is None: + initial_data = {} self.reset() self._current = WizardViewState('wallet_name', initial_data, {}) return self._current @@ -458,7 +460,9 @@ def __init__(self, daemon): } self._daemon = daemon - def start(self, initial_data = {}): + def start(self, initial_data=None): + if initial_data is None: + initial_data = {} self.reset() self._current = WizardViewState('proxy_ask', initial_data, {}) return self._current From 2fc9ee5c5100154e8837e3c7c0bf833169fd24a9 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 11:27:36 +0200 Subject: [PATCH 0806/1143] p4a: fix for Qt5 accessibility bug see https://github.com/accumulator/python-for-android/commit/087fc3c583d46bfb2dec54878ddea508afb27de6 --- contrib/android/Dockerfile | 2 +- electrum/gui/qml/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index a3d6040bd..35cd9ed90 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -180,7 +180,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "8d73dc4f2b74b187c4f1ff59b55873ba1e357b05^{commit}" \ + && git checkout "087fc3c583d46bfb2dec54878ddea508afb27de6^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index f475a58d5..6a0a70037 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -65,6 +65,8 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') os.environ['QT_VIRTUALKEYBOARD_STYLE'] = 'Electrum' os.environ['QML2_IMPORT_PATH'] = 'electrum/gui/qml' + os.environ['QT_ANDROID_DISABLE_ACCESSIBILITY'] = '1' + # set default locale to en_GB. This is for l10n (e.g. number formatting, number input etc), # but not for i18n, which is handled by the Translator # this can be removed once the backend wallet is fully l10n aware From 674c2b55e7574d86d17b354717de3d71907d4fbd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 12:38:41 +0200 Subject: [PATCH 0807/1143] qml: small fixes --- electrum/gui/qml/components/Wallets.qml | 2 +- electrum/gui/qml/components/controls/InfoTextArea.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 4a8949a37..fb25da666 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -118,7 +118,7 @@ Pane { FlatButton { Layout.fillWidth: true - text: 'Create Wallet' + text: qsTr('Create Wallet') icon.source: '../../icons/add.png' onClicked: rootItem.createWallet() } diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index dcdab8c4c..9b4db966f 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -39,7 +39,7 @@ TextHighlightPane { Image { Layout.preferredWidth: constants.iconSizeMedium Layout.preferredHeight: constants.iconSizeMedium - visible: iconStyle != InfoTextArea.IconStyle.Spinner + visible: iconStyle != InfoTextArea.IconStyle.Spinner && iconStyle != InfoTextArea.IconStyle.None source: iconStyle == InfoTextArea.IconStyle.Info ? "../../../icons/info.png" : iconStyle == InfoTextArea.IconStyle.Warn From 959d481e939567ced255bb536a1370a3d6071b0a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 12:39:29 +0200 Subject: [PATCH 0808/1143] qml: create ScanDialog --- electrum/gui/qml/components/ScanDialog.qml | 45 ++++++++++++++++++++++ electrum/gui/qml/components/main.qml | 8 ++++ 2 files changed, 53 insertions(+) create mode 100644 electrum/gui/qml/components/ScanDialog.qml diff --git a/electrum/gui/qml/components/ScanDialog.qml b/electrum/gui/qml/components/ScanDialog.qml new file mode 100644 index 000000000..3a8f6ed21 --- /dev/null +++ b/electrum/gui/qml/components/ScanDialog.qml @@ -0,0 +1,45 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.0 + +import "controls" + +ElDialog { + id: scanDialog + + property string scanData + property string error + property string hint + + signal found + + width: parent.width + height: parent.height + padding: 0 + + header: null + topPadding: 0 // dialog needs topPadding override + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + QRScan { + Layout.fillWidth: true + Layout.fillHeight: true + hint: scanDialog.hint + onFound: { + scanDialog.scanData = scanData + scanDialog.found() + } + } + + FlatButton { + id: button + Layout.fillWidth: true + text: qsTr('Cancel') + icon.source: '../../icons/closebutton.png' + onClicked: doReject() + } + } +} diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index c1695ce5b..66353a24d 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -361,6 +361,14 @@ ApplicationWindow } } + property alias scanDialog: _scanDialog + Component { + id: _scanDialog + ScanDialog { + onClosed: destroy() + } + } + property alias channelOpenProgressDialog: _channelOpenProgressDialog ChannelOpenProgressDialog { id: _channelOpenProgressDialog From 49df18c613d8b8498173b014173ace81eed0b9a4 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 13:19:40 +0200 Subject: [PATCH 0809/1143] qml: add hint property to QRScan --- electrum/gui/qml/components/controls/QRScan.qml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/QRScan.qml b/electrum/gui/qml/components/controls/QRScan.qml index 8b1de1100..75f8224c4 100644 --- a/electrum/gui/qml/components/controls/QRScan.qml +++ b/electrum/gui/qml/components/controls/QRScan.qml @@ -10,6 +10,7 @@ Item { property bool active: false property string url property string scanData + property string hint property bool _pointsVisible @@ -41,6 +42,20 @@ Item { anchors.bottom: parent.bottom color: Qt.rgba(0,0,0,0.5) } + InfoTextArea { + visible: scanner.hint + background.opacity: 0.5 + iconStyle: InfoTextArea.IconStyle.None + anchors { + top: parent.top + topMargin: constants.paddingXLarge + left: parent.left + leftMargin: constants.paddingXXLarge + right: parent.right + rightMargin: constants.paddingXXLarge + } + text: scanner.hint + } } Image { @@ -151,7 +166,7 @@ Item { } Component.onCompleted: { - console.log('Scan page initialized') + console.log('enumerating cameras') QtMultimedia.availableCameras.forEach(function(item) { console.log('cam found, id=' + item.deviceId + ' name=' + item.displayName) console.log('pos=' + item.position + ' orientation=' + item.orientation) From 6848b8f375f489f47aaf98da6253d2773354c51d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 13:34:05 +0200 Subject: [PATCH 0810/1143] qml: refactor all custom QRScan component wrappers to ScanDialog (except SendDialog, which has a Paste button and slightly different behavior) --- .../components/ImportAddressesKeysDialog.qml | 34 +++++---------- .../components/ImportChannelBackupDialog.qml | 30 ++++---------- .../gui/qml/components/OpenChannelDialog.qml | 14 ++++--- electrum/gui/qml/components/SendDialog.qml | 5 ++- .../qml/components/wizard/WCHaveMasterKey.qml | 41 +++++++------------ .../gui/qml/components/wizard/WCImport.qml | 36 ++++++---------- 6 files changed, 55 insertions(+), 105 deletions(-) diff --git a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml index a01e4ae2c..09cdfb7af 100644 --- a/electrum/gui/qml/components/ImportAddressesKeysDialog.qml +++ b/electrum/gui/qml/components/ImportAddressesKeysDialog.qml @@ -80,15 +80,20 @@ ElDialog { icon.width: constants.iconSizeMedium scale: 1.2 onClicked: { - var scan = qrscan.createObject(root.contentItem) // can't use dialog as parent? - scan.onFound.connect(function() { - if (verify(scan.scanData)) { + var dialog = app.scanDialog.createObject(app, { + hint: Daemon.currentWallet.isWatchOnly + ? qsTr('Scan another address') + : qsTr('Scan another private key') + }) + dialog.onFound.connect(function() { + if (verify(dialog.scanData)) { if (import_ta.text != '') import_ta.text = import_ta.text + ',\n' - import_ta.text = import_ta.text + scan.scanData + import_ta.text = import_ta.text + dialog.scanData } - scan.destroy() + dialog.close() }) + dialog.open() } } } @@ -109,25 +114,6 @@ ElDialog { } } - Component { - id: qrscan - QRScan { - width: parent.width - height: parent.height - - ToolButton { - icon.source: '../../icons/closebutton.png' - icon.height: constants.iconSizeMedium - icon.width: constants.iconSizeMedium - anchors.right: parent.right - anchors.top: parent.top - onClicked: { - parent.destroy() - } - } - } - } - Bitcoin { id: bitcoin } diff --git a/electrum/gui/qml/components/ImportChannelBackupDialog.qml b/electrum/gui/qml/components/ImportChannelBackupDialog.qml index 309066ac5..709022d31 100644 --- a/electrum/gui/qml/components/ImportChannelBackupDialog.qml +++ b/electrum/gui/qml/components/ImportChannelBackupDialog.qml @@ -59,11 +59,14 @@ ElDialog { icon.width: constants.iconSizeMedium scale: 1.2 onClicked: { - var scan = qrscan.createObject(root.contentItem) - scan.onFound.connect(function() { - channelbackup_ta.text = scan.scanData - scan.destroy() + var dialog = app.scanDialog.createObject(app, { + hint: qsTr('Scan a channel backup') }) + dialog.onFound.connect(function() { + channelbackup_ta.text = dialog.scanData + dialog.close() + }) + dialog.open() } } } @@ -92,23 +95,4 @@ ElDialog { } } - Component { - id: qrscan - QRScan { - width: root.contentItem.width - height: root.contentItem.height - - ToolButton { - icon.source: '../../icons/closebutton.png' - icon.height: constants.iconSizeMedium - icon.width: constants.iconSizeMedium - anchors.right: parent.right - anchors.top: parent.top - onClicked: { - parent.destroy() - } - } - } - } - } diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 279e11792..5ef5a2fcc 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -107,14 +107,17 @@ ElDialog { icon.width: constants.iconSizeMedium scale: 1.2 onClicked: { - var page = app.stack.push(Qt.resolvedUrl('Scan.qml')) - page.onFound.connect(function() { - if (channelopener.validate_connect_str(page.scanData)) { - channelopener.connectStr = page.scanData + var dialog = app.scanDialog.createObject(app, { + hint: qsTr('Scan a channel connect string') + }) + dialog.onFound.connect(function() { + if (channelopener.validate_connect_str(dialog.scanData)) { + channelopener.connectStr = dialog.scanData node.text = channelopener.connectStr } - app.stack.pop() + dialog.close() }) + dialog.open() } } } @@ -260,4 +263,5 @@ ElDialog { root.close() } } + } diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index 3615a3e11..a887b4e41 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -15,7 +15,7 @@ ElDialog { signal txFound(data: string) signal channelBackupFound(data: string) - header: Item {} + header: null padding: 0 topPadding: 0 @@ -39,9 +39,10 @@ ElDialog { QRScan { id: qrscan - Layout.preferredWidth: parent.width + Layout.fillWidth: true Layout.fillHeight: true + hint: qsTr('Scan an Invoice, an Address, an LNURL-pay, a PSBT or a Channel backup') onFound: dialog.dispatch(scanData) } diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index 09cb67347..0a1ab3734 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -79,9 +79,10 @@ WizardComponent { icon.source: '../../../icons/share.png' icon.color: 'transparent' onClicked: { - var dialog = app.genericShareDialog.createObject(app, - { title: qsTr('Master public key'), text: multisigMasterPubkey } - ) + var dialog = app.genericShareDialog.createObject(app, { + title: qsTr('Master public key'), + text: multisigMasterPubkey + }) dialog.open() } } @@ -141,14 +142,19 @@ WizardComponent { icon.width: constants.iconSizeMedium scale: 1.2 onClicked: { - var scan = qrscan.createObject(root) - scan.onFound.connect(function() { - if (verifyMasterKey(scan.scanData)) - masterkey_ta.text = scan.scanData + var dialog = app.scanDialog.createObject(app, { + hint: cosigner + ? qsTr('Scan a cosigner master public key') + : qsTr('Scan a master key') + }) + dialog.onFound.connect(function() { + if (verifyMasterKey(dialog.scanData)) + masterkey_ta.text = dialog.scanData else masterkey_ta.text = '' - scan.destroy() + dialog.close() }) + dialog.open() } } } @@ -166,25 +172,6 @@ WizardComponent { } } - Component { - id: qrscan - QRScan { - width: root.width - height: root.height - - ToolButton { - icon.source: '../../../icons/closebutton.png' - icon.height: constants.iconSizeMedium - icon.width: constants.iconSizeMedium - anchors.right: parent.right - anchors.top: parent.top - onClicked: { - parent.destroy() - } - } - } - } - Bitcoin { id: bitcoin onValidationMessageChanged: validationtext.text = validationMessage diff --git a/electrum/gui/qml/components/wizard/WCImport.qml b/electrum/gui/qml/components/wizard/WCImport.qml index 533308d71..3129082b8 100644 --- a/electrum/gui/qml/components/wizard/WCImport.qml +++ b/electrum/gui/qml/components/wizard/WCImport.qml @@ -62,40 +62,28 @@ WizardComponent { icon.width: constants.iconSizeMedium scale: 1.2 onClicked: { - var scan = qrscan.createObject(root) - scan.onFound.connect(function() { - if (verify(scan.scanData)) { + var dialog = app.scanDialog.createObject(app, { + hint: bitcoin.isAddressList(import_ta.text) + ? qsTr('Scan another address') + : bitcoin.isPrivateKeyList(import_ta.text) + ? qsTr('Scan another private key') + : qsTr('Scan a private key or an address') + }) + dialog.onFound.connect(function() { + if (verify(dialog.scanData)) { if (import_ta.text != '') import_ta.text = import_ta.text + ',\n' - import_ta.text = import_ta.text + scan.scanData + import_ta.text = import_ta.text + dialog.scanData } - scan.destroy() + dialog.close() }) + dialog.open() } } } } } - Component { - id: qrscan - QRScan { - width: root.width - height: root.height - - ToolButton { - icon.source: '../../../icons/closebutton.png' - icon.height: constants.iconSizeMedium - icon.width: constants.iconSizeMedium - anchors.right: parent.right - anchors.top: parent.top - onClicked: { - parent.destroy() - } - } - } - } - Bitcoin { id: bitcoin } From 312f2641e7284dc9ed7bde99a61060b4d9e35dd9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 01:33:12 +0000 Subject: [PATCH 0811/1143] don't use bare except use "except Exception", or if really needed explicitly "except BaseException" --- electrum/base_crash_reporter.py | 2 +- electrum/base_wizard.py | 2 +- electrum/bip32.py | 6 +++--- electrum/blockchain.py | 2 +- electrum/channel_db.py | 2 +- electrum/commands.py | 6 +++--- electrum/constants.py | 2 +- electrum/contacts.py | 2 +- electrum/crypto.py | 6 +++--- electrum/ecc.py | 2 +- electrum/exchange_rate.py | 6 +++--- electrum/gui/kivy/i18n.py | 6 +++--- electrum/gui/kivy/main_window.py | 4 ++-- electrum/gui/kivy/uix/dialogs/amount_dialog.py | 2 +- electrum/gui/kivy/uix/dialogs/installwizard.py | 4 ++-- electrum/gui/kivy/uix/screens.py | 4 ++-- electrum/gui/qml/qeapp.py | 2 +- electrum/gui/qml/qebitcoin.py | 2 +- electrum/gui/qml/qeconfig.py | 2 +- electrum/gui/qml/qefx.py | 8 ++++---- electrum/gui/qml/qeinvoice.py | 2 +- electrum/gui/qml/qetxdetails.py | 4 ++-- electrum/gui/qml/qewallet.py | 2 +- electrum/gui/qt/amountedit.py | 4 ++-- electrum/gui/qt/history_list.py | 4 ++-- electrum/gui/qt/locktimeedit.py | 8 ++++---- electrum/gui/qt/main_window.py | 4 ++-- electrum/gui/qt/my_treeview.py | 2 +- electrum/gui/qt/settings_dialog.py | 2 +- electrum/gui/qt/util.py | 2 +- electrum/gui/text.py | 6 +++--- electrum/interface.py | 4 ++-- electrum/json_db.py | 2 +- electrum/keystore.py | 4 ++-- electrum/lnpeer.py | 16 ++++++++-------- electrum/lnutil.py | 4 ++-- electrum/lnworker.py | 6 +++--- electrum/network.py | 10 +++++----- electrum/paymentrequest.py | 4 ++-- electrum/plugins/bitbox02/bitbox02.py | 8 ++++---- electrum/plugins/coldcard/coldcard.py | 4 ++-- electrum/plugins/digitalbitbox/digitalbitbox.py | 2 +- electrum/plugins/jade/jade.py | 2 +- electrum/plugins/labels/labels.py | 6 +++--- electrum/plugins/ledger/ledger.py | 2 +- electrum/plugins/revealer/revealer.py | 2 +- electrum/plugins/trustedcoin/trustedcoin.py | 4 ++-- electrum/scripts/txbroadcast.py | 2 +- electrum/scripts/txradar.py | 2 +- electrum/simple_config.py | 4 ++-- electrum/storage.py | 2 +- electrum/submarine_swaps.py | 2 +- electrum/transaction.py | 6 +++--- electrum/util.py | 8 ++++---- electrum/verifier.py | 2 +- electrum/wallet.py | 12 ++++++------ electrum/wallet_db.py | 4 ++-- 57 files changed, 118 insertions(+), 118 deletions(-) diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index ed75a1ac6..8471ee679 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -130,7 +130,7 @@ def get_additional_info(self): } try: args["wallet_type"] = self.get_wallet_type() - except: + except Exception: # Maybe the wallet isn't loaded yet pass return args diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 59bb22fb9..7a68f2bae 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -351,7 +351,7 @@ def failed_getting_device_infos(name, e): state = _("initialized") if info.initialized else _("wiped") label = info.label or _("An unnamed {}").format(name) try: transport_str = info.device.transport_ui_string[:20] - except: transport_str = 'unknown transport' + except Exception: transport_str = 'unknown transport' descr = f"{label} [{info.model_name or name}, {state}, {transport_str}]" choices.append(((name, info), descr)) msg = _('Select a device') + ':' diff --git a/electrum/bip32.py b/electrum/bip32.py index f3ea60f95..25eeadbd4 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -297,7 +297,7 @@ def is_xpub(text): try: node = BIP32Node.from_xkey(text) return not node.is_private() - except: + except Exception: return False @@ -305,7 +305,7 @@ def is_xprv(text): try: node = BIP32Node.from_xkey(text) return node.is_private() - except: + except Exception: return False @@ -374,7 +374,7 @@ def is_bip32_derivation(s: str) -> bool: if not (s == 'm' or s.startswith('m/')): return False convert_bip32_strpath_to_intpath(s) - except: + except Exception: return False else: return True diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 569f0338e..4a6aad4ed 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -622,7 +622,7 @@ def can_connect(self, header: dict, check_height: bool=True) -> bool: return hash_header(header) == constants.net.GENESIS try: prev_hash = self.get_hash(height - 1) - except: + except Exception: return False if prev_hash != header.get('prev_block_hash'): return False diff --git a/electrum/channel_db.py b/electrum/channel_db.py index b14bef7de..4331a56e5 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -175,7 +175,7 @@ def from_msg(payload) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]: alias = payload['alias'].rstrip(b'\x00') try: alias = alias.decode('utf8') - except: + except Exception: alias = '' timestamp = payload['timestamp'] node_info = NodeInfo(node_id=node_id, features=features, timestamp=timestamp, alias=alias) diff --git a/electrum/commands.py b/electrum/commands.py index 46cc6ad11..2038a6f85 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -313,7 +313,7 @@ def _setconfig_normalize_value(cls, key, value): # call literal_eval for backward compatibility (see #4225) try: value = ast.literal_eval(value) - except: + except Exception: pass return value @@ -631,7 +631,7 @@ async def convert_xkey(self, xkey, xtype): """Convert xtype of a master key. e.g. xpub -> ypub""" try: node = BIP32Node.from_xkey(xkey) - except: + except Exception: raise Exception('xkey should be a master public/private key') return node._replace(xtype=xtype).to_xkey() @@ -1376,7 +1376,7 @@ def eval_bool(x: str) -> bool: if x == 'true': return True try: return bool(ast.literal_eval(x)) - except: + except Exception: return bool(x) param_descriptions = { diff --git a/electrum/constants.py b/electrum/constants.py index 014106b1e..3facd4d66 100644 --- a/electrum/constants.py +++ b/electrum/constants.py @@ -35,7 +35,7 @@ def read_json(filename, default): try: with open(path, 'r') as f: r = json.loads(f.read()) - except: + except Exception: r = default return r diff --git a/electrum/contacts.py b/electrum/contacts.py index 0649ff7a3..69e8dc060 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -41,7 +41,7 @@ def __init__(self, db): d = self.db.get('contacts', {}) try: self.update(d) - except: + except Exception: return # backward compatibility for k, v in self.items(): diff --git a/electrum/crypto.py b/electrum/crypto.py index 4b50d6825..84d42340c 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -42,7 +42,7 @@ HAS_PYAES = False try: import pyaes -except: +except Exception: pass else: HAS_PYAES = True @@ -57,7 +57,7 @@ from Cryptodome.Cipher import ChaCha20_Poly1305 as CD_ChaCha20_Poly1305 from Cryptodome.Cipher import ChaCha20 as CD_ChaCha20 from Cryptodome.Cipher import AES as CD_AES -except: +except Exception: pass else: HAS_CRYPTODOME = True @@ -75,7 +75,7 @@ from cryptography.hazmat.primitives.ciphers import modes as CG_modes from cryptography.hazmat.backends import default_backend as CG_default_backend import cryptography.hazmat.primitives.ciphers.aead as CG_aead -except: +except Exception: pass else: HAS_CRYPTOGRAPHY = True diff --git a/electrum/ecc.py b/electrum/ecc.py index 687711d10..774f24cf3 100644 --- a/electrum/ecc.py +++ b/electrum/ecc.py @@ -370,7 +370,7 @@ def is_pubkey_bytes(cls, b: bytes) -> bool: try: ECPubkey(b) return True - except: + except Exception: return False diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 2f63b65a9..95adab7af 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -104,7 +104,7 @@ def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]: try: with open(filename, 'r', encoding='utf-8') as f: h = json.loads(f.read()) - except: + except Exception: return None if not h: # e.g. empty dict return None @@ -469,7 +469,7 @@ def get_exchanges_and_currencies(): try: with open(path, 'r', encoding='utf-8') as f: return json.loads(f.read()) - except: + except Exception: pass # or if not present, generate it now. print("cannot find currencies.json. will regenerate it now.") @@ -483,7 +483,7 @@ async def get_currencies_safe(name, exchange): try: d[name] = await exchange.get_currencies() print(name, "ok") - except: + except Exception: print(name, "error") async def query_all_exchanges_for_their_ccys_over_network(): diff --git a/electrum/gui/kivy/i18n.py b/electrum/gui/kivy/i18n.py index 74925ef0d..76cf2a09e 100644 --- a/electrum/gui/kivy/i18n.py +++ b/electrum/gui/kivy/i18n.py @@ -22,14 +22,14 @@ def translate(s, *args, **kwargs): def bind(label): try: _.observers.add(label) - except: + except Exception: pass # garbage collection new = set() for label in _.observers: try: new.add(label) - except: + except Exception: pass _.observers = new @@ -42,7 +42,7 @@ def switch_lang(lang): for label in _.observers: try: label.text = _(label.text.source_text) - except: + except Exception: pass # Note that all invocations of _() inside the core electrum library # use electrum.i18n instead of electrum.gui.kivy.i18n, so we should update the diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 951d7507c..38e3eff98 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -358,7 +358,7 @@ def get_amount(self, amount_str: str) -> Optional[int]: assert u == self.base_unit try: x = Decimal(a) - except: + except Exception: return None p = pow(10, self.decimal_point()) return int(p * x) @@ -487,7 +487,7 @@ def on_data_input(self, data: str) -> None: from electrum.transaction import tx_from_any try: tx = tx_from_any(data) - except: + except Exception: tx = None if tx: self.tx_dialog(tx) diff --git a/electrum/gui/kivy/uix/dialogs/amount_dialog.py b/electrum/gui/kivy/uix/dialogs/amount_dialog.py index 673c62f8f..b7434c02f 100644 --- a/electrum/gui/kivy/uix/dialogs/amount_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/amount_dialog.py @@ -145,7 +145,7 @@ def update_amount(self, c): try: Decimal(amount+c) amount += c - except: + except Exception: pass # truncate btc amounts to max precision: if not kb.is_fiat and '.' in amount: diff --git a/electrum/gui/kivy/uix/dialogs/installwizard.py b/electrum/gui/kivy/uix/dialogs/installwizard.py index 56abde19b..39a439747 100644 --- a/electrum/gui/kivy/uix/dialogs/installwizard.py +++ b/electrum/gui/kivy/uix/dialogs/installwizard.py @@ -665,7 +665,7 @@ def get_otp(self): return try: return int(otp) - except: + except Exception: return def on_text(self, dt): @@ -1037,7 +1037,7 @@ def __init__(self, wizard, **kwargs): def is_valid(x): try: return kwargs['is_valid'](x) - except: + except Exception: return False self.is_valid = is_valid self.title = kwargs['title'] diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 1d6115138..9359fc207 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -341,7 +341,7 @@ def read_invoice(self): else: try: amount_sat = self.app.get_amount(self.amount) - except: + except Exception: self.app.show_error(_('Invalid amount') + ':\n' + self.amount) return message = self.message @@ -384,7 +384,7 @@ def _lnurl_get_invoice(self) -> None: assert self.lnurl_data try: amount = self.app.get_amount(self.amount) - except: + except Exception: self.app.show_error(_('Invalid amount') + ':\n' + self.amount) return if not (self.lnurl_data.min_sendable_sat <= amount <= self.lnurl_data.max_sendable_sat): diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index b04ddfda2..694700cce 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -108,7 +108,7 @@ def on_wallet_loaded(self): # connect only once try: qewallet.userNotify.disconnect(self.on_wallet_usernotify) - except: + except Exception: pass qewallet.userNotify.connect(self.on_wallet_usernotify) diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index d0c7a0763..f651123a9 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -158,7 +158,7 @@ def isRawTx(self, rawtx): try: tx_from_any(rawtx) return True - except: + except Exception: return False @pyqtSlot(str, result=bool) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 7a4c1720b..b71811c94 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -248,7 +248,7 @@ def unitsToSats(self, unitAmount): self._amount = QEAmount() try: x = Decimal(unitAmount) - except: + except Exception: return self._amount # scale it to max allowed precision, make it an int diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index 689481cfa..8a226e2d5 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -105,7 +105,7 @@ def fiatValue(self, satoshis, plain=True): else: try: sd = Decimal(satoshis) - except: + except Exception: return '' if plain: return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), add_thousands_sep=False) @@ -122,14 +122,14 @@ def fiatValueHistoric(self, satoshis, timestamp, plain=True): else: try: sd = Decimal(satoshis) - except: + except Exception: return '' try: td = Decimal(timestamp) if td == 0: return '' - except: + except Exception: return '' dt = datetime.fromtimestamp(int(td)) if plain: @@ -143,7 +143,7 @@ def satoshiValue(self, fiat, plain=True): rate = self.fx.exchange_rate() try: fd = Decimal(fiat) - except: + except Exception: return '' v = fd / Decimal(rate) * COIN if v.is_nan(): diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 3b40ee596..0509c3fa4 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -617,7 +617,7 @@ def lnurlGetInvoice(self, amount, comment=None): try: assert amount >= self.lnurlData['min_sendable_sat'] assert amount <= self.lnurlData['max_sendable_sat'] - except: + except Exception: self.lnurlError.emit('amount', _('Amount out of bounds')) return diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index ee6aa28d0..e1af555aa 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -323,7 +323,7 @@ def _sign(self, broadcast): if broadcast: self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) - except: + except Exception: pass if broadcast: @@ -344,7 +344,7 @@ def broadcast(self): try: self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) - except: + except Exception: pass self._wallet.broadcastFailed.connect(self.onBroadcastFailed) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index d88f9817e..6eb7cb5ed 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -739,7 +739,7 @@ def retrieve_seed(self): try: self._seed = self.wallet.get_seed(self.password) self.seedRetrieved.emit() - except: + except Exception: self._seed = '' self.dataChanged.emit() diff --git a/electrum/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py index c6b55c81c..d4bc86c19 100644 --- a/electrum/gui/qt/amountedit.py +++ b/electrum/gui/qt/amountedit.py @@ -97,7 +97,7 @@ def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]: try: text = text.replace(DECIMAL_POINT, '.') return (int if self.is_int else Decimal)(text) - except: + except Exception: return None def get_amount(self) -> Union[None, Decimal, int]: @@ -130,7 +130,7 @@ def _get_amount_from_text(self, text): try: text = text.replace(DECIMAL_POINT, '.') x = Decimal(text) - except: + except Exception: return None # scale it to max allowed precision, make it an int power = pow(10, self.max_precision()) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 927659aa5..9f10c031f 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -92,7 +92,7 @@ def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): if v2 is None or isinstance(v2, Decimal) and v2.is_nan(): v2 = -float("inf") try: return v1 < v2 - except: + except Exception: return False def get_item_key(tx_item): @@ -538,7 +538,7 @@ def on_combo(self, x): else: try: year = int(s) - except: + except Exception: return self.start_date = datetime.datetime(year, 1, 1) self.end_date = datetime.datetime(year+1, 1, 1) diff --git a/electrum/gui/qt/locktimeedit.py b/electrum/gui/qt/locktimeedit.py index 2ac30b709..f81f3b884 100644 --- a/electrum/gui/qt/locktimeedit.py +++ b/electrum/gui/qt/locktimeedit.py @@ -93,7 +93,7 @@ def is_acceptable_locktime(cls, x: Any) -> bool: return True try: x = int(x) - except: + except Exception: return False return cls.min_allowed_value <= x <= cls.max_allowed_value @@ -120,13 +120,13 @@ def numbify(self): def get_locktime(self) -> Optional[int]: try: return int(str(self.text())) - except: + except Exception: return None def set_locktime(self, x: Any) -> None: try: x = int(x) - except: + except Exception: self.setText('') return x = max(x, self.min_allowed_value) @@ -185,7 +185,7 @@ def set_locktime(self, x: Any) -> None: return try: x = int(x) - except: + except Exception: self.setDateTime(QDateTime.currentDateTime()) return dt = datetime.fromtimestamp(x) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index c8d888495..89a3f87ef 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -515,7 +515,7 @@ def init_geometry(self): screen = self.app.desktop().screenGeometry() assert screen.contains(QRect(*winpos)) self.setGeometry(*winpos) - except: + except Exception: self.logger.info("using default geometry") self.setGeometry(100, 100, 840, 400) @@ -631,7 +631,7 @@ def update_recently_visited(self, filename): recent = self.config.get('recently_open', []) try: sorted(recent) - except: + except Exception: recent = [] if filename in recent: recent.remove(filename) diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index acbd560c0..13918b075 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -125,7 +125,7 @@ def lessThan(self, source_left: QModelIndex, source_right: QModelIndex): v2 = item2.text() try: return Decimal(v1) < Decimal(v2) - except: + except Exception: return v1 < v2 class ElectrumItemDelegate(QStyledItemDelegate): diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 3ae4044e4..6c4950a7b 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -287,7 +287,7 @@ def on_be_edit(): val = block_ex_custom_e.text() try: val = ast.literal_eval(val) # to also accept tuples - except: + except Exception: pass self.config.set_key('block_explorer_custom', val) block_ex_custom_e.editingFinished.connect(on_be_edit) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 32f06398a..3ba3bcebc 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -727,7 +727,7 @@ def qr_show(): from .qrcodewidget import QRDialog try: s = str(self.text()) - except: + except Exception: s = self.text() if not s: return diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 22f6361e1..7fbad039f 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -35,14 +35,14 @@ def parse_bip21(text): try: return util.parse_URI(text) - except: + except Exception: return def parse_bolt11(text): from electrum.lnaddr import lndecode try: return lndecode(text) - except: + except Exception: return @@ -594,7 +594,7 @@ def do_paste(self): def parse_amount(self, text): try: x = Decimal(text) - except: + except Exception: return None power = pow(10, self.config.get_decimal_point()) return int(power * x) diff --git a/electrum/interface.py b/electrum/interface.py index 737c94363..782c5af41 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -1153,14 +1153,14 @@ def check_cert(host, cert): try: b = pem.dePem(cert, 'CERTIFICATE') x = x509.X509(b) - except: + except Exception: traceback.print_exc(file=sys.stdout) return try: x.check_date() expired = False - except: + except Exception: expired = True m = "host: %s\n"%host diff --git a/electrum/json_db.py b/electrum/json_db.py index 681107550..00f249c16 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -155,7 +155,7 @@ def put(self, key, value): try: json.dumps(key, cls=JsonDBJsonEncoder) json.dumps(value, cls=JsonDBJsonEncoder) - except: + except Exception: self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})") return False if value is not None: diff --git a/electrum/keystore.py b/electrum/keystore.py index c1c5638a6..61305635c 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -1084,13 +1084,13 @@ def load_keystore(db: 'WalletDB', name: str) -> KeyStore: def is_old_mpk(mpk: str) -> bool: try: int(mpk, 16) # test if hex string - except: + except Exception: return False if len(mpk) != 128: return False try: ecc.ECPubkey(bfh('04' + mpk)) - except: + except Exception: return False return True diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index c4f2a3827..2dd1092cb 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -602,7 +602,7 @@ def close_and_cleanup(self): try: if self.transport: self.transport.close() - except: + except Exception: pass self.lnworker.peer_closed(self) self.got_disconnected.set() @@ -1594,7 +1594,7 @@ def maybe_forward_htlc( raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') try: next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] - except: + except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid) local_height = chain.height() @@ -1610,14 +1610,14 @@ def maybe_forward_htlc( raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) try: next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"] - except: + except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') if not next_chan.can_pay(next_amount_msat_htlc): self.logger.info(f"cannot forward htlc due to transient errors (likely due to insufficient funds)") raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) try: next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] - except: + except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') if htlc.cltv_expiry - next_cltv_expiry < next_chan.forwarding_cltv_expiry_delta: data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_message @@ -1746,7 +1746,7 @@ def log_fail_reason(reason: str): try: amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"] - except: + except Exception: log_fail_reason(f"'amt_to_forward' missing from onion") raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') @@ -1766,7 +1766,7 @@ def log_fail_reason(reason: str): raise exc_incorrect_or_unknown_pd try: cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] - except: + except Exception: log_fail_reason(f"'outgoing_cltv_value' missing from onion") raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') @@ -1778,7 +1778,7 @@ def log_fail_reason(reason: str): data=htlc.cltv_expiry.to_bytes(4, byteorder="big")) try: total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"] - except: + except Exception: total_msat = amt_to_forward # fall back to "amt_to_forward" if not is_trampoline and amt_to_forward != htlc.amount_msat: @@ -1789,7 +1789,7 @@ def log_fail_reason(reason: str): try: payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"] - except: + except Exception: if total_msat > amt_to_forward: # payment_secret is required for MPP log_fail_reason(f"'payment_secret' missing from onion") diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 49b424ba1..e348659af 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1470,7 +1470,7 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, Optional[str]]: invoice = lndecode(connect_contents) nodeid_bytes = invoice.pubkey.serialize() nodeid_hex = nodeid_bytes.hex() - except: + except Exception: # node id as hex? nodeid_hex = connect_contents if rest == '': @@ -1479,7 +1479,7 @@ def extract_nodeid(connect_contents: str) -> Tuple[bytes, Optional[str]]: node_id = bfh(nodeid_hex) if len(node_id) != 33: raise Exception() - except: + except Exception: raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal')) return node_id, rest diff --git a/electrum/lnworker.py b/electrum/lnworker.py index ffc56c74f..054fa4c15 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1072,7 +1072,7 @@ def add_new_channel(self, chan: Channel): self.wallet.set_reserved_state_of_address(addr, reserved=True) try: self.save_channel(chan) - except: + except Exception: chan.set_state(ChannelState.REDEEMED) self.remove_channel(chan.channel_id) raise @@ -1516,13 +1516,13 @@ def _decode_channel_update_msg(cls, chan_upd_msg: bytes) -> Optional[Dict[str, A if payload['chain_hash'] != constants.net.rev_genesis_bytes(): raise Exception() payload['raw'] = channel_update_typed return payload - except: # FIXME: too broad + except Exception: # FIXME: too broad try: message_type, payload = decode_msg(channel_update_as_received) if payload['chain_hash'] != constants.net.rev_genesis_bytes(): raise Exception() payload['raw'] = channel_update_as_received return payload - except: + except Exception: return None @staticmethod diff --git a/electrum/network.py b/electrum/network.py index e77836a95..47ed65baa 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -403,7 +403,7 @@ def _read_recent_servers(self) -> List[ServerAddr]: data = f.read() servers_list = json.loads(data) return [ServerAddr.from_str(s) for s in servers_list] - except: + except Exception: return [] @with_recent_servers_lock @@ -415,7 +415,7 @@ def _save_recent_servers(self): try: with open(path, "w", encoding='utf-8') as f: f.write(s) - except: + except Exception: pass async def _server_is_lagging(self) -> bool: @@ -516,7 +516,7 @@ def get_fee_estimates(self): for n in FEE_ETA_TARGETS: try: out[n] = int(median(filter(None, [i.fee_estimates_eta.get(n) for i in self.interfaces.values()]))) - except: + except Exception: continue return out else: @@ -595,7 +595,7 @@ def _set_default_server(self) -> None: if server: try: self.default_server = ServerAddr.from_str(server) - except: + except Exception: self.logger.warning(f'failed to parse server-string ({server!r}); falling back to localhost:1:s.') self.default_server = ServerAddr.from_str("localhost:1:s") else: @@ -626,7 +626,7 @@ async def set_parameters(self, net_params: NetworkParameters): if proxy: proxy_modes.index(proxy['mode']) + 1 int(proxy['port']) - except: + except Exception: return self.config.set_key('auto_connect', net_params.auto_connect, False) self.config.set_key('oneserver', net_params.oneserver, False) diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 8d46909e2..00e6d4f62 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -126,7 +126,7 @@ def parse(self, r: bytes): try: self.data = pb2.PaymentRequest() self.data.ParseFromString(r) - except: + except Exception: self.error = "cannot parse payment request" return self.details = pb2.PaymentDetails() @@ -157,7 +157,7 @@ def verify(self, contacts): pr = pb2.PaymentRequest() try: pr.ParseFromString(self.raw) - except: + except Exception: self.error = "Error: Cannot parse payment request" return False if not pr.signature: diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index fcd14a241..e07544213 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -79,7 +79,7 @@ def is_initialized(self) -> bool: def close(self): try: self.bitbox02_device.close() - except: + except Exception: pass def has_usable_connection_with_device(self) -> bool: @@ -104,7 +104,7 @@ def pairing_step(code: str, device_response: Callable[[], bool]) -> bool: self.handler.show_message(msg) try: res = device_response() - except: + except Exception: # Close the hid device on exception hid_device.close() raise @@ -327,7 +327,7 @@ def btc_multisig_config( ) except bitbox02.DuplicateEntryException: raise - except: + except Exception: raise UserFacingException("Failed to register multisig\naccount configuration on BitBox02") return multisig_config @@ -648,7 +648,7 @@ def get_library_version(self): try: from bitbox02 import bitbox02 version = bitbox02.__version__ - except: + except Exception: version = "unknown" if requirements_ok: return version diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py index 15d57f374..972e6def0 100644 --- a/electrum/plugins/coldcard/coldcard.py +++ b/electrum/plugins/coldcard/coldcard.py @@ -160,7 +160,7 @@ def has_usable_connection_with_device(self): try: self.ping_check() return True - except: + except Exception: return False @runs_in_hwd_thread @@ -187,7 +187,7 @@ def ping_check(self): try: echo = self.dev.send_recv(CCProtocolPacker.ping(req)) assert echo == req - except: + except Exception: raise RuntimeError("Communication trouble with Coldcard") @runs_in_hwd_thread diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 2701d6ca6..1c82b3a76 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -81,7 +81,7 @@ def close(self): if self.opened: try: self.dbb_hid.close() - except: + except Exception: pass self.opened = False diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 348ebf556..56b835a88 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -411,7 +411,7 @@ def get_library_version(self): version = jadepy.__version__ except ImportError: raise - except: + except Exception: version = "unknown" return version diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index d54afe3ee..a20cae8f4 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -116,7 +116,7 @@ async def push_thread(self, wallet: 'Abstract_Wallet'): try: encoded_key = self.encode(wallet, key) encoded_value = self.encode(wallet, value) - except: + except Exception: self.logger.info(f'cannot encode {repr(key)} {repr(value)}') continue bundle["labels"].append({'encryptedLabel': encoded_value, @@ -142,12 +142,12 @@ async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool): try: key = self.decode(wallet, label["externalId"]) value = self.decode(wallet, label["encryptedLabel"]) - except: + except Exception: continue try: json.dumps(key) json.dumps(value) - except: + except Exception: self.logger.info(f'error: no json {key}') continue if value: diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 78b281668..1b1ba4232 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1355,7 +1355,7 @@ def get_library_version(self): version = ledger_bitcoin.__version__ except ImportError: raise - except: + except Exception: version = "unknown" if LEDGER_BITCOIN: return version diff --git a/electrum/plugins/revealer/revealer.py b/electrum/plugins/revealer/revealer.py index 1ca1b8fc6..0a2850d2b 100644 --- a/electrum/plugins/revealer/revealer.py +++ b/electrum/plugins/revealer/revealer.py @@ -47,7 +47,7 @@ def get_versioned_seed_from_user_input(cls, txt: str) -> Optional[VersionedSeed] return None try: int(txt, 16) - except: + except Exception: return None version = txt[0] if version not in cls.KNOWN_VERSIONS: diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 20954c204..3ff7ff8ec 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -133,12 +133,12 @@ async def handle_response(self, resp: ClientResponse): try: r = await resp.json() message = r['message'] - except: + except Exception: message = await resp.text() raise TrustedCoinException(message, resp.status) try: return await resp.json() - except: + except Exception: return await resp.text() def send_request(self, method, relative_url, data=None, *, timeout=None): diff --git a/electrum/scripts/txbroadcast.py b/electrum/scripts/txbroadcast.py index cf813b23c..6cde24d27 100644 --- a/electrum/scripts/txbroadcast.py +++ b/electrum/scripts/txbroadcast.py @@ -12,7 +12,7 @@ try: rawtx = sys.argv[1] -except: +except Exception: print("usage: txbroadcast rawtx") sys.exit(1) diff --git a/electrum/scripts/txradar.py b/electrum/scripts/txradar.py index 8e301fd5d..c220733e7 100755 --- a/electrum/scripts/txradar.py +++ b/electrum/scripts/txradar.py @@ -9,7 +9,7 @@ try: txid = sys.argv[1] -except: +except Exception: print("usage: txradar txid") sys.exit(1) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index d57418b5f..181e7ba80 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -165,7 +165,7 @@ def set_key(self, key, value, save=True): try: json.dumps(key) json.dumps(value) - except: + except Exception: self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})") return self._set_key_in_user_config(key, value, save) @@ -674,7 +674,7 @@ def get_netaddress(self, key: str) -> Optional[NetAddress]: if text: try: return NetAddress.from_string(text) - except: + except Exception: pass def format_amount( diff --git a/electrum/storage.py b/electrum/storage.py index 2eecc7eb0..5d08c8a68 100644 --- a/electrum/storage.py +++ b/electrum/storage.py @@ -142,7 +142,7 @@ def _init_encryption_version(self): return StorageEncryptionVersion.XPUB_PASSWORD else: return StorageEncryptionVersion.PLAINTEXT - except: + except Exception: return StorageEncryptionVersion.PLAINTEXT @staticmethod diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index b0af03fd4..5708a0df9 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -501,7 +501,7 @@ def init_min_max_values(self): limits = pairs['pairs']['BTC/BTC']['limits'] self._min_amount = limits['minimal'] self._max_amount = limits['maximal'] - except: + except Exception: self._min_amount = 10000 self._max_amount = 10000000 diff --git a/electrum/transaction.py b/electrum/transaction.py index 0012468d8..b5bf0705e 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1195,18 +1195,18 @@ def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str: # try hex try: return binascii.unhexlify(raw).hex() - except: + except Exception: pass # try base43 try: return base_decode(raw, base=43).hex() - except: + except Exception: pass # try base64 if raw[0:6] in ('cHNidP', b'cHNidP'): # base64 psbt try: return base64.b64decode(raw).hex() - except: + except Exception: pass # raw bytes (do not strip whitespaces in this case) if isinstance(raw_unstripped, bytes): diff --git a/electrum/util.py b/electrum/util.py index a8e8dd648..a165117a9 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -432,7 +432,7 @@ def json_encode(obj): def json_decode(x): try: return json.loads(x, parse_float=Decimal) - except: + except Exception: return x def json_normalize(x): @@ -562,7 +562,7 @@ def assert_bytes(*args): try: for x in args: assert isinstance(x, (bytes, bytearray)) - except: + except Exception: print('assert bytes failed', list(map(type, args))) raise @@ -646,7 +646,7 @@ def is_hex_str(text: Any) -> bool: if not isinstance(text, str): return False try: b = bytes.fromhex(text) - except: + except Exception: return False # forbid whitespaces in text: if len(text) != 2 * len(b): @@ -1191,7 +1191,7 @@ def parse_json(message): return None, message try: j = json.loads(message[0:n].decode('utf8')) - except: + except Exception: j = None return j, message[n+1:] diff --git a/electrum/verifier.py b/electrum/verifier.py index c5eb8c000..13f556b0d 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -169,7 +169,7 @@ def _raise_if_valid_tx(cls, raw_tx: str): tx = Transaction(raw_tx) try: tx.deserialize() - except: + except Exception: pass else: raise InnerNodeOfSpvProofIsValidTx() diff --git a/electrum/wallet.py b/electrum/wallet.py index 1afccd715..dff9cef8a 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -653,7 +653,7 @@ def set_fiat_value(self, txid, ccy, text, fx, value_sat): text_dec = Decimal(text) text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, add_thousands_sep=False)) reset = text_dec_rounded == def_fiat_rounded - except: + except Exception: # garbage. not resetting, but not saving either return False if reset: @@ -673,7 +673,7 @@ def get_fiat_value(self, txid, ccy): fiat_value = self.fiat_value.get(ccy, {}).get(txid) try: return Decimal(fiat_value) - except: + except Exception: return def is_mine(self, address) -> bool: @@ -840,7 +840,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: try: self.cpfp(tx, 0) can_cpfp = True - except: + except Exception: can_cpfp = False else: status = _('Local') @@ -1107,7 +1107,7 @@ def import_requests(self, path): for x in data: try: req = Request(**x) - except: + except Exception: raise FileImportFailed(_("Invalid invoice format")) self.add_payment_request(req, write_to_disk=False) self.save_db() @@ -1121,7 +1121,7 @@ def import_invoices(self, path): for x in data: try: invoice = Invoice(**x) - except: + except Exception: raise FileImportFailed(_("Invalid invoice format")) self.save_invoice(invoice, write_to_disk=False) self.save_db() @@ -3460,7 +3460,7 @@ def load_keystore(self): self.keystore = load_keystore(self.db, 'keystore') # type: KeyStoreWithMPK try: xtype = bip32.xpub_type(self.keystore.xpub) - except: + except Exception: xtype = 'standard' self.txin_type = 'p2pkh' if xtype == 'standard' else xtype diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 3ef8a3a53..fd17f21c7 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -98,7 +98,7 @@ def __init__(self, raw, *, manual_upgrades: bool): def load_data(self, s): try: self.data = json.loads(s) - except: + except Exception: try: d = ast.literal_eval(s) labels = d.get('labels', {}) @@ -109,7 +109,7 @@ def load_data(self, s): try: json.dumps(key) json.dumps(value) - except: + except Exception: self.logger.info(f'Failed to convert label to json format: {key}') continue self.data[key] = value From e2406f21b4ccddb8231b5f0d0574179a9fcad101 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 01:45:14 +0000 Subject: [PATCH 0812/1143] fix flake8-bugbear B011 B011 Do not call assert False since python -O removes these calls. Instead callers should raise AssertionError(). --- electrum/gui/qt/history_list.py | 2 +- electrum/network.py | 2 +- electrum/plugins/trustedcoin/trustedcoin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 9f10c031f..cb4a2383f 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -709,7 +709,7 @@ def on_edited(self, idx, edit_key, *, text): if value is not None: self.hm.update_fiat(index) else: - assert False + raise Exception(f"did not expect {column=!r} to get edited") def on_double_click(self, idx): tx_item = idx.internalPointer().get_data() diff --git a/electrum/network.py b/electrum/network.py index 47ed65baa..e12f28a62 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -1362,7 +1362,7 @@ async def default_on_finish(resp: ClientResponse): async with session.post(url, json=json, headers=headers) as resp: return await on_finish(resp) else: - assert False + raise Exception(f"unexpected {method=!r}") @classmethod def send_http_on_proxy(cls, method, url, **kwargs): diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 3ff7ff8ec..181425dc2 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -165,7 +165,7 @@ def send_request(self, method, relative_url, data=None, *, timeout=None): on_finish=self.handle_response, timeout=timeout) else: - assert False + raise Exception(f"unexpected {method=!r}") except TrustedCoinException: raise except Exception as e: From 8266ebcc46a283c97630a1d2e1e3e54c252bc25b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 01:48:42 +0000 Subject: [PATCH 0813/1143] fix flake8-bugbear B008 B008 Do not perform function calls in argument defaults. The call is performed only once at function definition time. All calls to your function will reuse the result of that definition-time function call. If this is intended, assign the function call to a module-level variable and use that variable as a default value. --- electrum/gui/kivy/main_window.py | 4 +++- electrum/gui/qt/main_window.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 38e3eff98..892d6ffa3 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -1119,7 +1119,7 @@ def show_info(self, error, width='200dp', pos=None, arrow_pos=None, arrow_pos=arrow_pos) @scheduled_in_gui_thread - def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, + def show_info_bubble(self, text=None, pos=None, duration=0, arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False): '''Method to show an Information Bubble @@ -1130,6 +1130,8 @@ def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, width: width of the Bubble arrow_pos: arrow position for the bubble ''' + if text is None: + text = _('Hello World') text = str(text) # so that we also handle e.g. Exception info_bubble = self.info_bubble if not info_bubble: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 89a3f87ef..a67420ce2 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1294,8 +1294,10 @@ def on_open_channel_success(self, args): else: self.show_message(message) - def query_choice(self, msg, choices, title=_('Question'), default_choice=None): + def query_choice(self, msg, choices, title=None, default_choice=None): # Needed by QtHandler for hardware wallets + if title is None: + title = _('Question') dialog = WindowModalDialog(self.top_level_window(), title=title) dialog.setMinimumWidth(400) clayout = ChoicesLayout(msg, choices, checked_index=default_choice) @@ -1918,10 +1920,12 @@ def show_seed_dialog(self, password): d = SeedDialog(self, seed, passphrase, config=self.config) d.exec_() - def show_qrcode(self, data, title = _("QR code"), parent=None, *, + def show_qrcode(self, data, title=None, parent=None, *, help_text=None, show_copy_text_btn=False): if not data: return + if title is None: + title = _("QR code") d = QRDialog( data=data, parent=parent or self, From 90315e72d6bbb019a5dc4702e6cd55a4f93bff6a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 01:50:43 +0000 Subject: [PATCH 0814/1143] fix flake8-bugbear B016 B016 Cannot raise a literal. Did you intend to return it or raise an Exception? --- electrum/ripemd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/ripemd.py b/electrum/ripemd.py index b39cdc17b..bc30089eb 100644 --- a/electrum/ripemd.py +++ b/electrum/ripemd.py @@ -151,7 +151,7 @@ def RMD160Transform(state, block): #uint32 state[5], uchar block[64] if sys.byteorder == 'little': x = struct.unpack('<16L', bytes([x for x in block[0:64]])) else: - raise "Error!!" + raise Exception(f"unsupported {sys.byteorder=!r}") a = state[0] b = state[1] c = state[2] From 612d3493df2e0fc5a84ea7d8cfa00c4faed4d6fb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 02:00:50 +0000 Subject: [PATCH 0815/1143] fix flake8-bugbear B017 B017 `assertRaises(Exception)` and `pytest.raises(Exception)` should be considered evil. They can lead to your test passing even if the code being tested is never executed due to a typo. Assert for a more specific exception (builtin or custom), or use `assertRaisesRegex` (if using `assertRaises`), or add the `match` keyword argument (if using `pytest.raises`), or use the context manager form with a target. --- electrum/blockchain.py | 14 +++++++------- electrum/tests/test_blockchain.py | 16 ++++++++-------- electrum/tests/test_commands.py | 13 +++++++------ electrum/wallet.py | 6 +++--- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 4a6aad4ed..4cca1d282 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -296,17 +296,17 @@ def update_size(self) -> None: def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None: _hash = hash_header(header) if expected_header_hash and expected_header_hash != _hash: - raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash)) + raise InvalidHeader("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash)) if prev_hash != header.get('prev_block_hash'): - raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) + raise InvalidHeader("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) if constants.net.TESTNET: return bits = cls.target_to_bits(target) if bits != header.get('bits'): - raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits'))) + raise InvalidHeader("bits mismatch: %s vs %s" % (bits, header.get('bits'))) block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big') if block_hash_as_num > target: - raise Exception(f"insufficient proof of work: {block_hash_as_num} vs target {target}") + raise InvalidHeader(f"insufficient proof of work: {block_hash_as_num} vs target {target}") def verify_chunk(self, index: int, data: bytes) -> None: num = len(data) // HEADER_SIZE @@ -544,7 +544,7 @@ def get_target(self, index: int) -> int: def bits_to_target(cls, bits: int) -> int: # arith_uint256::SetCompact in Bitcoin Core if not (0 <= bits < (1 << 32)): - raise Exception(f"bits should be uint32. got {bits!r}") + raise InvalidHeader(f"bits should be uint32. got {bits!r}") bitsN = (bits >> 24) & 0xff bitsBase = bits & 0x7fffff if bitsN <= 3: @@ -553,12 +553,12 @@ def bits_to_target(cls, bits: int) -> int: target = bitsBase << (8 * (bitsN-3)) if target != 0 and bits & 0x800000 != 0: # Bit number 24 (0x800000) represents the sign of N - raise Exception("target cannot be negative") + raise InvalidHeader("target cannot be negative") if (target != 0 and (bitsN > 34 or (bitsN > 33 and bitsBase > 0xff) or (bitsN > 32 and bitsBase > 0xffff))): - raise Exception("target has overflown") + raise InvalidHeader("target has overflown") return target @classmethod diff --git a/electrum/tests/test_blockchain.py b/electrum/tests/test_blockchain.py index cc57d21ef..b16850045 100644 --- a/electrum/tests/test_blockchain.py +++ b/electrum/tests/test_blockchain.py @@ -4,7 +4,7 @@ from electrum import constants, blockchain from electrum.simple_config import SimpleConfig -from electrum.blockchain import Blockchain, deserialize_header, hash_header +from electrum.blockchain import Blockchain, deserialize_header, hash_header, InvalidHeader from electrum.util import bfh, make_dir from . import ElectrumTestCase @@ -418,11 +418,11 @@ def test_target_to_bits(self): # Make sure that we don't generate compacts with the 0x00800000 bit set self.assertEqual(0x02008000, Blockchain.target_to_bits(0x80)) - with self.assertRaises(Exception): # target cannot be negative + with self.assertRaises(InvalidHeader): # target cannot be negative Blockchain.bits_to_target(0x01fedcba) - with self.assertRaises(Exception): # target cannot be negative + with self.assertRaises(InvalidHeader): # target cannot be negative Blockchain.bits_to_target(0x04923456) - with self.assertRaises(Exception): # overflow + with self.assertRaises(InvalidHeader): # overflow Blockchain.bits_to_target(0xff123456) @@ -441,20 +441,20 @@ def test_valid_header(self): Blockchain.verify_header(self.header, self.prev_hash, self.target) def test_expected_hash_mismatch(self): - with self.assertRaises(Exception): + with self.assertRaises(InvalidHeader): Blockchain.verify_header(self.header, self.prev_hash, self.target, expected_header_hash="foo") def test_prev_hash_mismatch(self): - with self.assertRaises(Exception): + with self.assertRaises(InvalidHeader): Blockchain.verify_header(self.header, "foo", self.target) def test_target_mismatch(self): - with self.assertRaises(Exception): + with self.assertRaises(InvalidHeader): other_target = Blockchain.bits_to_target(0x1d00eeee) Blockchain.verify_header(self.header, self.prev_hash, other_target) def test_insufficient_pow(self): - with self.assertRaises(Exception): + with self.assertRaises(InvalidHeader): self.header["nonce"] = 42 Blockchain.verify_header(self.header, self.prev_hash, self.target) diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py index ff27a0db8..49eaa1165 100644 --- a/electrum/tests/test_commands.py +++ b/electrum/tests/test_commands.py @@ -8,6 +8,7 @@ from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED from electrum.simple_config import SimpleConfig from electrum.transaction import Transaction, TxOutput, tx_from_any +from electrum.util import UserFacingException from . import ElectrumTestCase from .test_wallet_vertical import WalletIntegrityHelper @@ -91,14 +92,14 @@ async def test_export_private_key_imported(self, mock_save_db): config=self.config)['wallet'] cmds = Commands(config=self.config) # single address tests - with self.assertRaises(Exception): + with self.assertRaises(UserFacingException): await cmds.getprivatekeys("asdasd", wallet=wallet) # invalid addr, though might raise "not in wallet" - with self.assertRaises(Exception): + with self.assertRaises(UserFacingException): await cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23", wallet=wallet) # not in wallet self.assertEqual("p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL", await cmds.getprivatekeys("bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw", wallet=wallet)) # list of addresses tests - with self.assertRaises(Exception): + with self.assertRaises(UserFacingException): await cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'asd'], wallet=wallet) self.assertEqual(['p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], await cmds.getprivatekeys(['bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], wallet=wallet)) @@ -111,14 +112,14 @@ async def test_export_private_key_deterministic(self, mock_save_db): config=self.config)['wallet'] cmds = Commands(config=self.config) # single address tests - with self.assertRaises(Exception): + with self.assertRaises(UserFacingException): await cmds.getprivatekeys("asdasd", wallet=wallet) # invalid addr, though might raise "not in wallet" - with self.assertRaises(Exception): + with self.assertRaises(UserFacingException): await cmds.getprivatekeys("bc1qgfam82qk7uwh5j2xxmcd8cmklpe0zackyj6r23", wallet=wallet) # not in wallet self.assertEqual("p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2", await cmds.getprivatekeys("bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af", wallet=wallet)) # list of addresses tests - with self.assertRaises(Exception): + with self.assertRaises(UserFacingException): await cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'asd'], wallet=wallet) self.assertEqual(['p2wpkh:L15oxP24NMNAXxq5r2aom24pHPtt3Fet8ZutgL155Bad93GSubM2', 'p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN'], await cmds.getprivatekeys(['bc1q3g5tmkmlvxryhh843v4dz026avatc0zzr6h3af', 'bc1q9pzjpjq4nqx5ycnywekcmycqz0wjp2nq604y2n'], wallet=wallet)) diff --git a/electrum/wallet.py b/electrum/wallet.py index dff9cef8a..cb0e5d01a 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -721,11 +721,11 @@ def get_txin_type(self, address: str) -> str: def export_private_key(self, address: str, password: Optional[str]) -> str: if self.is_watching_only(): - raise Exception(_("This is a watching-only wallet")) + raise UserFacingException(_("This is a watching-only wallet")) if not is_address(address): - raise Exception(f"Invalid bitcoin address: {address}") + raise UserFacingException(f"Invalid bitcoin address: {address}") if not self.is_mine(address): - raise Exception(_('Address not in wallet.') + f' {address}') + raise UserFacingException(_('Address not in wallet.') + f' {address}') index = self.get_address_index(address) pk, compressed = self.keystore.get_private_key(index, password) txin_type = self.get_txin_type(address) From 4219022c2ee08eb635204eacfeabcf66059fb602 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 02:23:40 +0000 Subject: [PATCH 0816/1143] fix flake8-bugbear B023 B023 Function definition does not bind loop variable 'already_selected_buckets_value_sum' in keepkey/qt.py, looks like this was an actual bug (fixed in trezor plugin already: https://github.com/spesmilo/electrum/commit/52a4810752c37312aeb7cb4739b5462dfdde2c58 ) --- electrum/coinchooser.py | 6 +++++- electrum/daemon.py | 5 ++++- electrum/plugins/keepkey/qt.py | 2 +- electrum/plugins/ledger/ledger.py | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index 67137b693..e59e3e6d7 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -410,7 +410,11 @@ def bucket_candidates_prefer_confirmed(self, buckets: List[Bucket], for bkts_choose_from in bucket_sets: try: - def sfunds(bkts, *, bucket_value_sum): + def sfunds( + bkts, *, bucket_value_sum, + already_selected_buckets_value_sum=already_selected_buckets_value_sum, + already_selected_buckets=already_selected_buckets, + ): bucket_value_sum += already_selected_buckets_value_sum return sufficient_funds(already_selected_buckets + bkts, bucket_value_sum=bucket_value_sum) diff --git a/electrum/daemon.py b/electrum/daemon.py index 2a10b106a..674e143bc 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -110,6 +110,7 @@ def request(config: SimpleConfig, endpoint, args=(), timeout=60): lockfile = get_lockfile(config) while True: create_time = None + path = None try: with open(lockfile) as f: socktype, address, create_time = ast.literal_eval(f.read()) @@ -127,7 +128,9 @@ def request(config: SimpleConfig, endpoint, args=(), timeout=60): server_url = 'http://%s:%d' % (host, port) auth = aiohttp.BasicAuth(login=rpc_user, password=rpc_password) loop = util.get_asyncio_loop() - async def request_coroutine(): + async def request_coroutine( + *, socktype=socktype, path=path, auth=auth, server_url=server_url, endpoint=endpoint, + ): if socktype == 'unix': connector = aiohttp.UnixConnector(path=path) elif socktype == 'tcp': diff --git a/electrum/plugins/keepkey/qt.py b/electrum/plugins/keepkey/qt.py index 00c893e91..7ea74b372 100644 --- a/electrum/plugins/keepkey/qt.py +++ b/electrum/plugins/keepkey/qt.py @@ -201,7 +201,7 @@ def receive_menu(self, menu, addrs, wallet): return for keystore in wallet.get_keystores(): if type(keystore) == self.keystore_class: - def show_address(): + def show_address(keystore=keystore): keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) device_name = "{} ({})".format(self.device, keystore.label) menu.addAction(_("Show on {}").format(device_name), show_address) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 1b1ba4232..0e4245f74 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1171,7 +1171,7 @@ def sign_transaction(self, keystore: Hardware_KeyStore, tx: PartialTransaction, registered_hmac, ) else: - def process_origin(origin: KeyOriginInfo) -> None: + def process_origin(origin: KeyOriginInfo, *, script_addrtype=script_addrtype) -> None: if is_standard_path(origin.path, script_addrtype, get_chain()): # these policies do not need to be registered policy = self.get_singlesig_default_wallet_policy(script_addrtype, origin.path[2]) From 4cbb8399d250c3d63bdd1ffe2ed8937bf6f373d2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 23 Apr 2023 02:33:05 +0000 Subject: [PATCH 0817/1143] CI: also run flake8-bugbear, as part of flake8 --- .cirrus.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index d68194dc8..8ef7f1767 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -132,9 +132,9 @@ task: folder: ~/.cache/pip fingerprint_script: echo Flake8 && echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS install_script: - - pip install flake8 + - pip install flake8 flake8-bugbear flake8_script: - - flake8 . --count --select=$ELECTRUM_LINTERS --show-source --statistics --exclude "*_pb2.py" + - flake8 . --count --select="$ELECTRUM_LINTERS" --ignore="$ELECTRUM_LINTERS_IGNORE" --show-source --statistics --exclude "*_pb2.py,electrum/_vendor/" env: ELECTRUM_IMAGE: python:3.8 ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt @@ -144,10 +144,13 @@ task: # list of error codes: # - https://flake8.pycqa.org/en/latest/user/error-codes.html # - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes - ELECTRUM_LINTERS: E9,E101,E129,E273,E274,E703,E71,F63,F7,F82,W191,W29 + # - https://github.com/PyCQA/flake8-bugbear/tree/8c0e7eb04217494d48d0ab093bf5b31db0921989#list-of-warnings + ELECTRUM_LINTERS: E9,E101,E129,E273,E274,E703,E71,F63,F7,F82,W191,W29,B + ELECTRUM_LINTERS_IGNORE: B007,B009,B010,B019 - name: Flake8 Non-Mandatory env: - ELECTRUM_LINTERS: E,F,W,C90 + ELECTRUM_LINTERS: E,F,W,C90,B + ELECTRUM_LINTERS_IGNORE: "" allow_failures: true task: From e097a3f875283b7b55b9fac2c10408ae1f06d501 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 24 Apr 2023 13:14:58 +0000 Subject: [PATCH 0818/1143] CI: add some task dependencies run linter first, then tests, then binary builds --- .cirrus.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.cirrus.yml b/.cirrus.yml index 8ef7f1767..981fffe35 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -60,6 +60,8 @@ task: CI_BRANCH: $CIRRUS_BRANCH CI_PULL_REQUEST: $CIRRUS_PR # in addition, COVERALLS_REPO_TOKEN is set as an "override" in https://cirrus-ci.com/settings/... + depends_on: + - Flake8 Mandatory task: name: Locale @@ -122,6 +124,8 @@ task: ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt # ElectrumX exits with an error without this: ALLOW_ROOT: 1 + depends_on: + - Flake8 Mandatory task: container: @@ -181,6 +185,8 @@ task: env: CIRRUS_WORKING_DIR: /opt/wine64/drive_c/electrum CIRRUS_DOCKER_CONTEXT: contrib/build-wine + depends_on: + - Tox Python 3.8 task: name: Android build (Kivy $APK_ARCH) @@ -207,6 +213,8 @@ task: - ./contrib/android/make_apk.sh kivy "$APK_ARCH" debug binaries_artifacts: path: "dist/*" + depends_on: + - Tox Python 3.8 task: name: Android build (QML $APK_ARCH) @@ -233,6 +241,8 @@ task: - ./contrib/android/make_apk.sh qml "$APK_ARCH" debug binaries_artifacts: path: "dist/*" + depends_on: + - Tox Python 3.8 ## mac build disabled, as Cirrus CI no longer supports Intel-based mac builds #task: @@ -289,6 +299,8 @@ task: path: "dist/*" env: CIRRUS_DOCKER_CONTEXT: contrib/build-linux/appimage + depends_on: + - Tox Python 3.8 task: container: @@ -310,6 +322,8 @@ task: - name: source-only tarball build env: OMIT_UNCLEAN_FILES: 1 + depends_on: + - Tox Python 3.8 task: name: Submodules From 5ead4feabbaa1ab6646a54e76b5d5d5e53edf355 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 17:24:22 +0200 Subject: [PATCH 0819/1143] qml: wallet menu wider to fit wide translated texts --- electrum/gui/qml/components/WalletMainView.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 4dec7ca0a..6b219083c 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -64,6 +64,8 @@ Item { } property QtObject menu: Menu { + id: menu + parent: Overlay.overlay dim: true modal: true @@ -71,7 +73,7 @@ Item { color: "#44000000" } - id: menu + width: parent.width / 2 MenuItem { icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor From 407769cb5f521cdd1af1312670257653c1f94f81 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 24 Apr 2023 17:32:13 +0200 Subject: [PATCH 0820/1143] qml: remove Scan.qml --- electrum/gui/qml/components/Scan.qml | 37 ---------------------------- 1 file changed, 37 deletions(-) delete mode 100644 electrum/gui/qml/components/Scan.qml diff --git a/electrum/gui/qml/components/Scan.qml b/electrum/gui/qml/components/Scan.qml deleted file mode 100644 index c04d390ee..000000000 --- a/electrum/gui/qml/components/Scan.qml +++ /dev/null @@ -1,37 +0,0 @@ -import QtQuick 2.6 -import QtQuick.Controls 2.0 - -import org.electrum 1.0 - -import "controls" - -Item { - id: scanPage - property string title: qsTr('Scan') - - property bool toolbar: false - - property string scanData - property string error - - signal found - - QRScan { - anchors.top: parent.top - anchors.bottom: parent.bottom - width: parent.width - - onFound: { - scanPage.scanData = scanData - scanPage.found() - } - } - - Button { - anchors.horizontalCenter: parent.horizontalCenter - id: button - anchors.bottom: parent.bottom - text: 'Cancel' - onClicked: app.stack.pop() - } -} From bf41675d4ccaffdddbbbf7bc9a0cbd0222de5208 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 24 Apr 2023 15:33:10 +0000 Subject: [PATCH 0821/1143] qml: invoice/request list: flip sort order: newest on top to be consistent with the History, and with other GUIs (the model is the easiest place to do it. QSortFilterProxyModel/etc looks overkill) --- electrum/gui/qml/qeinvoicelistmodel.py | 25 +++++++++++++++++-------- electrum/wallet.py | 6 +++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 605481adf..551b17d0c 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -1,15 +1,20 @@ from abc import abstractmethod +from typing import TYPE_CHECKING, List, Dict, Any from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex from electrum.logging import get_logger from electrum.util import Satoshis, format_time -from electrum.invoices import BaseInvoice, PR_EXPIRED, LN_EXPIRY_NEVER +from electrum.invoices import BaseInvoice, PR_EXPIRED, LN_EXPIRY_NEVER, Invoice, Request from .util import QtEventListener, qt_event_listener, status_update_timer_interval from .qetypes import QEAmount +if TYPE_CHECKING: + from electrum.wallet import Abstract_Wallet + + class QEAbstractInvoiceListModel(QAbstractListModel): _logger = get_logger(__name__) @@ -21,7 +26,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel): _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, parent=None): + def __init__(self, wallet: 'Abstract_Wallet', parent=None): super().__init__(parent) self.wallet = wallet @@ -159,11 +164,11 @@ def get_invoice_for_key(self, key: str): raise Exception('provide impl') @abstractmethod - def get_invoice_list(self): + def get_invoice_list(self) -> List[BaseInvoice]: raise Exception('provide impl') @abstractmethod - def get_invoice_as_dict(self, invoice: BaseInvoice): + def get_invoice_as_dict(self, invoice: BaseInvoice) -> Dict[str, Any]: raise Exception('provide impl') @@ -191,12 +196,14 @@ def invoice_to_model(self, invoice: BaseInvoice): return item def get_invoice_list(self): - return self.wallet.get_unpaid_invoices() + lst = self.wallet.get_unpaid_invoices() + lst.reverse() + return lst def get_invoice_for_key(self, key: str): return self.wallet.get_invoice(key) - def get_invoice_as_dict(self, invoice: BaseInvoice): + def get_invoice_as_dict(self, invoice: Invoice): return self.wallet.export_invoice(invoice) class QERequestListModel(QEAbstractInvoiceListModel, QtEventListener): @@ -223,12 +230,14 @@ def invoice_to_model(self, invoice: BaseInvoice): return item def get_invoice_list(self): - return self.wallet.get_unpaid_requests() + lst = self.wallet.get_unpaid_requests() + lst.reverse() + return lst def get_invoice_for_key(self, key: str): return self.wallet.get_request(key) - def get_invoice_as_dict(self, invoice: BaseInvoice): + def get_invoice_as_dict(self, invoice: Request): return self.wallet.export_request(invoice) @pyqtSlot(str, int) diff --git a/electrum/wallet.py b/electrum/wallet.py index cb0e5d01a..b48596e93 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1090,12 +1090,12 @@ def clear_requests(self): self._requests_addr_to_key.clear() self.save_db() - def get_invoices(self): + def get_invoices(self) -> List[Invoice]: out = list(self._invoices.values()) out.sort(key=lambda x:x.time) return out - def get_unpaid_invoices(self): + def get_unpaid_invoices(self) -> List[Invoice]: invoices = self.get_invoices() return [x for x in invoices if self.get_invoice_status(x) != PR_PAID] @@ -2638,7 +2638,7 @@ def get_sorted_requests(self) -> List[Request]: out.sort(key=lambda x: x.time) return out - def get_unpaid_requests(self): + def get_unpaid_requests(self) -> List[Request]: out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID] out.sort(key=lambda x: x.time) return out From adf976fef40ea847aee718f341bd20935df8e437 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 24 Apr 2023 16:21:26 +0000 Subject: [PATCH 0822/1143] qml: QERequestDetails: use uppercase in bolt11 QR code same trick as in other GUIs --- electrum/gui/qml/qerequestdetails.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qerequestdetails.py b/electrum/gui/qml/qerequestdetails.py index f957fad62..855b9feb9 100644 --- a/electrum/gui/qml/qerequestdetails.py +++ b/electrum/gui/qml/qerequestdetails.py @@ -1,3 +1,5 @@ +from typing import Optional + from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS from electrum.logging import get_logger @@ -8,6 +10,7 @@ from .qetypes import QEAmount from .util import QtEventListener, event_listener, status_update_timer_interval + class QERequestDetails(QObject, QtEventListener): class Status: @@ -30,7 +33,7 @@ class Status: def __init__(self, parent=None): super().__init__(parent) - self._wallet = None + self._wallet = None # type: Optional[QEWallet] self._key = None self._req = None self._timer = None @@ -118,9 +121,13 @@ def expiration(self): def bolt11(self): can_receive = self._wallet.wallet.lnworker.num_sats_can_receive() if self._wallet.wallet.lnworker else 0 if self._req and can_receive > 0 and (self._req.get_amount_sat() or 0) <= can_receive: - return self._wallet.wallet.get_bolt11_invoice(self._req) + bolt11 = self._wallet.wallet.get_bolt11_invoice(self._req) else: return '' + # encode lightning invoices as uppercase so QR encoding can use + # alphanumeric mode; resulting in smaller QR codes + bolt11 = bolt11.upper() + return bolt11 @pyqtProperty(str, notify=detailsChanged) def bip21(self): From 22fa84a0d51471f30fabd4a4dbb9936dc6a057a0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 24 Apr 2023 16:53:20 +0000 Subject: [PATCH 0823/1143] qml: ReceiveDialog: clicking QR code to show encoded text closes https://github.com/spesmilo/electrum/issues/8331 --- electrum/gui/qml/components/ReceiveDialog.qml | 26 +++--------------- .../gui/qml/components/controls/QRImage.qml | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index d7c6553bb..8839aba2a 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -85,6 +85,7 @@ ElDialog { QRImage { qrdata: _bolt11 render: _render_qr + enable_toggle_text: true } } Component { @@ -92,6 +93,7 @@ ElDialog { QRImage { qrdata: _bip21uri render: _render_qr + enable_toggle_text: true } } Component { @@ -99,32 +101,10 @@ ElDialog { QRImage { qrdata: _address render: _render_qr + enable_toggle_text: true } } } - - MouseArea { - anchors.fill: parent - onClicked: { - if (rootLayout.state == 'bolt11') { - if (_bip21uri != '') - rootLayout.state = 'bip21uri' - else if (_address != '') - rootLayout.state = 'address' - } else if (rootLayout.state == 'bip21uri') { - if (_address != '') - rootLayout.state = 'address' - else if (_bolt11 != '') - rootLayout.state = 'bolt11' - } else if (rootLayout.state == 'address') { - if (_bolt11 != '') - rootLayout.state = 'bolt11' - else if (_bip21uri != '') - rootLayout.state = 'bip21uri' - } - Config.preferredRequestType = rootLayout.state - } - } } RowLayout { diff --git a/electrum/gui/qml/components/controls/QRImage.qml b/electrum/gui/qml/components/controls/QRImage.qml index 875b116b3..f5baa72f1 100644 --- a/electrum/gui/qml/components/controls/QRImage.qml +++ b/electrum/gui/qml/components/controls/QRImage.qml @@ -7,6 +7,9 @@ Item { property bool render: true // init to false, then set true if render needs delay property var qrprops: QRIP.getDimensions(qrdata) + property bool enable_toggle_text: false // if true, clicking the QR code shows the encoded text + property bool is_in_text_state: false // internal state, if the above is enabled + width: r.width height: r.height @@ -19,6 +22,7 @@ Item { Image { source: qrdata && render ? 'image://qrgen/' + qrdata : '' + visible: !is_in_text_state Rectangle { visible: root.render && qrprops.valid @@ -44,4 +48,27 @@ Item { anchors.centerIn: parent } } + + Label { + visible: is_in_text_state + text: qrdata + wrapMode: Text.WrapAnywhere + elide: Text.ElideRight + anchors.centerIn: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: 'black' + width: r.width + height: r.height + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (enable_toggle_text) { + root.is_in_text_state = !root.is_in_text_state + } + } + } + } From 96fd339a523feb71f502f3bc0222ab0d8eb8c8bb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 12:46:30 +0200 Subject: [PATCH 0824/1143] qml: followup 22fa84a0, use fixed font, use camelcase for QML properties --- electrum/gui/qml/components/ReceiveDialog.qml | 6 ++-- .../gui/qml/components/controls/QRImage.qml | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 8839aba2a..3fa4ec432 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -85,7 +85,7 @@ ElDialog { QRImage { qrdata: _bolt11 render: _render_qr - enable_toggle_text: true + enableToggleText: true } } Component { @@ -93,7 +93,7 @@ ElDialog { QRImage { qrdata: _bip21uri render: _render_qr - enable_toggle_text: true + enableToggleText: true } } Component { @@ -101,7 +101,7 @@ ElDialog { QRImage { qrdata: _address render: _render_qr - enable_toggle_text: true + enableToggleText: true } } } diff --git a/electrum/gui/qml/components/controls/QRImage.qml b/electrum/gui/qml/components/controls/QRImage.qml index f5baa72f1..a28febb53 100644 --- a/electrum/gui/qml/components/controls/QRImage.qml +++ b/electrum/gui/qml/components/controls/QRImage.qml @@ -5,35 +5,35 @@ Item { id: root property string qrdata property bool render: true // init to false, then set true if render needs delay - property var qrprops: QRIP.getDimensions(qrdata) + property bool enableToggleText: false // if true, clicking the QR code shows the encoded text + property bool isTextState: false // internal state, if the above is enabled - property bool enable_toggle_text: false // if true, clicking the QR code shows the encoded text - property bool is_in_text_state: false // internal state, if the above is enabled + property var _qrprops: QRIP.getDimensions(qrdata) width: r.width height: r.height Rectangle { id: r - width: qrprops.modules * qrprops.box_size + width: _qrprops.modules * _qrprops.box_size height: width color: 'white' } Image { source: qrdata && render ? 'image://qrgen/' + qrdata : '' - visible: !is_in_text_state + visible: !isTextState Rectangle { - visible: root.render && qrprops.valid + visible: root.render && _qrprops.valid color: 'white' x: (parent.width - width) / 2 y: (parent.height - height) / 2 - width: qrprops.icon_modules * qrprops.box_size - height: qrprops.icon_modules * qrprops.box_size + width: _qrprops.icon_modules * _qrprops.box_size + height: _qrprops.icon_modules * _qrprops.box_size Image { - visible: qrprops.valid + visible: _qrprops.valid source: '../../../icons/electrum.png' x: 1 y: 1 @@ -43,14 +43,14 @@ Item { } } Label { - visible: !qrprops.valid + visible: !_qrprops.valid text: qsTr('Data too big for QR') anchors.centerIn: parent } } Label { - visible: is_in_text_state + visible: isTextState text: qrdata wrapMode: Text.WrapAnywhere elide: Text.ElideRight @@ -58,6 +58,10 @@ Item { horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter color: 'black' + font.family: FixedFont + font.pixelSize: text.length < 64 + ? constants.fontSizeXLarge + : constants.fontSizeMedium width: r.width height: r.height } @@ -65,8 +69,8 @@ Item { MouseArea { anchors.fill: parent onClicked: { - if (enable_toggle_text) { - root.is_in_text_state = !root.is_in_text_state + if (enableToggleText) { + root.isTextState = !root.isTextState } } } From 2b091b283a863c7517396cab9a0ef0c2c8688d6c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 13:04:09 +0200 Subject: [PATCH 0825/1143] qml: qebitcoin remove unused code, fix scoping, camelcase --- .../qml/components/wizard/WCCreateSeed.qml | 4 +- .../gui/qml/components/wizard/WCHaveSeed.qml | 6 +-- electrum/gui/qml/qebitcoin.py | 41 +++++-------------- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCCreateSeed.qml b/electrum/gui/qml/components/wizard/WCCreateSeed.qml index 096a016b4..905f4c49b 100644 --- a/electrum/gui/qml/components/wizard/WCCreateSeed.qml +++ b/electrum/gui/qml/components/wizard/WCCreateSeed.qml @@ -91,8 +91,8 @@ WizardComponent { Bitcoin { id: bitcoin onGeneratedSeedChanged: { - seedtext.text = generated_seed - setWarningText(generated_seed.split(' ').length) + seedtext.text = generatedSeed + setWarningText(generatedSeed.split(' ').length) } } } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 596b7ddce..f8bd211e6 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -21,13 +21,13 @@ WizardComponent { if (cosigner) { wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed'] = seedtext.text wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_variant'] = seed_variant_cb.currentValue - wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_type'] = bitcoin.seed_type + wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_type'] = bitcoin.seedType wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extend'] = extendcb.checked wizard_data['multisig_cosigner_data'][cosigner.toString()]['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' } else { wizard_data['seed'] = seedtext.text wizard_data['seed_variant'] = seed_variant_cb.currentValue - wizard_data['seed_type'] = bitcoin.seed_type + wizard_data['seed_type'] = bitcoin.seedType wizard_data['seed_extend'] = extendcb.checked wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' } @@ -222,7 +222,7 @@ WizardComponent { Bitcoin { id: bitcoin - onSeedTypeChanged: contentText.text = bitcoin.seed_type + onSeedTypeChanged: contentText.text = bitcoin.seedType onValidationMessageChanged: validationtext.text = validationMessage } diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index f651123a9..c0820293a 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -8,7 +8,7 @@ from electrum.bip32 import is_bip32_derivation, normalize_bip32_derivation, xpub_type from electrum.logging import get_logger from electrum.slip39 import decode_mnemonic, Slip39Error -from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop +from electrum.util import get_asyncio_loop from electrum.transaction import tx_from_any from electrum.mnemonic import Mnemonic, is_any_2fa_seed_type from electrum.old_mnemonic import wordlist as old_wordlist @@ -19,27 +19,24 @@ class QEBitcoin(QObject): _logger = get_logger(__name__) generatedSeedChanged = pyqtSignal() - generatedSeed = '' - seedTypeChanged = pyqtSignal() - seedType = '' - validationMessageChanged = pyqtSignal() - _validationMessage = '' - - _words = None def __init__(self, config, parent=None): super().__init__(parent) self.config = config + self._seed_type = '' + self._generated_seed = '' + self._validationMessage = '' + self._words = None @pyqtProperty('QString', notify=generatedSeedChanged) - def generated_seed(self): - return self.generatedSeed + def generatedSeed(self): + return self._generated_seed @pyqtProperty('QString', notify=seedTypeChanged) - def seed_type(self): - return self.seedType + def seedType(self): + return self._seed_type @pyqtProperty('QString', notify=validationMessageChanged) def validationMessage(self): @@ -58,7 +55,7 @@ def generateSeed(self, seed_type='segwit', language='en'): self._logger.debug('generating seed of type ' + str(seed_type)) async def co_gen_seed(seed_type, language): - self.generatedSeed = mnemonic.Mnemonic(language).make_seed(seed_type=seed_type) + self._generated_seed = mnemonic.Mnemonic(language).make_seed(seed_type=seed_type) self._logger.debug('seed generated') self.generatedSeedChanged.emit() @@ -101,7 +98,7 @@ def verifySeed(self, seed, seed_variant, wallet_type='standard'): elif wallet_type == 'multisig' and seed_type not in ['standard', 'segwit', 'bip39']: seed_valid = False - self.seedType = seed_type + self._seed_type = seed_type self.seedTypeChanged.emit() self._logger.debug('seed verified: ' + str(seed_valid)) @@ -137,22 +134,6 @@ def verifyMasterKey(self, key, wallet_type='standard'): def verifyDerivationPath(self, path): return is_bip32_derivation(path) - @pyqtSlot(str, result='QVariantMap') - def parse_uri(self, uri: str) -> dict: - try: - return parse_URI(uri) - except InvalidBitcoinURI as e: - return { 'error': str(e) } - - @pyqtSlot(str, QEAmount, str, int, int, result=str) - def create_bip21_uri(self, address, satoshis, message, timestamp, expiry): - extra_params = {} - if expiry: - extra_params['time'] = str(timestamp) - extra_params['exp'] = str(expiry) - - return create_bip21_uri(address, satoshis.satsInt, message, extra_query_params=extra_params) - @pyqtSlot(str, result=bool) def isRawTx(self, rawtx): try: From f0bbbe9955d2ccbfb410ce18de4ce086e295c8a0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 13:22:34 +0200 Subject: [PATCH 0826/1143] qml: consistency camelcase public slots listmodels --- electrum/gui/qml/components/Addresses.qml | 4 +- .../gui/qml/components/ChannelDetails.qml | 4 +- electrum/gui/qml/components/Invoices.qml | 4 +- .../gui/qml/components/OpenChannelDialog.qml | 2 +- .../controls/HistoryItemDelegate.qml | 4 +- electrum/gui/qml/qeaddresslistmodel.py | 6 +-- electrum/gui/qml/qechannellistmodel.py | 10 ++-- electrum/gui/qml/qeinvoicelistmodel.py | 4 +- electrum/gui/qml/qeserverlistmodel.py | 10 ++-- electrum/gui/qml/qetransactionlistmodel.py | 46 +++++++++---------- electrum/gui/qml/qewallet.py | 12 ++--- 11 files changed, 53 insertions(+), 53 deletions(-) diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index 935b5710d..91d7e20be 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -36,7 +36,7 @@ Pane { var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {'address': model.address}) page.addressDetailsChanged.connect(function() { // update listmodel when details change - listview.model.update_address(model.address) + listview.model.updateAddress(model.address) }) } } @@ -74,6 +74,6 @@ Pane { } Component.onCompleted: { - Daemon.currentWallet.addressModel.init_model() + Daemon.currentWallet.addressModel.initModel() } } diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index e614f0246..9c386ef72 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -290,8 +290,8 @@ Pane { dialog.accepted.connect(function() { channeldetails.deleteChannel() app.stack.pop() - Daemon.currentWallet.historyModel.init_model(true) // needed here? - Daemon.currentWallet.channelModel.remove_channel(channelid) + Daemon.currentWallet.historyModel.initModel(true) // needed here? + Daemon.currentWallet.channelModel.removeChannel(channelid) }) dialog.open() } diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 9d7bc6086..d947aa84e 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -52,7 +52,7 @@ Pane { onClicked: { var dialog = app.stack.getRoot().openInvoice(model.key) dialog.invoiceAmountChanged.connect(function () { - Daemon.currentWallet.invoiceModel.init_model() + Daemon.currentWallet.invoiceModel.initModel() }) listview.currentIndex = -1 } @@ -106,7 +106,7 @@ Pane { onClicked: { var dialog = app.stack.getRoot().openInvoice(listview.currentItem.getKey()) dialog.invoiceAmountChanged.connect(function () { - Daemon.currentWallet.invoiceModel.init_model() + Daemon.currentWallet.invoiceModel.initModel() }) } } diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 5ef5a2fcc..8c3e13816 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -252,7 +252,7 @@ ElDialog { + qsTr('This channel will be usable after %1 confirmations').arg(min_depth) if (!tx_complete) { message = message + '\n\n' + qsTr('Please sign and broadcast the funding transaction.') - channelopener.wallet.historyModel.init_model(true) // local tx doesn't trigger model update + channelopener.wallet.historyModel.initModel(true) // local tx doesn't trigger model update } app.channelOpenProgressDialog.state = 'success' app.channelOpenProgressDialog.info = message diff --git a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml index 28e26cfcc..3eec740b3 100644 --- a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml +++ b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml @@ -27,13 +27,13 @@ Item { var page = app.stack.push(Qt.resolvedUrl('../LightningPaymentDetails.qml'), {'key': model.key}) page.detailsChanged.connect(function() { // update listmodel when details change - visualModel.model.update_tx_label(model.key, page.label) + visualModel.model.updateTxLabel(model.key, page.label) }) } else { var page = app.stack.push(Qt.resolvedUrl('../TxDetails.qml'), {'txid': model.key}) page.detailsChanged.connect(function() { // update listmodel when details change - visualModel.model.update_tx_label(model.key, page.label) + visualModel.model.updateTxLabel(model.key, page.label) }) } } diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py index 29c2cb900..5ecf18975 100644 --- a/electrum/gui/qml/qeaddresslistmodel.py +++ b/electrum/gui/qml/qeaddresslistmodel.py @@ -25,7 +25,7 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): super().__init__(parent) self.wallet = wallet self.setDirty() - self.init_model() + self.initModel() def rowCount(self, index): return len(self.receive_addresses) + len(self.change_addresses) @@ -68,7 +68,7 @@ def setDirty(self): # initial model data @pyqtSlot() - def init_model(self): + def initModel(self): if not self._dirty: return @@ -97,7 +97,7 @@ def insert_row(atype, alist, address, iaddr): self._dirty = False @pyqtSlot(str) - def update_address(self, address): + def updateAddress(self, address): for i, a in enumerate(itertools.chain(self.receive_addresses, self.change_addresses)): if a['address'] == address: self.do_update(i,a) diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index 52582bf81..a58019f8f 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -27,7 +27,7 @@ class QEChannelListModel(QAbstractListModel, QtEventListener): def __init__(self, wallet, parent=None): super().__init__(parent) self.wallet = wallet - self.init_model() + self.initModel() # To avoid leaking references to "self" that prevent the # window from being GC-ed when closed, callbacks should be @@ -44,7 +44,7 @@ def on_event_channel(self, wallet, channel): @qt_event_listener def on_event_channels_updated(self, wallet): if wallet == self.wallet: - self.init_model() + self.initModel() def on_destroy(self): self.unregister_callbacks() @@ -108,7 +108,7 @@ def numOpenChannels(self): return sum([1 if x['state_code'] == ChannelState.OPEN else 0 for x in self.channels]) @pyqtSlot() - def init_model(self): + def initModel(self): self._logger.debug('init_model') if not self.wallet.lnworker: self._logger.warning('lnworker should be defined') @@ -149,7 +149,7 @@ def do_update(self, modelindex, channel): self.numOpenChannelsChanged.emit() @pyqtSlot(str) - def new_channel(self, cid): + def newChannel(self, cid): self._logger.debug('new channel with cid %s' % cid) lnchannels = self.wallet.lnworker.channels for channel in lnchannels.values(): @@ -163,7 +163,7 @@ def new_channel(self, cid): return @pyqtSlot(str) - def remove_channel(self, cid): + def removeChannel(self, cid): self._logger.debug('remove channel with cid %s' % cid) for i, channel in enumerate(self.channels): if cid == channel['cid']: diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 551b17d0c..ca017836c 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -35,7 +35,7 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None): self._timer.timeout.connect(self.updateStatusStrings) try: - self.init_model() + self.initModel() except Exception as e: self._logger.error(f'{repr(e)}') raise e @@ -63,7 +63,7 @@ def clear(self): self.endResetModel() @pyqtSlot() - def init_model(self): + def initModel(self): invoices = [] for invoice in self.get_invoice_list(): item = self.invoice_to_model(invoice) diff --git a/electrum/gui/qml/qeserverlistmodel.py b/electrum/gui/qml/qeserverlistmodel.py index 97a635b85..b5733d0bd 100644 --- a/electrum/gui/qml/qeserverlistmodel.py +++ b/electrum/gui/qml/qeserverlistmodel.py @@ -26,24 +26,24 @@ def __init__(self, network, parent=None): self._chaintips = 0 self.network = network - self.init_model() + self.initModel() self.register_callbacks() self.destroyed.connect(lambda: self.unregister_callbacks()) @qt_event_listener def on_event_network_updated(self): self._logger.info(f'network updated') - self.init_model() + self.initModel() @qt_event_listener def on_event_blockchain_updated(self): self._logger.info(f'blockchain updated') - self.init_model() + self.initModel() @qt_event_listener def on_event_default_server_changed(self): self._logger.info(f'default server changed') - self.init_model() + self.initModel() def rowCount(self, index): return len(self.servers) @@ -81,7 +81,7 @@ def get_chains(self): return chains @pyqtSlot() - def init_model(self): + def initModel(self): self.clear() servers = [] diff --git a/electrum/gui/qml/qetransactionlistmodel.py b/electrum/gui/qml/qetransactionlistmodel.py index 24af49ede..9ae055970 100644 --- a/electrum/gui/qml/qetransactionlistmodel.py +++ b/electrum/gui/qml/qetransactionlistmodel.py @@ -36,10 +36,10 @@ def __init__(self, wallet: 'Abstract_Wallet', parent=None, *, onchain_domain=Non self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) - self.requestRefresh.connect(lambda: self.init_model()) + self.requestRefresh.connect(lambda: self.initModel()) self.setDirty() - self.init_model() + self.initModel() def on_destroy(self): self.unregister_callbacks() @@ -60,6 +60,25 @@ def on_event_adb_set_future_tx(self, adb, txid): self._update_future_txitem(i) return + @qt_event_listener + def on_event_fee_histogram(self, histogram): + self._logger.debug(f'fee histogram updated') + for i, tx_item in enumerate(self.tx_history): + if 'height' not in tx_item: # filter to on-chain + continue + if tx_item['confirmations'] > 0: # filter out already mined + continue + txid = tx_item['txid'] + tx = self.wallet.db.get_transaction(txid) + if not tx: + continue + txinfo = self.wallet.get_tx_info(tx) + status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status) + tx_item['date'] = status_str + index = self.index(i, 0) + roles = [self._ROLE_RMAP['date']] + self.dataChanged.emit(index, index, roles) + def rowCount(self, index): return len(self.tx_history) @@ -180,7 +199,7 @@ def _tx_mined_info_from_tx_item(tx_item: Dict[str, Any]) -> TxMinedInfo: # initial model data @pyqtSlot() @pyqtSlot(bool) - def init_model(self, force: bool = False): + def initModel(self, force: bool = False): # only (re)construct if dirty or forced if not self._dirty and not force: return @@ -237,7 +256,7 @@ def _update_future_txitem(self, tx_item_idx: int): self.dataChanged.emit(index, index, roles) @pyqtSlot(str, str) - def update_tx_label(self, key, label): + def updateTxLabel(self, key, label): for i, tx in enumerate(self.tx_history): if tx['key'] == key: tx['label'] = label @@ -257,22 +276,3 @@ def updateBlockchainHeight(self, height): self.dataChanged.emit(index, index, roles) elif tx_item['height'] in (TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL): self._update_future_txitem(i) - - @qt_event_listener - def on_event_fee_histogram(self, histogram): - self._logger.debug(f'fee histogram updated') - for i, tx_item in enumerate(self.tx_history): - if 'height' not in tx_item: # filter to on-chain - continue - if tx_item['confirmations'] > 0: # filter out already mined - continue - txid = tx_item['txid'] - tx = self.wallet.db.get_transaction(txid) - if not tx: - continue - txinfo = self.wallet.get_tx_info(tx) - status, status_str = self.wallet.get_tx_status(txid, txinfo.tx_mined_status) - tx_item['date'] = status_str - index = self.index(i, 0) - roles = [self._ROLE_RMAP['date']] - self.dataChanged.emit(index, index, roles) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 6eb7cb5ed..f26e7b85b 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -168,7 +168,7 @@ def on_event_request_status(self, wallet, key, status): # TODO: only update if it was paid over lightning, # and even then, we can probably just add the payment instead # of recreating the whole history (expensive) - self.historyModel.init_model(True) + self.historyModel.initModel(True) @event_listener def on_event_invoice_status(self, wallet, key, status): @@ -196,7 +196,7 @@ def on_event_removed_transaction(self, wallet, tx): if wallet == self.wallet: self._logger.info(f'removed transaction {tx.txid()}') self.addressModel.setDirty() - self.historyModel.init_model(True) #setDirty() + self.historyModel.initModel(True) #setDirty() self.balanceChanged.emit() @qt_event_listener @@ -206,7 +206,7 @@ def on_event_wallet_updated(self, wallet): self.balanceChanged.emit() self.synchronizing = not wallet.is_up_to_date() if not self.synchronizing: - self.historyModel.init_model() # refresh if dirty + self.historyModel.initModel() # refresh if dirty @event_listener def on_event_channel(self, wallet, channel): @@ -224,7 +224,7 @@ def on_event_channels_updated(self, wallet): def on_event_payment_succeeded(self, wallet, key): if wallet == self.wallet: self.paymentSucceeded.emit(key) - self.historyModel.init_model(True) # TODO: be less dramatic + self.historyModel.initModel(True) # TODO: be less dramatic @event_listener def on_event_payment_failed(self, wallet, key, reason): @@ -527,7 +527,7 @@ def do_sign(self, tx, broadcast): self.broadcast(tx) else: # not broadcasted, so refresh history here - self.historyModel.init_model(True) + self.historyModel.initModel(True) return True @@ -589,7 +589,7 @@ def save_tx(self, tx: 'PartialTransaction'): return self.wallet.save_db() self.saveTxSuccess.emit(tx.txid()) - self.historyModel.init_model(True) + self.historyModel.initModel(True) return True except AddTransactionException as e: self.saveTxError.emit(tx.txid(), 'error', str(e)) From 61179ede8c9254a0e03ecb01db7a97805d98d8be Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 13:33:15 +0200 Subject: [PATCH 0827/1143] qml: consistency camelcase qewallet --- electrum/gui/qml/components/Invoices.qml | 2 +- .../gui/qml/components/ReceiveRequests.qml | 2 +- electrum/gui/qml/components/WalletDetails.qml | 2 +- electrum/gui/qml/components/WalletMainView.qml | 2 +- electrum/gui/qml/components/main.qml | 4 ++-- electrum/gui/qml/qewallet.py | 18 +++++------------- 6 files changed, 11 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index d947aa84e..473fe8f2b 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -94,7 +94,7 @@ Pane { icon.source: '../../icons/delete.png' visible: listview.currentIndex >= 0 onClicked: { - Daemon.currentWallet.delete_invoice(listview.currentItem.getKey()) + Daemon.currentWallet.deleteInvoice(listview.currentItem.getKey()) } } FlatButton { diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index 9c5e0f3d1..78f3bd4b3 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -93,7 +93,7 @@ Pane { icon.source: '../../icons/delete.png' visible: listview.currentIndex >= 0 onClicked: { - Daemon.currentWallet.delete_request(listview.currentItem.getKey()) + Daemon.currentWallet.deleteRequest(listview.currentItem.getKey()) } } FlatButton { diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 6d32a06e0..5ec636eda 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -493,7 +493,7 @@ Pane { infotext: qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') }) dialog.accepted.connect(function() { - Daemon.currentWallet.set_password(dialog.password) + Daemon.currentWallet.setPassword(dialog.password) }) dialog.open() } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 6b219083c..9a2c865e6 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -193,7 +193,7 @@ Item { } onPressAndHold: { Config.userKnowsPressAndHold = true - Daemon.currentWallet.delete_expired_requests() + Daemon.currentWallet.deleteExpiredRequests() app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml')) AppController.haptic() } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 66353a24d..f57aae203 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -541,13 +541,13 @@ ApplicationWindow function handleAuthRequired(qtobject, method, authMessage) { console.log('auth using method ' + method) if (method == 'wallet') { - if (Daemon.currentWallet.verify_password('')) { + if (Daemon.currentWallet.verifyPassword('')) { // wallet has no password qtobject.authProceed() } else { var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')}) dialog.accepted.connect(function() { - if (Daemon.currentWallet.verify_password(dialog.password)) { + if (Daemon.currentWallet.verifyPassword(dialog.password)) { qtobject.authProceed() } else { qtobject.authCancel() diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index f26e7b85b..abff997e1 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -614,7 +614,7 @@ def pay_thread(): threading.Thread(target=pay_thread, daemon=True).start() @pyqtSlot() - def delete_expired_requests(self): + def deleteExpiredRequests(self): keys = self.wallet.delete_expired_requests() for key in keys: self.requestModel.delete_invoice(key) @@ -654,27 +654,19 @@ def createRequest(self, amount: QEAmount, message: str, expiration: int, lightni self.requestCreateSuccess.emit(key) @pyqtSlot(str) - def delete_request(self, key: str): + def deleteRequest(self, key: str): self._logger.debug('delete req %s' % key) self.wallet.delete_request(key) self.requestModel.delete_invoice(key) - @pyqtSlot(str, result='QVariant') - def get_request(self, key: str): - return self.requestModel.get_model_invoice(key) - @pyqtSlot(str) - def delete_invoice(self, key: str): + def deleteInvoice(self, key: str): self._logger.debug('delete inv %s' % key) self.wallet.delete_invoice(key) self.invoiceModel.delete_invoice(key) - @pyqtSlot(str, result='QVariant') - def get_invoice(self, key: str): - return self.invoiceModel.get_model_invoice(key) - @pyqtSlot(str, result=bool) - def verify_password(self, password): + def verifyPassword(self, password): try: self.wallet.storage.check_password(password) return True @@ -682,7 +674,7 @@ def verify_password(self, password): return False @pyqtSlot(str) - def set_password(self, password): + def setPassword(self, password): if password == '': password = None From 264540e12b743f8a71e9a167c619929e13d0f4ef Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 13:40:16 +0200 Subject: [PATCH 0828/1143] qml: consistency camelcase public slots qedaemon, qeinvoice, qewizard --- electrum/gui/qml/components/InvoiceDialog.qml | 6 +++--- electrum/gui/qml/components/NewWalletWizard.qml | 2 +- electrum/gui/qml/components/OpenWalletDialog.qml | 2 +- electrum/gui/qml/components/ServerConnectWizard.qml | 2 +- electrum/gui/qml/components/WalletMainView.qml | 4 ++-- electrum/gui/qml/components/Wallets.qml | 4 ++-- electrum/gui/qml/components/main.qml | 6 +++--- electrum/gui/qml/qedaemon.py | 2 +- electrum/gui/qml/qeinvoice.py | 6 +++--- electrum/gui/qml/qewizard.py | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index cb86a2c8d..d46212573 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -397,7 +397,7 @@ ElDialog { if (invoice.amount.isEmpty) { invoice.amountOverride = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text) } - invoice.save_invoice() + invoice.saveInvoice() app.stack.push(Qt.resolvedUrl('Invoices.qml')) dialog.close() } @@ -414,7 +414,7 @@ ElDialog { } if (!invoice.isSaved) { // save invoice if newly parsed - invoice.save_invoice() + invoice.saveInvoice() } doPay() // only signal here } @@ -432,7 +432,7 @@ ElDialog { if (payImmediately) { if (invoice.canPay) { if (!invoice.isSaved) { - invoice.save_invoice() + invoice.saveInvoice() } doPay() } diff --git a/electrum/gui/qml/components/NewWalletWizard.qml b/electrum/gui/qml/components/NewWalletWizard.qml index a9789b8f7..efb8e5867 100644 --- a/electrum/gui/qml/components/NewWalletWizard.qml +++ b/electrum/gui/qml/components/NewWalletWizard.qml @@ -18,7 +18,7 @@ Wizard { wiz: Daemon.newWalletWizard Component.onCompleted: { - var view = wiz.start_wizard() + var view = wiz.startWizard() _loadNextComponent(view) } diff --git a/electrum/gui/qml/components/OpenWalletDialog.qml b/electrum/gui/qml/components/OpenWalletDialog.qml index eb125e998..b9922c42c 100644 --- a/electrum/gui/qml/components/OpenWalletDialog.qml +++ b/electrum/gui/qml/components/OpenWalletDialog.qml @@ -120,7 +120,7 @@ ElDialog { } onReadyChanged: { if (ready) { - Daemon.load_wallet(openwalletdialog.path, password.text) + Daemon.loadWallet(openwalletdialog.path, password.text) openwalletdialog.close() } } diff --git a/electrum/gui/qml/components/ServerConnectWizard.qml b/electrum/gui/qml/components/ServerConnectWizard.qml index cfffa0f52..725388f34 100644 --- a/electrum/gui/qml/components/ServerConnectWizard.qml +++ b/electrum/gui/qml/components/ServerConnectWizard.qml @@ -28,7 +28,7 @@ Wizard { } Component.onCompleted: { - var view = wiz.start_wizard() + var view = wiz.startWizard() _loadNextComponent(view) } } diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 9a2c865e6..f10008682 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -164,7 +164,7 @@ Item { newww.walletCreated.connect(function() { Daemon.availableWallets.reload() // and load the new wallet - Daemon.load_wallet(newww.path, newww.wizard_data['password']) + Daemon.loadWallet(newww.path, newww.wizard_data['password']) }) newww.open() } @@ -341,7 +341,7 @@ Item { dialog.open() } else if (invoice.invoiceType == Invoice.LightningInvoice) { console.log('About to pay lightning invoice') - invoice.pay_lightning_invoice() + invoice.payLightningInvoice() } } diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index fb25da666..2ac0c7763 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -21,7 +21,7 @@ Pane { dialog.walletCreated.connect(function() { Daemon.availableWallets.reload() // and load the new wallet - Daemon.load_wallet(dialog.path, dialog.wizard_data['password']) + Daemon.loadWallet(dialog.path, dialog.wizard_data['password']) }) } @@ -59,7 +59,7 @@ Pane { onClicked: { if (!Daemon.currentWallet || Daemon.currentWallet.name != model.name) - Daemon.load_wallet(model.path) + Daemon.loadWallet(model.path) else app.stack.pop() } diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index f57aae203..acb7ac36a 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -420,7 +420,7 @@ ApplicationWindow newww.walletCreated.connect(function() { Daemon.availableWallets.reload() // and load the new wallet - Daemon.load_wallet(newww.path, newww.wizard_data['password']) + Daemon.loadWallet(newww.path, newww.wizard_data['password']) }) newww.open() }) @@ -428,13 +428,13 @@ ApplicationWindow } else { Daemon.startNetwork() if (Daemon.availableWallets.rowCount() > 0) { - Daemon.load_wallet() + Daemon.loadWallet() } else { var newww = app.newWalletWizard.createObject(app) newww.walletCreated.connect(function() { Daemon.availableWallets.reload() // and load the new wallet - Daemon.load_wallet(newww.path, newww.wizard_data['password']) + Daemon.loadWallet(newww.path, newww.wizard_data['password']) }) newww.open() } diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 10645328d..cf6ba212b 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -158,7 +158,7 @@ def onWalletOpenProblem(self, error): @pyqtSlot() @pyqtSlot(str) @pyqtSlot(str, str) - def load_wallet(self, path=None, password=None): + def loadWallet(self, path=None, password=None): if path is None: self._path = self.daemon.config.get('wallet_path') # command line -w option if self._path is None: diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 0509c3fa4..dda1759da 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -370,12 +370,12 @@ def determine_can_pay(self): self.canPay = True @pyqtSlot() - def pay_lightning_invoice(self): + def payLightningInvoice(self): if not self.canPay: raise Exception('can not pay invoice, canPay is false') if self.invoiceType != QEInvoice.Type.LightningInvoice: - raise Exception('pay_lightning_invoice can only pay lightning invoices') + raise Exception('payLightningInvoice can only pay lightning invoices') if self.amount.isEmpty: if self.amountOverride.isEmpty: @@ -651,7 +651,7 @@ def on_lnurl_invoice(self, orig_amount, invoice): self.recipient = invoice['pr'] @pyqtSlot() - def save_invoice(self): + def saveInvoice(self): if not self._effectiveInvoice: return if self.isSaved: diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 50da4b0dc..a061099c9 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -13,7 +13,7 @@ def __init__(self, parent = None): QObject.__init__(self, parent) @pyqtSlot(result=str) - def start_wizard(self): + def startWizard(self): self.start() return self._current.view From e26d49f11e1e9bb0f33e165c5ce1a6adbddb7101 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 13:46:30 +0200 Subject: [PATCH 0829/1143] qml: consistency camelcase public slots qetxdetails, qelnpaymentdetails, qechannelopener, qeaddressdetails --- electrum/gui/qml/components/AddressDetails.qml | 2 +- electrum/gui/qml/components/LightningPaymentDetails.qml | 2 +- electrum/gui/qml/components/OpenChannelDialog.qml | 8 ++++---- electrum/gui/qml/components/TxDetails.qml | 2 +- electrum/gui/qml/qeaddressdetails.py | 2 +- electrum/gui/qml/qechannelopener.py | 4 ++-- electrum/gui/qml/qelnpaymentdetails.py | 2 +- electrum/gui/qml/qetxdetails.py | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index 6cbe6e848..eb5d815fa 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -123,7 +123,7 @@ Pane { icon.color: 'transparent' onClicked: { labelContent.editmode = false - addressdetails.set_label(labelEdit.text) + addressdetails.setLabel(labelEdit.text) } } ToolButton { diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index 097643947..5d6d03e78 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -120,7 +120,7 @@ Pane { icon.color: 'transparent' onClicked: { labelContent.editmode = false - lnpaymentdetails.set_label(labelEdit.text) + lnpaymentdetails.setLabel(labelEdit.text) } } ToolButton { diff --git a/electrum/gui/qml/components/OpenChannelDialog.qml b/electrum/gui/qml/components/OpenChannelDialog.qml index 8c3e13816..63b293119 100644 --- a/electrum/gui/qml/components/OpenChannelDialog.qml +++ b/electrum/gui/qml/components/OpenChannelDialog.qml @@ -95,7 +95,7 @@ ElDialog { icon.height: constants.iconSizeMedium icon.width: constants.iconSizeMedium onClicked: { - if (channelopener.validate_connect_str(AppController.clipboardToText())) { + if (channelopener.validateConnectString(AppController.clipboardToText())) { channelopener.connectStr = AppController.clipboardToText() node.text = channelopener.connectStr } @@ -111,7 +111,7 @@ ElDialog { hint: qsTr('Scan a channel connect string') }) dialog.onFound.connect(function() { - if (channelopener.validate_connect_str(dialog.scanData)) { + if (channelopener.validateConnectString(dialog.scanData)) { channelopener.connectStr = dialog.scanData node.text = channelopener.connectStr } @@ -196,7 +196,7 @@ ElDialog { text: qsTr('Open Channel') icon.source: '../../icons/confirmed.png' enabled: channelopener.valid - onClicked: channelopener.open_channel() + onClicked: channelopener.openChannel() } } @@ -225,7 +225,7 @@ ElDialog { var dialog = app.messageDialog.createObject(app, { 'text': message, 'yesno': true }) dialog.open() dialog.accepted.connect(function() { - channelopener.open_channel(true) + channelopener.openChannel(true) }) } onFinalizerChanged: { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 624f5aaab..1625d1f6f 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -212,7 +212,7 @@ Pane { icon.color: 'transparent' onClicked: { labelContent.editmode = false - txdetails.set_label(labelEdit.text) + txdetails.setLabel(labelEdit.text) } } ToolButton { diff --git a/electrum/gui/qml/qeaddressdetails.py b/electrum/gui/qml/qeaddressdetails.py index 2d910c160..0b23dcbbf 100644 --- a/electrum/gui/qml/qeaddressdetails.py +++ b/electrum/gui/qml/qeaddressdetails.py @@ -94,7 +94,7 @@ def freeze(self, freeze: bool): self._wallet.balanceChanged.emit() @pyqtSlot(str) - def set_label(self, label: str): + def setLabel(self, label: str): if label != self._label: self._wallet.wallet.set_label(self._address, label) self._label = label diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 16433258d..08d855764 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -132,7 +132,7 @@ def validate(self): self.validChanged.emit() @pyqtSlot(str, result=bool) - def validate_connect_str(self, connect_str): + def validateConnectString(self, connect_str): try: node_id, rest = extract_nodeid(connect_str) except ConnStringFormatError as e: @@ -143,7 +143,7 @@ def validate_connect_str(self, connect_str): # FIXME "max" button in amount_dialog should enforce LN_MAX_FUNDING_SAT @pyqtSlot() @pyqtSlot(bool) - def open_channel(self, confirm_backup_conflict=False): + def openChannel(self, confirm_backup_conflict=False): if not self.valid: return diff --git a/electrum/gui/qml/qelnpaymentdetails.py b/electrum/gui/qml/qelnpaymentdetails.py index 24250252c..d01988c54 100644 --- a/electrum/gui/qml/qelnpaymentdetails.py +++ b/electrum/gui/qml/qelnpaymentdetails.py @@ -50,7 +50,7 @@ def label(self): return self._label @pyqtSlot(str) - def set_label(self, label: str): + def setLabel(self, label: str): if label != self._label: self._wallet.wallet.set_label(self._key, label) self._label = label diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index e1af555aa..8ff2cc6b2 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -126,7 +126,7 @@ def label(self): return self._label @pyqtSlot(str) - def set_label(self, label: str): + def setLabel(self, label: str): if label != self._label: self._wallet.wallet.set_label(self._txid, label) self._label = label From a23457f48dd4ed1904356a850b4f13560c5e6bb3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 14:15:13 +0200 Subject: [PATCH 0830/1143] qml: consistency camelcase pyqtProperties --- electrum/gui/qml/components/BalanceDetails.qml | 2 +- electrum/gui/qml/components/ChannelDetails.qml | 4 ++-- electrum/gui/qml/components/CloseChannelDialog.qml | 4 ++-- electrum/gui/qml/components/InvoiceDialog.qml | 2 +- .../gui/qml/components/LightningPaymentDetails.qml | 4 ++-- electrum/gui/qml/components/NetworkOverview.qml | 6 +++--- electrum/gui/qml/components/SwapDialog.qml | 2 +- .../gui/qml/components/controls/BalanceSummary.qml | 8 ++++---- .../controls/OnchainNetworkStatusIndicator.qml | 2 +- electrum/gui/qml/qechanneldetails.py | 4 ++-- electrum/gui/qml/qeinvoice.py | 2 +- electrum/gui/qml/qelnpaymentdetails.py | 2 +- electrum/gui/qml/qenetwork.py | 6 +++--- electrum/gui/qml/qeswaphelper.py | 14 +++++++------- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/electrum/gui/qml/components/BalanceDetails.qml b/electrum/gui/qml/components/BalanceDetails.qml index 3071a1c58..accd6d7c6 100644 --- a/electrum/gui/qml/components/BalanceDetails.qml +++ b/electrum/gui/qml/components/BalanceDetails.qml @@ -38,7 +38,7 @@ Pane { InfoTextArea { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: Daemon.currentWallet.synchronizing || !Network.is_connected + visible: Daemon.currentWallet.synchronizing || !Network.isConnected text: Daemon.currentWallet.synchronizing ? qsTr('Your wallet is not synchronized. The displayed balance may be inaccurate.') : qsTr('Your wallet is not connected to an Electrum server. The displayed balance may be outdated.') diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 9c386ef72..28b00948e 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -62,7 +62,7 @@ Pane { Label { Layout.fillWidth: true - text: channeldetails.short_cid + text: channeldetails.shortCid } Label { @@ -254,7 +254,7 @@ Pane { text: qsTr('Backup') onClicked: { var dialog = app.genericShareDialog.createObject(root, { - title: qsTr('Channel Backup for %1').arg(channeldetails.short_cid), + title: qsTr('Channel Backup for %1').arg(channeldetails.shortCid), text_qr: channeldetails.channelBackup(), text_help: channeldetails.channelBackupHelpText(), iconSource: Qt.resolvedUrl('../../icons/file.png') diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index a5ac0d707..529ce1c0e 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -63,7 +63,7 @@ ElDialog { } Label { - text: channeldetails.short_cid + text: channeldetails.shortCid } Label { @@ -92,7 +92,7 @@ ElDialog { Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - text: channeldetails.message_force_close + text: channeldetails.messageForceClose } Label { diff --git a/electrum/gui/qml/components/InvoiceDialog.qml b/electrum/gui/qml/components/InvoiceDialog.qml index d46212573..eb9ac48bd 100644 --- a/electrum/gui/qml/components/InvoiceDialog.qml +++ b/electrum/gui/qml/components/InvoiceDialog.qml @@ -52,7 +52,7 @@ ElDialog { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge visible: text - text: invoice.userinfo ? invoice.userinfo : invoice.status_str + text: invoice.userinfo ? invoice.userinfo : invoice.statusString iconStyle: invoice.status == Invoice.Failed || invoice.status == Invoice.Unknown ? InfoTextArea.IconStyle.Warn : invoice.status == Invoice.Expired diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index 5d6d03e78..fa490487e 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -146,7 +146,7 @@ Pane { RowLayout { width: parent.width Label { - text: lnpaymentdetails.payment_hash + text: lnpaymentdetails.paymentHash font.pixelSize: constants.fontSizeLarge font.family: FixedFont Layout.fillWidth: true @@ -157,7 +157,7 @@ Pane { icon.color: 'transparent' onClicked: { var dialog = app.genericShareDialog.createObject(root, - { title: qsTr('Payment hash'), text: lnpaymentdetails.payment_hash } + { title: qsTr('Payment hash'), text: lnpaymentdetails.paymentHash } ) dialog.open() } diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 41fdc0984..1e9d69e9e 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -69,11 +69,11 @@ Pane { Label { text: qsTr('Server Height:'); color: Material.accentColor - visible: Network.server_height != Network.height + visible: Network.serverHeight != Network.height } Label { - text: Network.server_height + " (lagging)" - visible: Network.server_height != Network.height + text: Network.serverHeight + " (lagging)" + visible: Network.serverHeight != Network.height } Heading { Layout.columnSpan: 2 diff --git a/electrum/gui/qml/components/SwapDialog.qml b/electrum/gui/qml/components/SwapDialog.qml index de0aaeb67..b005b28ca 100644 --- a/electrum/gui/qml/components/SwapDialog.qml +++ b/electrum/gui/qml/components/SwapDialog.qml @@ -124,7 +124,7 @@ ElDialog { Layout.preferredWidth: 1 Layout.fillWidth: true Label { - text: Config.formatSats(swaphelper.server_miningfee) + text: Config.formatSats(swaphelper.serverMiningfee) font.family: FixedFont } Label { diff --git a/electrum/gui/qml/components/controls/BalanceSummary.qml b/electrum/gui/qml/components/controls/BalanceSummary.qml index 15e3ace0d..ec266c896 100644 --- a/electrum/gui/qml/components/controls/BalanceSummary.qml +++ b/electrum/gui/qml/components/controls/BalanceSummary.qml @@ -31,7 +31,7 @@ Item { GridLayout { id: balanceLayout columns: 3 - opacity: Daemon.currentWallet.synchronizing || !Network.is_connected ? 0 : 1 + opacity: Daemon.currentWallet.synchronizing || !Network.isConnected ? 0 : 1 Label { font.pixelSize: constants.fontSizeXLarge @@ -129,7 +129,7 @@ Item { } Label { - opacity: Daemon.currentWallet.synchronizing && Network.is_connected ? 1 : 0 + opacity: Daemon.currentWallet.synchronizing && Network.isConnected ? 1 : 0 anchors.centerIn: balancePane text: Daemon.currentWallet.synchronizingProgress color: Material.accentColor @@ -137,9 +137,9 @@ Item { } Label { - opacity: !Network.is_connected ? 1 : 0 + opacity: !Network.isConnected ? 1 : 0 anchors.centerIn: balancePane - text: Network.server_status + text: Network.serverStatus color: Material.accentColor font.pixelSize: constants.fontSizeLarge } diff --git a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml index 54242040b..7a575f4f4 100644 --- a/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml +++ b/electrum/gui/qml/components/controls/OnchainNetworkStatusIndicator.qml @@ -6,7 +6,7 @@ Image { sourceSize.width: constants.iconSizeMedium sourceSize.height: constants.iconSizeMedium - property bool connected: Network.is_connected + property bool connected: Network.isConnected property bool lagging: connected && Network.isLagging property bool fork: connected && Network.chaintips > 1 property bool syncing: connected && Daemon.currentWallet && Daemon.currentWallet.synchronizing diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 4706f6a47..8340e2619 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -92,7 +92,7 @@ def pubkey(self): return self._channel.node_id.hex() @pyqtProperty(str, notify=channelChanged) - def short_cid(self): + def shortCid(self): return self._channel.short_id_for_GUI() @pyqtProperty(str, notify=channelChanged) @@ -171,7 +171,7 @@ def canDelete(self): return self._channel.can_be_deleted() @pyqtProperty(str, notify=channelChanged) - def message_force_close(self, notify=channelChanged): + def messageForceClose(self, notify=channelChanged): return _(messages.MSG_REQUEST_FORCE_CLOSE).strip() @pyqtProperty(bool, notify=channelChanged) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index dda1759da..2118c8aef 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -166,7 +166,7 @@ def status(self): return self._wallet.wallet.get_invoice_status(self._effectiveInvoice) @pyqtProperty(str, notify=statusChanged) - def status_str(self): + def statusString(self): if not self._effectiveInvoice: return '' status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice) diff --git a/electrum/gui/qml/qelnpaymentdetails.py b/electrum/gui/qml/qelnpaymentdetails.py index d01988c54..3ea29eb50 100644 --- a/electrum/gui/qml/qelnpaymentdetails.py +++ b/electrum/gui/qml/qelnpaymentdetails.py @@ -65,7 +65,7 @@ def date(self): return self._date @pyqtProperty(str, notify=detailsChanged) - def payment_hash(self): + def paymentHash(self): return self._phash @pyqtProperty(str, notify=detailsChanged) diff --git a/electrum/gui/qml/qenetwork.py b/electrum/gui/qml/qenetwork.py index c72a91bff..8abf94134 100644 --- a/electrum/gui/qml/qenetwork.py +++ b/electrum/gui/qml/qenetwork.py @@ -193,7 +193,7 @@ def height(self): # local blockchain height return self._height @pyqtProperty(int, notify=serverHeightChanged) - def server_height(self): + def serverHeight(self): return self._server_height @pyqtProperty(str, notify=statusChanged) @@ -223,11 +223,11 @@ def status(self): return self._network_status @pyqtProperty(str, notify=statusChanged) - def server_status(self): + def serverStatus(self): return self.network.get_connection_status_for_GUI() @pyqtProperty(bool, notify=statusChanged) - def is_connected(self): + def isConnected(self): return self._is_connected @pyqtProperty(int, notify=chaintipsChanged) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index c27ecf2dc..d75f5355d 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -181,16 +181,16 @@ def toreceive(self, toreceive): self._toreceive = toreceive self.toreceiveChanged.emit() - server_miningfeeChanged = pyqtSignal() - @pyqtProperty(QEAmount, notify=server_miningfeeChanged) - def server_miningfee(self): + serverMiningfeeChanged = pyqtSignal() + @pyqtProperty(QEAmount, notify=serverMiningfeeChanged) + def serverMiningfee(self): return self._server_miningfee - @server_miningfee.setter - def server_miningfee(self, server_miningfee): + @serverMiningfee.setter + def serverMiningfee(self, server_miningfee): if self._server_miningfee != server_miningfee: self._server_miningfee = server_miningfee - self.server_miningfeeChanged.emit() + self.serverMiningfeeChanged.emit() serverfeepercChanged = pyqtSignal() @pyqtProperty(str, notify=serverfeepercChanged) @@ -319,7 +319,7 @@ def swap_slider_moved(self): # fee breakdown self.serverfeeperc = f'{swap_manager.percentage:0.1f}%' server_miningfee = swap_manager.lockup_fee if self.isReverse else swap_manager.normal_fee - self.server_miningfee = QEAmount(amount_sat=server_miningfee) + self.serverMiningfee = QEAmount(amount_sat=server_miningfee) if self.isReverse: self.check_valid(self._send_amount, self._receive_amount) else: From 663ea431f6e06939511aa8c0d271d47862415f00 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 25 Apr 2023 14:22:19 +0200 Subject: [PATCH 0831/1143] followup 61179ede8c9254a0e03ecb01db7a97805d98d8be --- electrum/gui/qml/qewallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index abff997e1..c95187543 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -624,7 +624,7 @@ def deleteExpiredRequests(self): @pyqtSlot(QEAmount, str, int, bool, bool) @pyqtSlot(QEAmount, str, int, bool, bool, bool) def createRequest(self, amount: QEAmount, message: str, expiration: int, lightning_only: bool = False, reuse_address: bool = False): - self.delete_expired_requests() + self.deleteExpiredRequests() try: amount = amount.satsInt addr = self.wallet.get_unused_address() From 55140a9e27378a4c910ab187a14eaea18a86def7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 25 Apr 2023 17:54:07 +0000 Subject: [PATCH 0832/1143] gui/messages.py: allow localization of these strings --- electrum/gui/kivy/main_window.py | 2 +- .../kivy/uix/dialogs/lightning_channels.py | 2 +- .../uix/dialogs/lightning_open_channel.py | 2 +- electrum/gui/kivy/uix/dialogs/settings.py | 4 +- electrum/gui/messages.py | 80 ++++++++++--------- electrum/gui/qml/qechanneldetails.py | 2 +- electrum/gui/qml/qechannelopener.py | 2 +- electrum/gui/qt/channels_list.py | 4 +- electrum/gui/qt/main_window.py | 4 +- electrum/gui/qt/new_channel_dialog.py | 2 +- electrum/gui/qt/settings_dialog.py | 2 +- 11 files changed, 57 insertions(+), 49 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 892d6ffa3..3a053373d 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -751,7 +751,7 @@ def lightning_open_channel_dialog(self): self.show_error(_('Lightning is not enabled for this wallet')) return if not self.wallet.lnworker.channels and not self.wallet.lnworker.channel_backups: - warning = _(messages.MSG_LIGHTNING_WARNING) + warning = messages.MSG_LIGHTNING_WARNING d = Question(_('Do you want to create your first channel?') + '\n\n' + warning, self.open_channel_dialog_with_warning) d.open() diff --git a/electrum/gui/kivy/uix/dialogs/lightning_channels.py b/electrum/gui/kivy/uix/dialogs/lightning_channels.py index aaefac03a..5c3a4a098 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_channels.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_channels.py @@ -522,7 +522,7 @@ def close(self, close_options): choices=choices, key = min(choices.keys()), callback=self._close, - description=_(messages.MSG_REQUEST_FORCE_CLOSE), + description=messages.MSG_REQUEST_FORCE_CLOSE, keep_choice_order=True) dialog.open() diff --git a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py index 3123b1ff6..e1b6c11c6 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_open_channel.py @@ -233,7 +233,7 @@ def do_open_channel(self, funding_tx, conn_str, password): self.maybe_show_funding_tx(chan, funding_tx) else: title = _('Save backup') - help_text = _(messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL) + help_text = messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL data = lnworker.export_channel_backup(chan.channel_id) popup = QRDialog( title, data, diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py index b9fe39b3a..c5fc9da4b 100644 --- a/electrum/gui/kivy/uix/dialogs/settings.py +++ b/electrum/gui/kivy/uix/dialogs/settings.py @@ -94,7 +94,7 @@ status: _('Yes') if (app.use_recoverable_channels and not self.disabled) else _('No') title: _('Create recoverable channels') + ': ' + self.status description: _("Add channel recovery data to funding transaction.") - message: _(messages.MSG_RECOVERABLE_CHANNELS) + message: messages.MSG_RECOVERABLE_CHANNELS action: partial(root.boolean_dialog, 'use_recoverable_channels', _('Create recoverable channels'), self.message) CardSeparator SettingsItem: @@ -175,7 +175,7 @@ def cb(text): self._unit_dialog.open() def routing_dialog(self, item, dt): - description = _(messages.MSG_HELP_TRAMPOLINE) + description = messages.MSG_HELP_TRAMPOLINE def cb(text): self.app.use_gossip = (text == 'Gossip') dialog = ChoiceDialog( diff --git a/electrum/gui/messages.py b/electrum/gui/messages.py index d4edfc0f0..810aa7ffb 100644 --- a/electrum/gui/messages.py +++ b/electrum/gui/messages.py @@ -1,62 +1,70 @@ -# note: qt and kivy use different i18n methods -# FIXME all these messages *cannot* be localized currently! +from electrum.i18n import _ + +# note: kivy uses its own i18n methods in order to allow changing the language at runtime. +# These strings use electrum.i18n directly, to be GUI-agnostic, so the language for these +# cannot be changed at runtime. def to_rtf(msg): return '\n'.join(['

' + x + '

' for x in msg.split('\n\n')]) -MSG_RECOVERABLE_CHANNELS = """ -Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed. + +MSG_RECOVERABLE_CHANNELS = _( +"""Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed. Note that static backups only allow you to request a force-close with the remote node. This assumes that the remote node is still online, did not lose its data, and accepts to force close the channel. -If this is enabled, other nodes cannot open a channel to you. Channel recovery data is encrypted, so that only your wallet can decrypt it. However, blockchain analysis will be able to tell that the transaction was probably created by Electrum. -""" +If this is enabled, other nodes cannot open a channel to you. Channel recovery data is encrypted, so that only your wallet can decrypt it. However, blockchain analysis will be able to tell that the transaction was probably created by Electrum.""" +) -MSG_CONFIG_INSTANT_SWAPS = """ -If this option is checked, your client will complete reverse swaps before the funding transaction is confirmed. +MSG_CONFIG_INSTANT_SWAPS = _( +"""If this option is checked, your client will complete reverse swaps before the funding transaction is confirmed. -Note you are at risk of losing the funds in the swap, if the funding transaction never confirms. -""" +Note you are at risk of losing the funds in the swap, if the funding transaction never confirms.""" +) -MSG_COOPERATIVE_CLOSE = """ -Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees.""" +MSG_COOPERATIVE_CLOSE = _( +"""Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees.""" +) -MSG_REQUEST_FORCE_CLOSE = """ -If you request a force-close, your node will pretend that it has lost its data and ask the remote node to broadcast their latest state. Doing so from time to time helps make sure that nodes are honest, because your node can punish them if they broadcast a revoked state.""" +MSG_REQUEST_FORCE_CLOSE = _( +"""If you request a force-close, your node will pretend that it has lost its data and ask the remote node to broadcast their latest state. Doing so from time to time helps make sure that nodes are honest, because your node can punish them if they broadcast a revoked state.""" +) -MSG_CREATED_NON_RECOVERABLE_CHANNEL = """ -The channel you created is not recoverable from seed. +MSG_CREATED_NON_RECOVERABLE_CHANNEL = _( +"""The channel you created is not recoverable from seed. To prevent fund losses, please save this backup on another device. -It may be imported in another Electrum wallet with the same seed. -""" +It may be imported in another Electrum wallet with the same seed.""" +) -MSG_LIGHTNING_EXPERIMENTAL_WARNING = """ -Lightning support in Electrum is experimental. Do not put large amounts in lightning channels. -""" +MSG_LIGHTNING_EXPERIMENTAL_WARNING = _( +"""Lightning support in Electrum is experimental. Do not put large amounts in lightning channels.""" +) -MSG_LIGHTNING_SCB_WARNING = """ -Electrum uses static channel backups. If you lose your wallet file, you will need to request your channel to be force-closed by the remote peer in order to recover your funds. This assumes that the remote peer is reachable, and has not lost its own data. -""" +MSG_LIGHTNING_SCB_WARNING = _( +"""Electrum uses static channel backups. If you lose your wallet file, you will need to request your channel to be force-closed by the remote peer in order to recover your funds. This assumes that the remote peer is reachable, and has not lost its own data.""" +) MSG_LIGHTNING_WARNING = MSG_LIGHTNING_EXPERIMENTAL_WARNING + MSG_LIGHTNING_SCB_WARNING -MSG_HELP_TRAMPOLINE = """ -Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip). +MSG_HELP_TRAMPOLINE = _( +"""Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip). -Downloading the network gossip uses quite some bandwidth and storage, and is not recommended on mobile devices. If you use trampoline, you can only open channels with trampoline nodes. -""" +Downloading the network gossip uses quite some bandwidth and storage, and is not recommended on mobile devices. If you use trampoline, you can only open channels with trampoline nodes.""" +) -MGS_CONFLICTING_BACKUP_INSTANCE = """ -Another instance of this wallet (same seed) has an open channel with the same remote node. If you create this channel, you will not be able to use both wallets at the same time. +MGS_CONFLICTING_BACKUP_INSTANCE = _( +"""Another instance of this wallet (same seed) has an open channel with the same remote node. If you create this channel, you will not be able to use both wallets at the same time. -Are you sure? -""" +Are you sure?""" +) -MSG_CAPITAL_GAINS = """ -This summary covers only on-chain transactions (no lightning!). Capital gains are computed by attaching an acquisition price to each UTXO in the wallet, and uses the order of blockchain events (not FIFO). -""" +MSG_CAPITAL_GAINS = _( +"""This summary covers only on-chain transactions (no lightning!). Capital gains are computed by attaching an acquisition price to each UTXO in the wallet, and uses the order of blockchain events (not FIFO).""" +) -MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP = """Trampoline routing is enabled, but this channel is with a non-trampoline node. +MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP = _( +"""Trampoline routing is enabled, but this channel is with a non-trampoline node. This channel may still be used for receiving, but it is frozen for sending. If you want to keep using this channel, you need to disable trampoline routing in your preferences.""" +) diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 8340e2619..233a67731 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -172,7 +172,7 @@ def canDelete(self): @pyqtProperty(str, notify=channelChanged) def messageForceClose(self, notify=channelChanged): - return _(messages.MSG_REQUEST_FORCE_CLOSE).strip() + return messages.MSG_REQUEST_FORCE_CLOSE.strip() @pyqtProperty(bool, notify=channelChanged) def isBackup(self): diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 08d855764..833930ced 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -224,7 +224,7 @@ def open_thread(): #self.maybe_show_funding_tx(chan, funding_tx) #else: #title = _('Save backup') - #help_text = _(messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL) + #help_text = messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL #data = lnworker.export_channel_backup(chan.channel_id) #popup = QRDialog( #title, data, diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index e3fc3d416..acfc6523f 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -130,7 +130,7 @@ def on_failure(self, exc_info): def close_channel(self, channel_id): self.is_force_close = False msg = _('Cooperative close?') - msg += '\n' + _(messages.MSG_COOPERATIVE_CLOSE) + msg += '\n' + messages.MSG_COOPERATIVE_CLOSE if not self.main_window.question(msg): return coro = self.lnworker.close_channel(channel_id) @@ -185,7 +185,7 @@ def export_channel_backup(self, channel_id): def request_force_close(self, channel_id): msg = _('Request force-close from remote peer?') - msg += '\n' + _(messages.MSG_REQUEST_FORCE_CLOSE) + msg += '\n' + messages.MSG_REQUEST_FORCE_CLOSE if not self.main_window.question(msg): return def task(): diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index a67420ce2..683944622 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1275,7 +1275,7 @@ def on_open_channel_success(self, args): lnworker = self.wallet.lnworker if not chan.has_onchain_backup(): data = lnworker.export_channel_backup(chan.channel_id) - help_text = _(messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL) + help_text = messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL help_text += '\n\n' + _('Alternatively, you can save a backup of your wallet file from the File menu') self.show_qrcode( data, _('Save channel backup'), @@ -1697,7 +1697,7 @@ def new_channel_dialog(self, *, amount_sat=None, min_amount_sat=None): return lnworker = self.wallet.lnworker if not lnworker.channels and not lnworker.channel_backups: - msg = _('Do you want to create your first channel?') + '\n\n' + _(messages.MSG_LIGHTNING_WARNING) + msg = _('Do you want to create your first channel?') + '\n\n' + messages.MSG_LIGHTNING_WARNING if not self.question(msg): return d = NewChannelDialog(self, amount_sat, min_amount_sat) diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index 6eaf95fbe..36b589026 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -35,7 +35,7 @@ def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, m self.min_amount_sat = min_amount_sat or MIN_FUNDING_SAT vbox = QVBoxLayout(self) toolbar, menu = create_toolbar_with_menu(self.config, '') - recov_tooltip = messages.to_rtf(_(messages.MSG_RECOVERABLE_CHANNELS)) + recov_tooltip = messages.to_rtf(messages.MSG_RECOVERABLE_CHANNELS) menu.addConfig( _("Create recoverable channels"), 'use_recoverable_channels', True, tooltip=recov_tooltip, diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 6c4950a7b..4fa16734f 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -105,7 +105,7 @@ def on_nz(): nz.valueChanged.connect(on_nz) # lightning - help_trampoline = _(messages.MSG_HELP_TRAMPOLINE) + help_trampoline = messages.MSG_HELP_TRAMPOLINE trampoline_cb = QCheckBox(_("Use trampoline routing")) trampoline_cb.setToolTip(messages.to_rtf(help_trampoline)) trampoline_cb.setChecked(not bool(self.config.get('use_gossip', False))) From 5b122e723fdac9e862d873307a6c45e7b082c4d2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 25 Apr 2023 18:19:19 +0000 Subject: [PATCH 0833/1143] follow-up prev: re-add some newlines --- electrum/gui/messages.py | 2 +- electrum/gui/qt/channels_list.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/messages.py b/electrum/gui/messages.py index 810aa7ffb..66b9c13ee 100644 --- a/electrum/gui/messages.py +++ b/electrum/gui/messages.py @@ -44,7 +44,7 @@ def to_rtf(msg): """Electrum uses static channel backups. If you lose your wallet file, you will need to request your channel to be force-closed by the remote peer in order to recover your funds. This assumes that the remote peer is reachable, and has not lost its own data.""" ) -MSG_LIGHTNING_WARNING = MSG_LIGHTNING_EXPERIMENTAL_WARNING + MSG_LIGHTNING_SCB_WARNING +MSG_LIGHTNING_WARNING = MSG_LIGHTNING_EXPERIMENTAL_WARNING + "\n\n" + MSG_LIGHTNING_SCB_WARNING MSG_HELP_TRAMPOLINE = _( """Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip). diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index acfc6523f..a7fa706c0 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -130,7 +130,7 @@ def on_failure(self, exc_info): def close_channel(self, channel_id): self.is_force_close = False msg = _('Cooperative close?') - msg += '\n' + messages.MSG_COOPERATIVE_CLOSE + msg += '\n\n' + messages.MSG_COOPERATIVE_CLOSE if not self.main_window.question(msg): return coro = self.lnworker.close_channel(channel_id) @@ -185,7 +185,7 @@ def export_channel_backup(self, channel_id): def request_force_close(self, channel_id): msg = _('Request force-close from remote peer?') - msg += '\n' + messages.MSG_REQUEST_FORCE_CLOSE + msg += '\n\n' + messages.MSG_REQUEST_FORCE_CLOSE if not self.main_window.question(msg): return def task(): From 312e50e9a9d4dd6a88d878bfe2550d8c5c0bc774 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 25 Apr 2023 22:23:24 +0000 Subject: [PATCH 0834/1143] qml: send screen: bip21: fallback to onchain addr if no LN channels given a bip21 uri that has both onchain addr and bolt11, if we have LN enabled but no LN channels, auto-fallback to paying onchain we will have to clean up and unify this logic between GUIs. becoming spaghetti :/ rumour has it, Thomas has a branch? :P --- electrum/gui/qml/components/WalletMainView.qml | 3 +++ electrum/gui/qml/qeinvoice.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index f10008682..f20c3ea16 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -232,6 +232,9 @@ Item { onValidationWarning: { if (code == 'no_channels') { var dialog = app.messageDialog.createObject(app, { text: message }) + dialog.closed.connect(function() { + restartSendDialog() + }) dialog.open() // TODO: ask user to open a channel, if funds allow // and maybe store invoice if expiry allows diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 2118c8aef..6ef09e7c7 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -554,10 +554,14 @@ def validateRecipient(self, recipient): self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') self._validateRecipient_bip21_onchain(bip21) else: - self.setValidLightningInvoice(lninvoice) if not self._wallet.wallet.lnworker.channels: - self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) + if bip21 and 'address' in bip21: + self._logger.debug('flow where invoice has both LN and onchain, we have LN enabled but no channels') + self._validateRecipient_bip21_onchain(bip21) + else: + self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) else: + self.setValidLightningInvoice(lninvoice) self.validationSuccess.emit() else: self._logger.debug('flow without LN but having bip21 uri') From ee52154542aa4daa48fe4288c9f683056a109006 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 26 Apr 2023 13:08:27 +0000 Subject: [PATCH 0835/1143] transaction: TxInput: store addr/spk/value_sats, instead of re-calc We were re-calculating txin.address from the prevtx-utxo on every call, e.g. in electrum/gui/qt/utxo_list.py#L103 (for every utxo in the wallet). For a wallet with ~1300 utxos, UTXOList.update() took ~3.2 sec before this, now it's ~0.8 sec. --- electrum/transaction.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index b5bf0705e..d30183ffc 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -265,6 +265,9 @@ def __init__(self, *, self.spent_height = None # type: Optional[int] # height at which the TXO got spent self.spent_txid = None # type: Optional[str] # txid of the spender self._utxo = None # type: Optional[Transaction] + self.__scriptpubkey = None # type: Optional[bytes] + self.__address = None # type: Optional[str] + self.__value_sats = None # type: Optional[int] @property def short_id(self): @@ -289,6 +292,11 @@ def utxo(self, tx: Optional['Transaction']): return self.validate_data(utxo=tx) self._utxo = tx + # update derived fields + out_idx = self.prevout.out_idx + self.__scriptpubkey = self._utxo.outputs()[out_idx].scriptpubkey + self.__address = get_address_from_output_script(self.__scriptpubkey) + self.__value_sats = self._utxo.outputs()[out_idx].value def validate_data(self, *, utxo: Optional['Transaction'] = None, **kwargs) -> None: utxo = utxo or self.utxo @@ -308,23 +316,15 @@ def is_coinbase_output(self) -> bool: return self._is_coinbase_output def value_sats(self) -> Optional[int]: - if self.utxo: - out_idx = self.prevout.out_idx - return self.utxo.outputs()[out_idx].value - return None + return self.__value_sats @property def address(self) -> Optional[str]: - if self.scriptpubkey: - return get_address_from_output_script(self.scriptpubkey) - return None + return self.__address @property def scriptpubkey(self) -> Optional[bytes]: - if self.utxo: - out_idx = self.prevout.out_idx - return self.utxo.outputs()[out_idx].scriptpubkey - return None + return self.__scriptpubkey def to_json(self): d = { @@ -1560,6 +1560,8 @@ def address(self) -> Optional[str]: return addr if self._trusted_address is not None: return self._trusted_address + if self.witness_utxo: + return self.witness_utxo.address return None @property From ad5f95cb87d62225e12f288da4316476155e693e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 26 Apr 2023 14:49:37 +0000 Subject: [PATCH 0836/1143] util.profiler: add "min_threshold" arg --- electrum/gui/qt/utxo_list.py | 2 ++ electrum/util.py | 14 +++++++++++--- electrum/wallet.py | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index aff7e04e7..02d5fcb6e 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -35,6 +35,7 @@ from electrum.bitcoin import is_address from electrum.transaction import PartialTxInput, PartialTxOutput from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT +from electrum.util import profiler from .util import ColorScheme, MONOSPACE_FONT, EnterButton from .my_treeview import MyTreeView @@ -87,6 +88,7 @@ def create_toolbar(self, config): menu.addAction(_('Coin control'), lambda: self.add_selection_to_coincontrol()) return toolbar + @profiler(min_threshold=0.05) def update(self): # not calling maybe_defer_update() as it interferes with coincontrol status bar utxos = self.wallet.get_utxos() diff --git a/electrum/util.py b/electrum/util.py index a165117a9..29958a595 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -46,6 +46,7 @@ import random import secrets import functools +from functools import partial from abc import abstractmethod, ABC import socket @@ -449,15 +450,22 @@ def constant_time_compare(val1, val2): return hmac.compare_digest(to_bytes(val1, 'utf8'), to_bytes(val2, 'utf8')) -# decorator that prints execution time _profiler_logger = _logger.getChild('profiler') -def profiler(func): +def profiler(func=None, *, min_threshold: Union[int, float, None] = None): + """Function decorator that logs execution time. + + min_threshold: if set, only log if time taken is higher than threshold + NOTE: does not work with async methods. + """ + if func is None: + return partial(profiler, min_threshold=min_threshold) def do_profile(args, kw_args): name = func.__qualname__ t0 = time.time() o = func(*args, **kw_args) t = time.time() - t0 - _profiler_logger.debug(f"{name} {t:,.4f} sec") + if min_threshold is None or t > min_threshold: + _profiler_logger.debug(f"{name} {t:,.4f} sec") return o return lambda *args, **kw_args: do_profile(args, kw_args) diff --git a/electrum/wallet.py b/electrum/wallet.py index b48596e93..b5919abc1 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1657,6 +1657,7 @@ def can_pay_onchain(self, outputs, coins=None): return False return True + @profiler(min_threshold=0.1) def make_unsigned_transaction( self, *, coins: Sequence[PartialTxInput], From 8dca907891da3a3fa9e1a3a78d107c403d1a721b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 26 Apr 2023 18:29:46 +0200 Subject: [PATCH 0837/1143] get_tx_parent: populate cache in chronological order --- electrum/wallet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index b5919abc1..55018fc52 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -893,7 +893,11 @@ def get_tx_parents(self, txid) -> Dict: return {} with self.lock, self.transaction_lock: if self._last_full_history is None: - self._last_full_history = self.get_full_history(None) + self._last_full_history = self.get_full_history(None, include_lightning=False) + # populate cache in chronological order to avoid recursion limit + for _txid in self._last_full_history.keys(): + self.get_tx_parents(_txid) + result = self._tx_parents_cache.get(txid, None) if result is not None: return result From 910832c153d736d042c429a58ab54075e973000c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 26 Apr 2023 16:54:13 +0000 Subject: [PATCH 0838/1143] transaction: calc and cache TxInput/TxOutput.address on-demand In one wallet, before this, make_unsigned_transaction() took 120 sec, now it takes ~8 sec. hot path: ``` make_unsigned_transaction (electrum/wallet.py:1696) add_input_info (electrum/wallet.py:2261) utxo (electrum/transaction.py:289) tx_from_any (electrum/transaction.py:1232) deserialize (electrum/transaction.py:805) (electrum/transaction.py:805) parse_output (electrum/transaction.py:706) __init__ (electrum/transaction.py:127) scriptpubkey (electrum/transaction.py:173) get_address_from_output_script (electrum/transaction.py:672) ``` --- electrum/transaction.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index d30183ffc..82aecaada 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -63,6 +63,9 @@ DEBUG_PSBT_PARSING = False +_NEEDS_RECALC = ... # sentinel value + + class SerializationError(Exception): """ Thrown when there's a problem deserializing or serializing """ @@ -170,10 +173,12 @@ def scriptpubkey(self) -> bytes: @scriptpubkey.setter def scriptpubkey(self, scriptpubkey: bytes): self._scriptpubkey = scriptpubkey - self._address = get_address_from_output_script(scriptpubkey) + self._address = _NEEDS_RECALC @property def address(self) -> Optional[str]: + if self._address is _NEEDS_RECALC: + self._address = get_address_from_output_script(self._scriptpubkey) return self._address def get_ui_address_str(self) -> str: @@ -295,7 +300,7 @@ def utxo(self, tx: Optional['Transaction']): # update derived fields out_idx = self.prevout.out_idx self.__scriptpubkey = self._utxo.outputs()[out_idx].scriptpubkey - self.__address = get_address_from_output_script(self.__scriptpubkey) + self.__address = _NEEDS_RECALC self.__value_sats = self._utxo.outputs()[out_idx].value def validate_data(self, *, utxo: Optional['Transaction'] = None, **kwargs) -> None: @@ -320,6 +325,8 @@ def value_sats(self) -> Optional[int]: @property def address(self) -> Optional[str]: + if self.__address is _NEEDS_RECALC: + self.__address = get_address_from_output_script(self.__scriptpubkey) return self.__address @property From 57ae933582e44eb775ddbeab278b8e25d9aaf2d4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 26 Apr 2023 18:11:55 +0000 Subject: [PATCH 0839/1143] (trivial) add some type hints to wallet.get_tx_parents --- electrum/wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 55018fc52..f2d36ee0d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -884,7 +884,7 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: is_related_to_wallet=is_relevant, ) - def get_tx_parents(self, txid) -> Dict: + def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: """ recursively calls itself and returns a flat dict: txid -> list of parent txids @@ -901,9 +901,9 @@ def get_tx_parents(self, txid) -> Dict: result = self._tx_parents_cache.get(txid, None) if result is not None: return result - result = {} - parents = [] - uncles = [] + result = {} # type: Dict[str, Tuple[List[str], List[str]]] + parents = [] # type: List[str] + uncles = [] # type: List[str] tx = self.adb.get_transaction(txid) assert tx, f"cannot find {txid} in db" for i, txin in enumerate(tx.inputs()): From 87909485c5075a63be209e023af693e2c33e4d2e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 08:47:02 +0000 Subject: [PATCH 0840/1143] qml: wizard to check if wallet name is already used was erroring at the very last moment previously --- electrum/gui/qml/components/wizard/WCWalletName.qml | 2 +- electrum/gui/qml/qedaemon.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/wizard/WCWalletName.qml b/electrum/gui/qml/components/wizard/WCWalletName.qml index 07d12f8bb..3da936ee1 100644 --- a/electrum/gui/qml/components/wizard/WCWalletName.qml +++ b/electrum/gui/qml/components/wizard/WCWalletName.qml @@ -5,7 +5,7 @@ import QtQuick.Controls 2.1 import org.electrum 1.0 WizardComponent { - valid: wallet_name.text.length > 0 + valid: wallet_name.text.length > 0 && !Daemon.availableWallets.wallet_name_exists(wallet_name.text) function apply() { wizard_data['wallet_name'] = wallet_name.text diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index cf6ba212b..6f4a50b5a 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -91,6 +91,7 @@ def remove_wallet(self, path): self.wallets = wallets self.endRemoveRows() + @pyqtSlot(str, result=bool) def wallet_name_exists(self, name): for wallet_name, wallet_path in self.wallets: if name == wallet_name: From f5f177f7e8baebd97418a081f0ae4b93c1024565 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 09:34:23 +0000 Subject: [PATCH 0841/1143] qml wizard: fix restoring from old mpk (watchonly for "old" seeds) fixes https://github.com/spesmilo/electrum/issues/8356 --- electrum/gui/qml/qebitcoin.py | 31 +++++++++++++++---------------- electrum/gui/qml/qewizard.py | 2 +- electrum/wizard.py | 19 +++++++++++-------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index c0820293a..274f9cd81 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -107,28 +107,27 @@ def verifySeed(self, seed, seed_variant, wallet_type='standard'): @pyqtSlot(str, str, result=bool) def verifyMasterKey(self, key, wallet_type='standard'): + # FIXME exceptions raised in here are not well-behaved... self.validationMessage = '' if not keystore.is_master_key(key): self.validationMessage = _('Not a master key') return False k = keystore.from_master_key(key) - has_xpub = isinstance(k, keystore.Xpub) - assert has_xpub - t1 = xpub_type(k.xpub) - - if wallet_type == 'standard': - if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: - self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) - return False - return True - elif wallet_type == 'multisig': - if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: - self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) - return False - return True - - raise Exception(f'Unsupported wallet type: {wallet_type}') + if isinstance(k, keystore.Xpub): # has xpub # TODO are these checks useful? + t1 = xpub_type(k.xpub) + if wallet_type == 'standard': + if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: + self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) + return False + return True + elif wallet_type == 'multisig': + if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: + self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) + return False + return True + raise Exception(f'Unsupported wallet type: {wallet_type}') + return True @pyqtSlot(str, result=bool) def verifyDerivationPath(self, path): diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index a061099c9..5b26fa488 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -108,7 +108,7 @@ def createStorage(self, js_data, single_password_enabled, single_password): self.createSuccess.emit() except Exception as e: - self._logger.error(repr(e)) + self._logger.error(f"createStorage errored: {e!r}") self.createError.emit(str(e)) class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): diff --git a/electrum/wizard.py b/electrum/wizard.py index d46908fc6..072aa4534 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -381,15 +381,18 @@ def create_storage(self, path, data): raise Exception('unsupported/unknown seed_type %s' % data['seed_type']) elif data['keystore_type'] == 'masterkey': k = keystore.from_master_key(data['master_key']) - has_xpub = isinstance(k, keystore.Xpub) - assert has_xpub - t1 = xpub_type(k.xpub) - if data['wallet_type'] == 'multisig': - if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: - raise Exception('wrong key type %s' % t1) + if isinstance(k, keystore.Xpub): # has xpub + t1 = xpub_type(k.xpub) + if data['wallet_type'] == 'multisig': + if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: + raise Exception('wrong key type %s' % t1) + else: + if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: + raise Exception('wrong key type %s' % t1) + elif isinstance(k, keystore.Old_KeyStore): + pass else: - if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: - raise Exception('wrong key type %s' % t1) + raise Exception(f"unexpected keystore type: {type(keystore)}") else: raise Exception('unsupported/unknown keystore_type %s' % data['keystore_type']) From 4416f735922fbef87b8f533f1ac169b9ce824379 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 11:08:05 +0000 Subject: [PATCH 0842/1143] update locale submodule --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 1131e5258..9484b83d6 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 1131e525808c36d2b8300463e129830984049ec7 +Subproject commit 9484b83d622bea4a4c9d88cbe16f5b4f87822216 From 9e1bb940ac138c2447c2f5704853ca2eab5581e9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 25 Apr 2023 23:19:44 +0000 Subject: [PATCH 0843/1143] prepare release 4.4.1 --- RELEASE-NOTES | 24 ++++++++++++++++++++++++ electrum/version.py | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index c7c30ee1c..4ee585fba 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,27 @@ +# Release 4.4.1 (April 27, 2023) + * Qt GUI: + - fix sweeping (#8340) + - fix send tab input_qr_from_camera (#8342) + - fix crash reporter showing if send fails on typical errors (#8312) + - bumpfee: disallow targeting an abs fee. only allow feerate (#8318) + * QML GUI: + - fix offline-signing or co-signing pre-segwit txs (#8319) + - add option to show onchain address in ReceiveDetailsDialog (#8331) + - fix strings unique to QML did not get localized/translated (#8323) + - allow paying bip21 uri onchain that has both onchain and bolt11 + if we cannot pay on LN (#8334, 312e50e9) + - virtual keyboard: make buttons somewhat larger (75e65c5c) + - fix(?) Android crash with some OS-accessibility settings (#8344) + - fix channelopener.connectStr qr scan popping under (#8335) + - fix restoring from old mpk (watchonly for "old" seeds) (#8356) + * libsecp256k1: add runtime support for 0.3.x, bump bundled to 0.3.1 + * forbid paying to "http:" lnurls (enforce https or .onion) (1b5c7d46) + * fix wallet.bump_fee "decrease payment" erroring on too high target + fee rate (#8316) + * fix performance regressions in tx logic (ee521545, 910832c1) + * fix "recursion depth exceeded" for utxo privacy analysis (#8315) + + # Release 4.4.0 (April 18, 2023) * New Android app, using QML instead of Kivy diff --git a/electrum/version.py b/electrum/version.py index 349e96364..b9c775ac0 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.4.0' # version of the client package -APK_VERSION = '4.4.0.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.4.1' # version of the client package +APK_VERSION = '4.4.1.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 499f51535ffa54607fd75eb4bf817cb96bd0e0cd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 17:03:16 +0000 Subject: [PATCH 0844/1143] bip32: fix hardened char "h" vs "'" compatibility for some hw wallets in particular, ledger: fix sign_message for some wallets ``` 156.02 | E | plugins.ledger.ledger | Traceback (most recent call last): File "...\electrum\electrum\plugins\ledger\ledger.py", line 1265, in sign_message result = base64.b64decode(self.client.sign_message(message, address_path)) File "...\Python310\site-packages\ledger_bitcoin\client.py", line 230, in sign_message sw, response = self._make_request(self.builder.sign_message(message_bytes, bip32_path), client_intepreter) File "...\Python310\site-packages\ledger_bitcoin\command_builder.py", line 176, in sign_message bip32_path: List[bytes] = bip32_path_from_string(bip32_path) File "...\Python310\site-packages\ledger_bitcoin\common.py", line 68, in bip32_path_from_string return [int(p).to_bytes(4, byteorder="big") if "'" not in p File "...\Python310\site-packages\ledger_bitcoin\common.py", line 68, in return [int(p).to_bytes(4, byteorder="big") if "'" not in p ValueError: invalid literal for int() with base 10: '84h' ``` Regression from df2bd61de6607fa8bbc73cbf53ee1d99fac61bfd, where the default hardened char was changed from "'" to "h". Note that there was no corresponding wallet db upgrade, so some files use one char and others use the other. --- electrum/bip32.py | 10 ++++++---- electrum/keystore.py | 4 +++- electrum/plugins/digitalbitbox/digitalbitbox.py | 5 ++++- electrum/plugins/ledger/ledger.py | 16 ++++++++++------ electrum/tests/test_bitcoin.py | 4 ++++ 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 25eeadbd4..248abef21 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -352,7 +352,9 @@ def convert_bip32_strpath_to_intpath(n: str) -> List[int]: return path -def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str: +def convert_bip32_intpath_to_strpath(path: Sequence[int], *, hardened_char=BIP32_HARDENED_CHAR) -> str: + assert isinstance(hardened_char, str), hardened_char + assert len(hardened_char) == 1, hardened_char s = "m/" for child_index in path: if not isinstance(child_index, int): @@ -361,7 +363,7 @@ def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str: raise ValueError(f"bip32 path child index out of range: {child_index}") prime = "" if child_index & BIP32_PRIME: - prime = BIP32_HARDENED_CHAR + prime = hardened_char child_index = child_index ^ BIP32_PRIME s += str(child_index) + prime + '/' # cut trailing "/" @@ -380,13 +382,13 @@ def is_bip32_derivation(s: str) -> bool: return True -def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]: +def normalize_bip32_derivation(s: Optional[str], *, hardened_char=BIP32_HARDENED_CHAR) -> Optional[str]: if s is None: return None if not is_bip32_derivation(s): raise ValueError(f"invalid bip32 derivation: {s}") ints = convert_bip32_strpath_to_intpath(s) - return convert_bip32_intpath_to_strpath(ints) + return convert_bip32_intpath_to_strpath(ints, hardened_char=hardened_char) def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool: diff --git a/electrum/keystore.py b/electrum/keystore.py index 61305635c..fdab6f644 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -508,7 +508,9 @@ def get_bip32_node_for_xpub(self) -> Optional[BIP32Node]: return self._xpub_bip32_node def get_derivation_prefix(self) -> Optional[str]: - return self._derivation_prefix + if self._derivation_prefix is None: + return None + return normalize_bip32_derivation(self._derivation_prefix) def get_root_fingerprint(self) -> Optional[str]: return self._root_fingerprint diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 1c82b3a76..22caaa155 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -19,6 +19,7 @@ from electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.bitcoin import public_key_to_p2pkh from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation +from electrum.bip32 import normalize_bip32_derivation from electrum import descriptor from electrum import ecc from electrum.ecc import msg_magic @@ -104,7 +105,8 @@ def has_usable_connection_with_device(self): return False return True - def _get_xpub(self, bip32_path): + def _get_xpub(self, bip32_path: str): + bip32_path = normalize_bip32_derivation(bip32_path, hardened_char="'") if self.check_device_dialog(): return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8')) @@ -458,6 +460,7 @@ def sign_message(self, sequence, message, password, *, script_type=None): try: message = message.encode('utf8') inputPath = self.get_derivation_prefix() + "/%d/%d" % sequence + inputPath = normalize_bip32_derivation(inputPath, hardened_char="'") msg_hash = sha256d(msg_magic(message)) inputHash = to_hexstr(msg_hash) hasharray = [] diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 0e4245f74..17c1caca4 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -10,7 +10,7 @@ from electrum import bip32, constants, ecc from electrum import descriptor from electrum.base_wizard import ScriptTypeNotSupported -from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, normalize_bip32_derivation from electrum.bitcoin import EncodeBase58Check, int_to_hex, is_b58_address, is_segwit_script_type, var_int from electrum.crypto import hash_160 from electrum.i18n import _ @@ -430,7 +430,7 @@ def get_xpub(self, bip32_path, xtype): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) - bip32_path = bip32.normalize_bip32_derivation(bip32_path) + bip32_path = bip32.normalize_bip32_derivation(bip32_path, hardened_char="'") bip32_intpath = bip32.convert_bip32_strpath_to_intpath(bip32_path) bip32_path = bip32_path[2:] # cut off "m/" if len(bip32_intpath) >= 1: @@ -931,10 +931,10 @@ def has_usable_connection_with_device(self): @runs_in_hwd_thread @test_pin_unlocked - def get_xpub(self, bip32_path, xtype): + def get_xpub(self, bip32_path: str, xtype): # try silently first; if not a standard path, repeat with on-screen display - bip32_path = bip32_path.replace('h', '\'') + bip32_path = normalize_bip32_derivation(bip32_path, hardened_char="'") # cache known path/xpubs combinations in order to avoid requesting them many times if bip32_path in self._known_xpubs: @@ -1300,14 +1300,18 @@ def decrypt_message(self, pubkey, message, password): raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) def sign_message(self, sequence, *args, **kwargs): - address_path = self.get_derivation_prefix()[2:] + "/%d/%d" % sequence + address_path = self.get_derivation_prefix() + "/%d/%d" % sequence + address_path = normalize_bip32_derivation(address_path, hardened_char="'") + address_path = address_path[2:] # cut m/ return self.get_client_dongle_object().sign_message(address_path, *args, **kwargs) def sign_transaction(self, *args, **kwargs): return self.get_client_dongle_object().sign_transaction(self, *args, **kwargs) def show_address(self, sequence, *args, **kwargs): - address_path = self.get_derivation_prefix()[2:] + "/%d/%d" % sequence + address_path = self.get_derivation_prefix() + "/%d/%d" % sequence + address_path = normalize_bip32_derivation(address_path, hardened_char="'") + address_path = address_path[2:] # cut m/ return self.get_client_dongle_object().show_address(address_path, *args, **kwargs) diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py index 5699902c4..0a8d29741 100644 --- a/electrum/tests/test_bitcoin.py +++ b/electrum/tests/test_bitcoin.py @@ -862,6 +862,10 @@ def test_convert_bip32_intpath_to_strpath(self): self.assertEqual("m", convert_bip32_intpath_to_strpath([])) self.assertEqual("m/44h/5241h/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221])) + self.assertEqual("m/0/1'/1'", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001], hardened_char="'")) + self.assertEqual("m", convert_bip32_intpath_to_strpath([], hardened_char="'")) + self.assertEqual("m/44'/5241'/221", convert_bip32_intpath_to_strpath([2147483692, 2147488889, 221], hardened_char="'")) + def test_normalize_bip32_derivation(self): self.assertEqual("m/0/1h/1h", normalize_bip32_derivation("m/0/1h/1'")) self.assertEqual("m", normalize_bip32_derivation("m////")) From 22b8c4e39768adefd2ca8794c3e97a90c73b697a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 17:33:54 +0000 Subject: [PATCH 0845/1143] hww: fix digitalbitbox(1) support regression from cea4238b8110d247faea8bfbe5294563ef25edba ``` 975.04 | E | plugin.DeviceMgr | failed to create client for digitalbitbox at : AttributeError("'NoneType' object has no attribute 'get_passphrase'") Traceback (most recent call last): File "...\electrum\electrum\plugin.py", line 620, in list_pairable_device_infos soft_device_id = client.get_soft_device_id() File "...\electrum\electrum\plugins\hw_wallet\plugin.py", line 251, in get_soft_device_id root_fp = self.request_root_fingerprint_from_device() File "...\electrum\electrum\plugin.py", line 362, in wrapper return run_in_hwd_thread(partial(func, *args, **kwargs)) File "...\electrum\electrum\plugin.py", line 352, in run_in_hwd_thread return func() File "...\electrum\electrum\plugins\hw_wallet\plugin.py", line 264, in request_root_fingerprint_from_device child_of_root_xpub = self.get_xpub("m/0'", xtype='standard') File "...\electrum\electrum\plugins\digitalbitbox\digitalbitbox.py", line 115, in get_xpub reply = self._get_xpub(bip32_path) File "...\electrum\electrum\plugins\digitalbitbox\digitalbitbox.py", line 110, in _get_xpub if self.check_device_dialog(): File "...\electrum\electrum\plugins\digitalbitbox\digitalbitbox.py", line 197, in check_device_dialog if not self.password_dialog(msg): File "...\electrum\electrum\plugins\digitalbitbox\digitalbitbox.py", line 159, in password_dialog password = self.handler.get_passphrase(msg, False) AttributeError: 'NoneType' object has no attribute 'get_passphrase' ``` --- electrum/plugins/digitalbitbox/digitalbitbox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py index 22caaa155..f5fd6f8ff 100644 --- a/electrum/plugins/digitalbitbox/digitalbitbox.py +++ b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -125,6 +125,9 @@ def get_xpub(self, bip32_path, xtype): else: raise Exception('no reply') + def get_soft_device_id(self): + return None + def dbb_has_password(self): reply = self.hid_send_plain(b'{"ping":""}') if 'ping' not in reply: From 155258f2087b000ce4115d744c0856689c5f68c2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 27 Apr 2023 15:25:35 +0000 Subject: [PATCH 0846/1143] release.sh: check we have each binary (RM case) ~duplicated from the non-RM case --- contrib/android/make_apk.sh | 1 + contrib/release.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/contrib/android/make_apk.sh b/contrib/android/make_apk.sh index 5f0d383fb..7f2a34890 100755 --- a/contrib/android/make_apk.sh +++ b/contrib/android/make_apk.sh @@ -89,6 +89,7 @@ fi if [[ "$2" == "all" ]] ; then # build all apks + # FIXME failures are not propagated out: we should fail the script if any arch build fails export APP_ANDROID_ARCH=armeabi-v7a make $TARGET export APP_ANDROID_ARCH=arm64-v8a diff --git a/contrib/release.sh b/contrib/release.sh index ce5b59634..3320eb8a5 100755 --- a/contrib/release.sh +++ b/contrib/release.sh @@ -259,6 +259,18 @@ else cd "$PROJECT_ROOT" + # check we have each binary + test -f "$PROJECT_ROOT/dist/$tarball" || fail "tarball not found among built files" + test -f "$PROJECT_ROOT/dist/$srctarball" || fail "srctarball not found among built files" + test -f "$PROJECT_ROOT/dist/$appimage" || fail "appimage not found among built files" + test -f "$PROJECT_ROOT/dist/$win1" || fail "win1 not found among built files" + test -f "$PROJECT_ROOT/dist/$win2" || fail "win2 not found among built files" + test -f "$PROJECT_ROOT/dist/$win3" || fail "win3 not found among built files" + test -f "$PROJECT_ROOT/dist/$apk1" || fail "apk1 not found among built files" + test -f "$PROJECT_ROOT/dist/$apk2" || fail "apk2 not found among built files" + test -f "$PROJECT_ROOT/dist/$apk3" || fail "apk3 not found among built files" + test -f "$PROJECT_ROOT/dist/$dmg" || fail "dmg not found among built files" + if [ $REV != $VERSION ]; then fail "versions differ, not uploading" fi From db53b0f5730f97be5dea62df2f27f47d80fe5a8f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 29 Apr 2023 12:05:25 +0200 Subject: [PATCH 0847/1143] qml: fix 2fa callback issue #8368 --- electrum/gui/qml/qewallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index c95187543..72cba6b7f 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -488,7 +488,7 @@ def enableLightning(self): @auth_protect() def sign(self, tx, *, broadcast: bool = False, on_success: Callable[[Transaction], None] = None, on_failure: Callable[[], None] = None): - sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, on_success, broadcast), partial(self.on_sign_failed, on_failure)) + sign_hook = run_hook('tc_sign_wrapper', self.wallet, tx, partial(self.on_sign_complete, broadcast, on_success), partial(self.on_sign_failed, on_failure)) if sign_hook: success = self.do_sign(tx, False) if success: From 1b362f64f2246dd0c011c44370c59154f2c8609d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 29 Apr 2023 13:57:16 +0200 Subject: [PATCH 0848/1143] qml: properly delete wizard components after use. fixes #8357 --- electrum/gui/qml/components/main.qml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index acb7ac36a..e6b770ad9 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -304,13 +304,17 @@ ApplicationWindow property alias newWalletWizard: _newWalletWizard Component { id: _newWalletWizard - NewWalletWizard { } + NewWalletWizard { + onClosed: destroy() + } } property alias serverConnectWizard: _serverConnectWizard Component { id: _serverConnectWizard - ServerConnectWizard { } + ServerConnectWizard { + onClosed: destroy() + } } property alias messageDialog: _messageDialog From 56165c3790ede32ea81917f5499322e4dd6fd629 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 29 Apr 2023 14:13:12 +0200 Subject: [PATCH 0849/1143] qml: don't start loadWallet if daemon is busy loading. --- electrum/gui/qml/components/Wallets.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index 2ac0c7763..f651fa33c 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -59,7 +59,8 @@ Pane { onClicked: { if (!Daemon.currentWallet || Daemon.currentWallet.name != model.name) - Daemon.loadWallet(model.path) + if (!Daemon.loading) // wallet load in progress + Daemon.loadWallet(model.path) else app.stack.pop() } From 5600375d51b942dfeab598b5f6f850c87bd9bf74 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 29 Apr 2023 13:42:10 +0200 Subject: [PATCH 0850/1143] qml: no auto caps on import and master key controls --- electrum/gui/qml/components/wizard/WCHaveMasterKey.qml | 2 +- electrum/gui/qml/components/wizard/WCImport.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index 0a1ab3734..9c8fde40f 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -122,7 +122,7 @@ WizardComponent { if (activeFocus) verifyMasterKey(text) } - inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase } ColumnLayout { ToolButton { diff --git a/electrum/gui/qml/components/wizard/WCImport.qml b/electrum/gui/qml/components/wizard/WCImport.qml index 3129082b8..8a5aca37f 100644 --- a/electrum/gui/qml/components/wizard/WCImport.qml +++ b/electrum/gui/qml/components/wizard/WCImport.qml @@ -40,7 +40,7 @@ WizardComponent { focus: true wrapMode: TextEdit.WrapAnywhere onTextChanged: valid = verify(text) - inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase } ColumnLayout { Layout.alignment: Qt.AlignTop From 9ac83f6d9f31199322a2de4c10f28cbd55d197bb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 1 May 2023 16:59:40 +0200 Subject: [PATCH 0851/1143] qml: add SeedKeyboard for seed entry without using system virtual keyboard --- .../qml/components/controls/SeedKeyboard.qml | 90 +++++++++++++++++++ .../components/controls/SeedKeyboardKey.qml | 34 +++++++ .../qml/components/controls/SeedTextArea.qml | 18 +++- 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 electrum/gui/qml/components/controls/SeedKeyboard.qml create mode 100644 electrum/gui/qml/components/controls/SeedKeyboardKey.qml diff --git a/electrum/gui/qml/components/controls/SeedKeyboard.qml b/electrum/gui/qml/components/controls/SeedKeyboard.qml new file mode 100644 index 000000000..73efc2fba --- /dev/null +++ b/electrum/gui/qml/components/controls/SeedKeyboard.qml @@ -0,0 +1,90 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 + +Item { + id: root + + signal keyEvent(keycode: int, text: string) + + property int padding: 15 + + property int keywidth: (root.width - 2 * padding) / 11 - keyhspacing + property int keyheight: (root.height - 2 * padding) / 4 - keyvspacing + property int keyhspacing: 4 + property int keyvspacing: 5 + + function emitKeyEvent(key) { + var keycode + if (key == '<=') { + keycode = Qt.Key_Backspace + } else { + keycode = parseInt(key, 36) - 9 + 0x40 // map char to key code + } + keyEvent(keycode, key) + } + + ColumnLayout { + id: rootLayout + x: padding + y: padding + width: parent.width - 2*padding + spacing: keyvspacing + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: keyhspacing + Repeater { + model: ['q','w','e','r','t','y','u','i','o','p','<='] + delegate: SeedKeyboardKey { + key: modelData + kbd: root + implicitWidth: keywidth + implicitHeight: keyheight + } + } + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: keyhspacing + Repeater { + model: ['a','s','d','f','g','h','j','k','l'] + delegate: SeedKeyboardKey { + key: modelData + kbd: root + implicitWidth: keywidth + implicitHeight: keyheight + } + } + // spacer + Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth / 2 } + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: keyhspacing + Repeater { + model: ['z','x','c','v','b','n','m'] + delegate: SeedKeyboardKey { + key: modelData + kbd: root + implicitWidth: keywidth + implicitHeight: keyheight + } + } + // spacer + Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth } + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + SeedKeyboardKey { + key: ' ' + kbd: root + implicitWidth: keywidth * 5 + implicitHeight: keyheight + } + // spacer + Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth / 2 } + } + } + +} diff --git a/electrum/gui/qml/components/controls/SeedKeyboardKey.qml b/electrum/gui/qml/components/controls/SeedKeyboardKey.qml new file mode 100644 index 000000000..edf878df4 --- /dev/null +++ b/electrum/gui/qml/components/controls/SeedKeyboardKey.qml @@ -0,0 +1,34 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.0 + +Pane { + id: root + + property string key + property QtObject kbd + padding: 1 + + FlatButton { + anchors.fill: parent + + focusPolicy: Qt.NoFocus + autoRepeat: true + autoRepeatDelay: 750 + + text: key + + padding: 0 + font.pixelSize: Math.max(root.height * 1/3, constants.fontSizeSmall) + + onClicked: { + kbd.emitKeyEvent(key) + } + + // send keyevent again, otherwise it is ignored + onDoubleClicked: { + kbd.emitKeyEvent(key) + } + } +} diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 0047869be..096c7bb52 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -11,7 +11,7 @@ Pane { padding: 0 property string text - property alias readOnly: seedtextarea.readOnly + property bool readOnly: false property alias placeholderText: seedtextarea.placeholderText property var _suggestions: [] @@ -83,6 +83,7 @@ Pane { font.pixelSize: constants.fontSizeLarge font.family: FixedFont inputMethodHints: Qt.ImhSensitiveData | Qt.ImhLowercaseOnly | Qt.ImhNoPredictiveText + readOnly: true background: Rectangle { color: constants.darkerBackground @@ -100,6 +101,21 @@ Pane { cursorPosition = text.length } } + + SeedKeyboard { + id: kbd + Layout.fillWidth: true + Layout.preferredHeight: kbd.width / 2 + visible: !root.readOnly + onKeyEvent: { + if (keycode == Qt.Key_Backspace) { + if (seedtextarea.text.length > 0) + seedtextarea.text = seedtextarea.text.substring(0, seedtextarea.text.length-1) + } else { + seedtextarea.text = seedtextarea.text + text + } + } + } } FontMetrics { From 6394cdcd3b25885d8bf0e31331b45f8dd4e1529b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 1 May 2023 18:39:16 +0200 Subject: [PATCH 0852/1143] qml: disable Qt Virtual Keyboard and refactor keyboardFreeZone item to take android system keyboard into account --- electrum/gui/qml/__init__.py | 4 --- electrum/gui/qml/components/main.qml | 54 ++++++++++++++++------------ 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 6a0a70037..5d5bf5045 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -61,10 +61,6 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') # os.environ['QML_IMPORT_TRACE'] = '1' # os.environ['QT_DEBUG_PLUGINS'] = '1' - os.environ['QT_IM_MODULE'] = 'qtvirtualkeyboard' - os.environ['QT_VIRTUALKEYBOARD_STYLE'] = 'Electrum' - os.environ['QML2_IMPORT_PATH'] = 'electrum/gui/qml' - os.environ['QT_ANDROID_DISABLE_ACCESSIBILITY'] = '1' # set default locale to en_GB. This is for l10n (e.g. number formatting, number input etc), diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index e6b770ad9..1dd04544a 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -3,6 +3,7 @@ import QtQuick.Layouts 1.0 import QtQuick.Controls 2.3 import QtQuick.Controls.Material 2.0 import QtQuick.Controls.Material.impl 2.12 +import QtQuick.Window 2.15 import QtQml 2.6 import QtMultimedia 5.6 @@ -31,7 +32,6 @@ ApplicationWindow Constants { id: appconstants } property alias stack: mainStackView - property alias inputPanel: inputPanel property variant activeDialogs: [] @@ -224,7 +224,7 @@ ApplicationWindow StackView { id: mainStackView width: parent.width - height: inputPanel.y - header.height + height: keyboardFreeZone.height - header.height initialItem: Qt.resolvedUrl('WalletMainView.qml') function getRoot() { @@ -266,39 +266,47 @@ ApplicationWindow } Item { + id: keyboardFreeZone // Item as first child in Overlay that adjusts its size to the available // screen space minus the virtual keyboard (e.g. to center dialogs in) - // see ElDialog.resizeWithKeyboard property + // see also ElDialog.resizeWithKeyboard property parent: Overlay.overlay width: parent.width - height: inputPanel.y - } - - InputPanel { - id: inputPanel - width: parent.width - y: parent.height + height: parent.height states: State { name: "visible" - when: inputPanel.active + when: Qt.inputMethod.visible PropertyChanges { - target: inputPanel - y: parent.height - height + target: keyboardFreeZone + height: keyboardFreeZone.parent.height - Qt.inputMethod.keyboardRectangle.height / Screen.devicePixelRatio } } - transitions: Transition { - from: '' - to: 'visible' - reversible: true - ParallelAnimation { - NumberAnimation { - properties: "y" - duration: 250 - easing.type: Easing.OutQuad + transitions: [ + Transition { + from: '' + to: 'visible' + ParallelAnimation { + NumberAnimation { + properties: "height" + duration: 250 + easing.type: Easing.OutQuad + } + } + }, + Transition { + from: 'visible' + to: '' + ParallelAnimation { + NumberAnimation { + properties: "height" + duration: 50 + easing.type: Easing.OutQuad + } } } - } + ] + } property alias newWalletWizard: _newWalletWizard From 6d392a67f9887265b0677a2f91acec41b18e8758 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 2 May 2023 08:51:32 +0200 Subject: [PATCH 0853/1143] wallet.get_tx_parents: populate cache regardless of self.is_up_to_date() Fixes #8315 --- electrum/gui/qt/utxo_list.py | 4 ++-- electrum/wallet.py | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 02d5fcb6e..adb966383 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -134,8 +134,8 @@ def refresh_row(self, key, row): utxo = self._utxo_dict[key] utxo_item = [self.std_model.item(row, col) for col in self.Columns] txid = utxo.prevout.txid.hex() - parents = self.wallet.get_tx_parents(txid) - utxo_item[self.Columns.PARENTS].setText('%6s'%len(parents)) + num_parents = self.wallet.get_num_parents(txid) + utxo_item[self.Columns.PARENTS].setText('%6s'%num_parents if num_parents else '-') label = self.wallet.get_label_for_txid(txid) or '' utxo_item[self.Columns.LABEL].setText(label) SELECTED_TO_SPEND_TOOLTIP = _('Coin selected to be spent') diff --git a/electrum/wallet.py b/electrum/wallet.py index f2d36ee0d..958d58236 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -884,17 +884,20 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: is_related_to_wallet=is_relevant, ) + def get_num_parents(self, txid: str) -> Optional[int]: + if not self.is_up_to_date(): + return + return len(self.get_tx_parents(txid)) + def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: """ - recursively calls itself and returns a flat dict: + returns a flat dict: txid -> list of parent txids """ - if not self.is_up_to_date(): - return {} with self.lock, self.transaction_lock: if self._last_full_history is None: self._last_full_history = self.get_full_history(None, include_lightning=False) - # populate cache in chronological order to avoid recursion limit + # populate cache in chronological order for _txid in self._last_full_history.keys(): self.get_tx_parents(_txid) @@ -924,7 +927,8 @@ def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: for _txid in parents + uncles: if _txid in self._last_full_history.keys(): - result.update(self.get_tx_parents(_txid)) + p = self._tx_parents_cache[_txid] + result.update(p) result[txid] = parents, uncles self._tx_parents_cache[txid] = result return result From 8a25d59ba66800367225a3eba5d17a241b0e8b9b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 2 May 2023 08:54:00 +0200 Subject: [PATCH 0854/1143] wallet: save num_parents of utxos in wallet file. fixes #8361 --- electrum/wallet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 958d58236..d31e10261 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -339,6 +339,7 @@ def __init__(self, db: WalletDB, storage: Optional[WalletStorage], *, config: Si self._receive_requests = db.get_dict('payment_requests') # type: Dict[str, Request] self._invoices = db.get_dict('invoices') # type: Dict[str, Invoice] self._reserved_addresses = set(db.get('reserved_addresses', [])) + self._num_parents = db.get_dict('num_parents') self._freeze_lock = threading.RLock() # for mutating/iterating frozen_{addresses,coins} @@ -472,6 +473,7 @@ def tx_is_related(self, tx): def clear_tx_parents_cache(self): with self.lock, self.transaction_lock: self._tx_parents_cache.clear() + self._num_parents.clear() self._last_full_history = None @event_listener @@ -887,7 +889,9 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: def get_num_parents(self, txid: str) -> Optional[int]: if not self.is_up_to_date(): return - return len(self.get_tx_parents(txid)) + if txid not in self._num_parents: + self._num_parents[txid] = len(self.get_tx_parents(txid)) + return self._num_parents[txid] def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: """ From b290fbd4b5fa315dfba2e716a247b52aead94942 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 2 May 2023 13:03:13 +0200 Subject: [PATCH 0855/1143] qml: seedkeyboard padding, explicit keycode, backspace next to spacebar --- .../qml/components/controls/SeedKeyboard.qml | 29 ++++++++++--------- .../components/controls/SeedKeyboardKey.qml | 13 +++++++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/components/controls/SeedKeyboard.qml b/electrum/gui/qml/components/controls/SeedKeyboard.qml index 73efc2fba..15b87ab28 100644 --- a/electrum/gui/qml/components/controls/SeedKeyboard.qml +++ b/electrum/gui/qml/components/controls/SeedKeyboard.qml @@ -8,34 +8,29 @@ Item { signal keyEvent(keycode: int, text: string) - property int padding: 15 + property int hpadding: 0 + property int vpadding: 15 - property int keywidth: (root.width - 2 * padding) / 11 - keyhspacing + property int keywidth: (root.width - 2 * padding) / 10 - keyhspacing property int keyheight: (root.height - 2 * padding) / 4 - keyvspacing property int keyhspacing: 4 property int keyvspacing: 5 - function emitKeyEvent(key) { - var keycode - if (key == '<=') { - keycode = Qt.Key_Backspace - } else { - keycode = parseInt(key, 36) - 9 + 0x40 // map char to key code - } + function emitKeyEvent(key, keycode) { keyEvent(keycode, key) } ColumnLayout { id: rootLayout - x: padding - y: padding - width: parent.width - 2*padding + x: hpadding + y: vpadding + width: parent.width - 2*hpadding spacing: keyvspacing RowLayout { Layout.alignment: Qt.AlignHCenter spacing: keyhspacing Repeater { - model: ['q','w','e','r','t','y','u','i','o','p','<='] + model: ['q','w','e','r','t','y','u','i','o','p'] delegate: SeedKeyboardKey { key: modelData kbd: root @@ -78,10 +73,18 @@ Item { Layout.alignment: Qt.AlignHCenter SeedKeyboardKey { key: ' ' + keycode: Qt.Key_Space kbd: root implicitWidth: keywidth * 5 implicitHeight: keyheight } + SeedKeyboardKey { + key: '<' + keycode: Qt.Key_Backspace + kbd: root + implicitWidth: keywidth + implicitHeight: keyheight + } // spacer Item { Layout.preferredHeight: 1; Layout.preferredWidth: keywidth / 2 } } diff --git a/electrum/gui/qml/components/controls/SeedKeyboardKey.qml b/electrum/gui/qml/components/controls/SeedKeyboardKey.qml index edf878df4..16c39b8e2 100644 --- a/electrum/gui/qml/components/controls/SeedKeyboardKey.qml +++ b/electrum/gui/qml/components/controls/SeedKeyboardKey.qml @@ -7,9 +7,18 @@ Pane { id: root property string key + property int keycode: -1 + property QtObject kbd padding: 1 + function emitKeyEvent() { + if (keycode == -1) { + keycode = parseInt(key, 36) - 9 + 0x40 // map a-z char to key code + } + kbd.keyEvent(keycode, key) + } + FlatButton { anchors.fill: parent @@ -23,12 +32,12 @@ Pane { font.pixelSize: Math.max(root.height * 1/3, constants.fontSizeSmall) onClicked: { - kbd.emitKeyEvent(key) + emitKeyEvent() } // send keyevent again, otherwise it is ignored onDoubleClicked: { - kbd.emitKeyEvent(key) + emitKeyEvent() } } } From 95f960bb677cd6ddfe4adbf0a027044e2f7d1150 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 2 May 2023 13:04:33 +0200 Subject: [PATCH 0856/1143] qml: seedtext word suggestions below text area --- .../qml/components/controls/SeedTextArea.qml | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 096c7bb52..1681cad5d 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -29,6 +29,39 @@ Pane { id: rootLayout width: parent.width spacing: 0 + + TextArea { + id: seedtextarea + Layout.fillWidth: true + Layout.minimumHeight: fontMetrics.height * 3 + topPadding + bottomPadding + + rightPadding: constants.paddingLarge + leftPadding: constants.paddingLarge + + wrapMode: TextInput.WordWrap + font.bold: true + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + inputMethodHints: Qt.ImhSensitiveData | Qt.ImhLowercaseOnly | Qt.ImhNoPredictiveText + readOnly: true + + background: Rectangle { + color: constants.darkerBackground + } + + onTextChanged: { + // work around Qt issue, TextArea fires spurious textChanged events + // NOTE: might be Qt virtual keyboard, or Qt upgrade from 5.15.2 to 5.15.7 + if (root.text != text) + root.text = text + + // update suggestions + _suggestions = bitcoin.mnemonicsFor(seedtextarea.text.split(' ').pop()) + // TODO: cursorPosition only on suggestion apply + cursorPosition = text.length + } + } + Flickable { Layout.preferredWidth: parent.width Layout.minimumHeight: fontMetrics.lineSpacing + 2*constants.paddingXXSmall + 2*constants.paddingXSmall + 2 @@ -70,38 +103,6 @@ Pane { } } - TextArea { - id: seedtextarea - Layout.fillWidth: true - Layout.minimumHeight: fontMetrics.height * 3 + topPadding + bottomPadding - - rightPadding: constants.paddingLarge - leftPadding: constants.paddingLarge - - wrapMode: TextInput.WordWrap - font.bold: true - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - inputMethodHints: Qt.ImhSensitiveData | Qt.ImhLowercaseOnly | Qt.ImhNoPredictiveText - readOnly: true - - background: Rectangle { - color: constants.darkerBackground - } - - onTextChanged: { - // work around Qt issue, TextArea fires spurious textChanged events - // NOTE: might be Qt virtual keyboard, or Qt upgrade from 5.15.2 to 5.15.7 - if (root.text != text) - root.text = text - - // update suggestions - _suggestions = bitcoin.mnemonicsFor(seedtextarea.text.split(' ').pop()) - // TODO: cursorPosition only on suggestion apply - cursorPosition = text.length - } - } - SeedKeyboard { id: kbd Layout.fillWidth: true From 8cd26820bf8733cee7181778d3d8b173227de7fc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 2 May 2023 13:56:55 +0200 Subject: [PATCH 0857/1143] qml: styling seedtextarea --- .../qml/components/controls/SeedTextArea.qml | 19 +++++++++++++++ .../qml/components/wizard/WCConfirmSeed.qml | 2 +- .../gui/qml/components/wizard/WCHaveSeed.qml | 23 ++++--------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qml/components/controls/SeedTextArea.qml b/electrum/gui/qml/components/controls/SeedTextArea.qml index 1681cad5d..4115096bd 100644 --- a/electrum/gui/qml/components/controls/SeedTextArea.qml +++ b/electrum/gui/qml/components/controls/SeedTextArea.qml @@ -13,6 +13,8 @@ Pane { property string text property bool readOnly: false property alias placeholderText: seedtextarea.placeholderText + property string indicatorText + property bool indicatorValid property var _suggestions: [] @@ -60,6 +62,23 @@ Pane { // TODO: cursorPosition only on suggestion apply cursorPosition = text.length } + + Rectangle { + anchors.fill: contentText + color: root.indicatorValid ? 'green' : 'red' + border.color: Material.accentColor + radius: 2 + } + Label { + id: contentText + text: root.indicatorText + anchors.right: parent.right + anchors.bottom: parent.bottom + leftPadding: root.indicatorText != '' ? constants.paddingLarge : 0 + rightPadding: root.indicatorText != '' ? constants.paddingLarge : 0 + font.bold: false + font.pixelSize: constants.fontSizeSmall + } } Flickable { diff --git a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml index ac7a0c8ff..f67739733 100644 --- a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml +++ b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml @@ -28,13 +28,13 @@ WizardComponent { InfoTextArea { Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge text: qsTr('Your seed is important!') + ' ' + qsTr('If you lose your seed, your money will be permanently lost.') + ' ' + qsTr('To make sure that you have properly saved your seed, please retype it here.') } Label { - Layout.topMargin: constants.paddingMedium text: qsTr('Confirm your seed (re-enter)') } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index f8bd211e6..59559bde0 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -163,6 +163,7 @@ WizardComponent { id: infotext Layout.fillWidth: true Layout.columnSpan: 2 + Layout.bottomMargin: constants.paddingLarge } SeedTextArea { @@ -172,25 +173,11 @@ WizardComponent { placeholderText: cosigner ? qsTr('Enter cosigner seed') : qsTr('Enter your seed') + indicatorValid: root.valid + onTextChanged: { startValidationTimer() } - - Rectangle { - anchors.fill: contentText - color: root.valid ? 'green' : 'red' - border.color: Material.accentColor - radius: 2 - } - Label { - id: contentText - anchors.right: parent.right - anchors.bottom: parent.bottom - leftPadding: text != '' ? constants.paddingLarge : 0 - rightPadding: text != '' ? constants.paddingLarge : 0 - font.bold: false - font.pixelSize: constants.fontSizeSmall - } } TextArea { id: validationtext @@ -222,13 +209,13 @@ WizardComponent { Bitcoin { id: bitcoin - onSeedTypeChanged: contentText.text = bitcoin.seedType + onSeedTypeChanged: seedtext.indicatorText = bitcoin.seedType onValidationMessageChanged: validationtext.text = validationMessage } function startValidationTimer() { valid = false - contentText.text = '' + seedtext.indicatorText = '' validationTimer.restart() } From 0672ea20abd833feabb7fd0efa22ced99393ee1a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 29 Apr 2023 13:45:28 +0200 Subject: [PATCH 0858/1143] qml: implement toggle for android SECURE_FLAG and add marker to wizard pages that should be secured. --- contrib/android/Dockerfile | 2 +- .../gui/qml/components/wizard/WCConfirmSeed.qml | 2 ++ .../gui/qml/components/wizard/WCCreateSeed.qml | 2 ++ .../gui/qml/components/wizard/WCHaveMasterKey.qml | 1 + electrum/gui/qml/components/wizard/WCHaveSeed.qml | 1 + electrum/gui/qml/components/wizard/WCImport.qml | 1 + electrum/gui/qml/components/wizard/Wizard.qml | 7 +++++++ .../gui/qml/components/wizard/WizardComponent.qml | 1 + electrum/gui/qml/qeapp.py | 14 ++++++++++++++ 9 files changed, 30 insertions(+), 1 deletion(-) diff --git a/contrib/android/Dockerfile b/contrib/android/Dockerfile index 35cd9ed90..08e887caf 100644 --- a/contrib/android/Dockerfile +++ b/contrib/android/Dockerfile @@ -180,7 +180,7 @@ RUN cd /opt \ && git remote add accumulator https://github.com/accumulator/python-for-android \ && git fetch --all \ # commit: from branch accumulator/electrum_20210421d (note: careful with force-pushing! see #8162) - && git checkout "087fc3c583d46bfb2dec54878ddea508afb27de6^{commit}" \ + && git checkout "052b9f7945bae557347fa4a4b418040d9da9eaff^{commit}" \ && python3 -m pip install --no-build-isolation --no-dependencies --user -e . # build env vars diff --git a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml index ac7a0c8ff..b5979b290 100644 --- a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml +++ b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml @@ -8,6 +8,8 @@ import ".." import "../controls" WizardComponent { + securePage: true + valid: false function checkValid() { diff --git a/electrum/gui/qml/components/wizard/WCCreateSeed.qml b/electrum/gui/qml/components/wizard/WCCreateSeed.qml index 905f4c49b..f2e76f230 100644 --- a/electrum/gui/qml/components/wizard/WCCreateSeed.qml +++ b/electrum/gui/qml/components/wizard/WCCreateSeed.qml @@ -7,6 +7,8 @@ import org.electrum 1.0 import "../controls" WizardComponent { + securePage: true + valid: seedtext.text != '' function apply() { diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index 9c8fde40f..cf6452432 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -9,6 +9,7 @@ import "../controls" WizardComponent { id: root + securePage: true valid: false diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index f8bd211e6..6b76bd5b1 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -9,6 +9,7 @@ import "../controls" WizardComponent { id: root + securePage: true valid: false diff --git a/electrum/gui/qml/components/wizard/WCImport.qml b/electrum/gui/qml/components/wizard/WCImport.qml index 8a5aca37f..879a5c652 100644 --- a/electrum/gui/qml/components/wizard/WCImport.qml +++ b/electrum/gui/qml/components/wizard/WCImport.qml @@ -8,6 +8,7 @@ import "../controls" WizardComponent { id: root + securePage: true valid: false diff --git a/electrum/gui/qml/components/wizard/Wizard.qml b/electrum/gui/qml/components/wizard/Wizard.qml index 5df89f842..e034a1489 100644 --- a/electrum/gui/qml/components/wizard/Wizard.qml +++ b/electrum/gui/qml/components/wizard/Wizard.qml @@ -2,6 +2,8 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.1 +import org.electrum 1.0 + import "../controls" ElDialog { @@ -141,6 +143,11 @@ ElDialog { _setWizardData({}) } + Binding { + target: AppController + property: 'secureWindow' + value: pages.contentChildren[pages.currentIndex].securePage + } } ColumnLayout { diff --git a/electrum/gui/qml/components/wizard/WizardComponent.qml b/electrum/gui/qml/components/wizard/WizardComponent.qml index df633c2f3..f7ffd4ec0 100644 --- a/electrum/gui/qml/components/wizard/WizardComponent.qml +++ b/electrum/gui/qml/components/wizard/WizardComponent.qml @@ -11,6 +11,7 @@ Pane { property bool valid property bool last: false property string title: '' + property bool securePage: false leftPadding: constants.paddingXLarge rightPadding: constants.paddingXLarge diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 694700cce..e670f1087 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -67,6 +67,7 @@ class QEAppController(BaseCrashReporter, QObject): sendingBugreport = pyqtSignal() sendingBugreportSuccess = pyqtSignal(str) sendingBugreportFailure = pyqtSignal(str) + secureWindowChanged = pyqtSignal() def __init__(self, qedaemon: 'QEDaemon', plugins: 'Plugins'): BaseCrashReporter.__init__(self, None, None, None) @@ -79,6 +80,7 @@ def __init__(self, qedaemon: 'QEDaemon', plugins: 'Plugins'): self._crash_user_text = '' self._app_started = False self._intent = '' + self._secureWindow = False # set up notification queue and notification_timer self.user_notification_queue = queue.Queue() @@ -295,6 +297,18 @@ def haptic(self): return jview.performHapticFeedback(jHfc.VIRTUAL_KEY) + @pyqtProperty(bool, notify=secureWindowChanged) + def secureWindow(self): + return self._secureWindow + + @secureWindow.setter + def secureWindow(self, secure): + if not self.isAndroid(): + return + if self._secureWindow != secure: + jpythonActivity.setSecureWindow(secure) + self._secureWindow = secure + self.secureWindowChanged.emit() class ElectrumQmlApplication(QGuiApplication): From 7f52415807bc4de2c5e2fc03e06ebf8a4d9ffd57 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 2 May 2023 13:41:47 +0000 Subject: [PATCH 0859/1143] util: add some logging to NetworkJobOnDefaultServer starting/stopping to help debug claims of Synchronizer maybe getting stuck --- electrum/util.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index 29958a595..15da9e832 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1565,8 +1565,20 @@ def _reset(self): self.reset_request_counters() async def _start(self, interface: 'Interface'): + self.logger.debug(f"starting. interface.server={repr(str(interface.server))}") self.interface = interface - await interface.taskgroup.spawn(self._run_tasks(taskgroup=self.taskgroup)) + + taskgroup = self.taskgroup + async def run_tasks_wrapper(): + self.logger.debug(f"starting taskgroup ({hex(id(taskgroup))}).") + try: + await self._run_tasks(taskgroup=taskgroup) + except Exception as e: + self.logger.error(f"taskgroup died ({hex(id(taskgroup))}). exc={e!r}") + raise + finally: + self.logger.debug(f"taskgroup stopped ({hex(id(taskgroup))}).") + await interface.taskgroup.spawn(run_tasks_wrapper) @abstractmethod async def _run_tasks(self, *, taskgroup: OldTaskGroup) -> None: @@ -1579,6 +1591,7 @@ async def _run_tasks(self, *, taskgroup: OldTaskGroup) -> None: raise asyncio.CancelledError() async def stop(self, *, full_shutdown: bool = True): + self.logger.debug(f"stopping. {full_shutdown=}") if full_shutdown: unregister_callback(self._restart) await self.taskgroup.cancel_remaining() From 33d394c9d76acceeffaa3adb032e51bed38dfe87 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 2 May 2023 17:23:07 +0000 Subject: [PATCH 0860/1143] ledger: fix scan_devices for certain devices with multiple interfaces regression from https://github.com/spesmilo/electrum/pull/8041 (4.3.3) maybe fixes https://github.com/spesmilo/electrum/issues/8293 ----- Testing with a "Ledger Nano S", btc app version 1.6.3. btchip-python==0.1.32 ledgercomm==1.1.1 ledger-bitcoin==0.1.1 Trying to scan devices hangs/blocks forever on Linux (ubuntu 22.04). ```patch diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 17c1caca48..ba5ae2e3ee 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -306,17 +306,25 @@ class Ledger_Client(HardwareClientBase, ABC): @staticmethod def construct_new(*args, device: Device, **kwargs) -> 'Ledger_Client': """The 'real' constructor, that automatically decides which subclass to use.""" + _logger.info(f"xxx construct_new(). cp1. {device.path=}. {device=}") if LedgerPlugin.is_hw1(device.product_key): return Ledger_Client_Legacy_HW1(*args, **kwargs, device=device) # for nano S or newer hw, decide which client impl to use based on software/firmware version: + _logger.info(f"xxx construct_new(). cp2.") hid_device = HID() hid_device.path = device.path + _logger.info(f"xxx construct_new(). cp3.") hid_device.open() + _logger.info(f"xxx construct_new(). cp4.") transport = ledger_bitcoin.TransportClient('hid', hid=hid_device) - cl = ledger_bitcoin.createClient(transport, chain=get_chain()) + _logger.info(f"xxx construct_new(). cp5.") + cl = ledger_bitcoin.createClient(transport, chain=get_chain(), debug=True) + _logger.info(f"xxx construct_new(). cp6.") if isinstance(cl, ledger_bitcoin.client.NewClient): + _logger.info(f"xxx construct_new(). cp7. trying Ledger_Client_New") return Ledger_Client_New(hid_device, *args, **kwargs) else: + _logger.info(f"xxx construct_new(). cp7. trying Ledger_Client_Legacy") return Ledger_Client_Legacy(hid_device, *args, **kwargs) def __init__(self, *, plugin: HW_PluginBase): @@ -1416,7 +1424,7 @@ class LedgerPlugin(HW_PluginBase): try: return Ledger_Client.construct_new(device=device, product_key=device.product_key, plugin=self) except Exception as e: - self.logger.info(f"cannot connect at {device.path} {e}") + self.logger.info(f"cannot connect at {device.path} {e}") # return None def setup_device(self, device_info, wizard, purpose): ``` scanning devices freezes... log: ``` 8.94 | I | plugin.DeviceMgr | scanning devices... 9.18 | D | util.profiler | DeviceMgr.scan_devices 0.2357 sec 9.18 | I | plugins.ledger.ledger | xxx construct_new(). cp1. device.path=b'0001:0008:00'. device=Device(path=b'0001:0008:00', interface_number=0, id_="b'0001:0008:00',0001,0,0", product_key=(11415, 4117), usage_page=0, transport_ui_string='hid') 9.18 | I | plugins.ledger.ledger | xxx construct_new(). cp2. 9.18 | I | plugins.ledger.ledger | xxx construct_new(). cp3. heyheyhey. cp1. self.path=b'0001:0008:00' 9.18 | I | plugins.ledger.ledger | xxx construct_new(). cp4. 9.18 | I | plugins.ledger.ledger | xxx construct_new(). cp5. => b001000000 <= 010c426974636f696e205465737405312e362e3301029000 9.22 | I | plugins.ledger.ledger | xxx construct_new(). cp6. 9.22 | I | plugins.ledger.ledger | xxx construct_new(). cp7. trying Ledger_Client_Legacy 9.29 | I | plugin.DeviceMgr | Registering 10.33 | I | plugins.ledger.ledger | xxx construct_new(). cp1. device.path=b'0001:0008:01'. device=Device(path=b'0001:0008:01', interface_number=1, id_="b'0001:0008:01',0001,1,0", product_key=(11415, 4117), usage_page=0, transport_ui_string='hid') 10.33 | I | plugins.ledger.ledger | xxx construct_new(). cp2. 10.33 | I | plugins.ledger.ledger | xxx construct_new(). cp3. heyheyhey. cp1. self.path=b'0001:0008:01' 10.33 | I | plugins.ledger.ledger | xxx construct_new(). cp4. 10.33 | I | plugins.ledger.ledger | xxx construct_new(). cp5. => b001000000 ``` in Qt console (before change): ``` >>> lp = plugins.get_plugin("ledger") >>> plugins.device_manager.scan_devices() [Device(path=b'0001:000a:00', interface_number=0, id_="b'0001:000a:00',0001,0,0", product_key=(11415, 4117), usage_page=0, transport_ui_string='hid'), Device(path=b'0001:000a:01', interface_number=1, id_="b'0001:000a:01',0001,1,0", product_key=(11415, 4117), usage_page=0, transport_ui_string='hid')] ``` --- electrum/plugins/ledger/ledger.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 17c1caca4..1d17cbf47 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1399,7 +1399,16 @@ def _recognize_device(cls, product_key) -> Tuple[bool, Optional[str]]: return False, None def can_recognize_device(self, device: Device) -> bool: - return self._recognize_device(device.product_key)[0] + can_recognize = self._recognize_device(device.product_key)[0] + if can_recognize: + # Do a further check, duplicated from: + # https://github.com/LedgerHQ/ledgercomm/blob/bc5ada865980cb63c2b9b71a916e01f2f8e53716/ledgercomm/interfaces/hid_device.py#L79-L82 + # Modern ledger devices can have multiple interfaces picked up by hid, only one of which is usable by us. + # If we try communicating with the wrong one, we might not get a reply and block forever. + if device.product_key[0] == 0x2c97: + if not (device.interface_number == 0 or device.usage_page == 0xffa0): + return False + return can_recognize @classmethod def device_name_from_product_key(cls, product_key) -> Optional[str]: From c8c76a8d6f6fd42105c571b43b4cd46cc6eb4eee Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 2 May 2023 20:16:36 +0200 Subject: [PATCH 0861/1143] qml: fix var ref --- electrum/gui/qml/components/controls/SeedKeyboard.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/SeedKeyboard.qml b/electrum/gui/qml/components/controls/SeedKeyboard.qml index 15b87ab28..750e9ceb8 100644 --- a/electrum/gui/qml/components/controls/SeedKeyboard.qml +++ b/electrum/gui/qml/components/controls/SeedKeyboard.qml @@ -11,8 +11,8 @@ Item { property int hpadding: 0 property int vpadding: 15 - property int keywidth: (root.width - 2 * padding) / 10 - keyhspacing - property int keyheight: (root.height - 2 * padding) / 4 - keyvspacing + property int keywidth: (root.width - 2 * hpadding) / 10 - keyhspacing + property int keyheight: (root.height - 2 * vpadding) / 4 - keyvspacing property int keyhspacing: 4 property int keyvspacing: 5 From 3bdda3a8616fff8a1d53c4168e54a688f4961994 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 May 2023 12:18:34 +0000 Subject: [PATCH 0862/1143] config: force 'history_rates' config var to bool fixes https://github.com/spesmilo/electrum/issues/8367 probably regression from 503776c0dee2a544c0d746e9b853f29fe0b54279 (note that before that commit, we were casting to bool) --- electrum/exchange_rate.py | 4 ++-- electrum/gui/qml/qefx.py | 2 +- electrum/gui/qt/history_list.py | 2 +- electrum/gui/qt/my_treeview.py | 2 +- electrum/gui/qt/settings_dialog.py | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 95adab7af..ea0a79ca7 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -592,8 +592,8 @@ def set_enabled(self, b): def can_have_history(self): return self.is_enabled() and self.ccy in self.exchange.history_ccys() - def has_history(self): - return self.can_have_history() and self.config.get('history_rates', False) + def has_history(self) -> bool: + return self.can_have_history() and bool(self.config.get('history_rates', False)) def get_currency(self) -> str: '''Use when dynamic fetching is needed''' diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index 8a226e2d5..ce69cc304 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -63,7 +63,7 @@ def fiatCurrency(self, currency): historicRatesChanged = pyqtSignal() @pyqtProperty(bool, notify=historicRatesChanged) def historicRates(self): - return self.fx.config.get('history_rates', True) + return bool(self.fx.config.get('history_rates', True)) @historicRates.setter def historicRates(self, checked): diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index cb4a2383f..284fac817 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -252,7 +252,7 @@ def should_include_lightning_payments(self) -> bool: return True def should_show_fiat(self): - if not self.window.config.get('history_rates', False): + if not bool(self.window.config.get('history_rates', False)): return False fx = self.window.fx if not fx or not fx.is_enabled(): diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index 13918b075..b10674fde 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -83,7 +83,7 @@ def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=No b = self.config.get(name, default) m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback)) m.setCheckable(True) - m.setChecked(b) + m.setChecked(bool(b)) m.setToolTip(tooltip) return m diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 4fa16734f..4a4f59759 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -307,7 +307,7 @@ def on_be_edit(): def update_currencies(): if not self.fx: return - h = self.config.get('history_rates', False) + h = bool(self.config.get('history_rates', False)) currencies = sorted(self.fx.get_currencies(h)) ccy_combo.clear() ccy_combo.addItems([_('None')] + currencies) @@ -319,7 +319,7 @@ def update_exchanges(): b = self.fx.is_enabled() ex_combo.setEnabled(b) if b: - h = self.config.get('history_rates', False) + h = bool(self.config.get('history_rates', False)) c = self.fx.get_currency() exchanges = self.fx.get_exchanges_by_ccy(c, h) else: @@ -356,7 +356,7 @@ def on_history_rates(checked): update_currencies() update_exchanges() ccy_combo.currentIndexChanged.connect(on_currency) - self.history_rates_cb.setChecked(self.config.get('history_rates', False)) + self.history_rates_cb.setChecked(bool(self.config.get('history_rates', False))) self.history_rates_cb.stateChanged.connect(on_history_rates) ex_combo.currentIndexChanged.connect(on_exchange) From 53d61c011a901b645d74e10e7eaa0014492a7199 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 May 2023 13:49:44 +0000 Subject: [PATCH 0863/1143] qml network: restrict cases where server is shown "lagging" --- electrum/gui/qml/components/NetworkOverview.qml | 4 ++-- electrum/network.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/NetworkOverview.qml b/electrum/gui/qml/components/NetworkOverview.qml index 1e9d69e9e..f30fcb747 100644 --- a/electrum/gui/qml/components/NetworkOverview.qml +++ b/electrum/gui/qml/components/NetworkOverview.qml @@ -69,11 +69,11 @@ Pane { Label { text: qsTr('Server Height:'); color: Material.accentColor - visible: Network.serverHeight != Network.height + visible: Network.serverHeight != 0 && Network.serverHeight < Network.height } Label { text: Network.serverHeight + " (lagging)" - visible: Network.serverHeight != Network.height + visible: Network.serverHeight != 0 && Network.serverHeight < Network.height } Heading { Layout.columnSpan: 2 diff --git a/electrum/network.py b/electrum/network.py index e12f28a62..90d66a344 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -1218,7 +1218,7 @@ def get_server_height(self) -> int: interface = self.interface return interface.tip if interface else 0 - def get_local_height(self): + def get_local_height(self) -> int: """Length of header chain, POW-verified. In case of a chain split, this is for the branch the main interface is on, but it is the tip of that branch (even if main interface is behind). From 5512c7d905b5d17374201e9ad5e04d02b68b9fd7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 May 2023 14:15:08 +0000 Subject: [PATCH 0864/1143] wallet.get_tx_info: distinguish "future" tx from local in "status" str closes https://github.com/spesmilo/electrum/issues/8379 --- electrum/wallet.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index d31e10261..653af5ba6 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -846,6 +846,10 @@ def get_tx_info(self, tx: Transaction) -> TxWalletDetails: can_cpfp = False else: status = _('Local') + if tx_mined_status.height == TX_HEIGHT_FUTURE: + num_blocks_remainining = tx_mined_status.wanted_height - self.adb.get_local_height() + num_blocks_remainining = max(0, num_blocks_remainining) + status = _('Local (future: {})').format(_('in {} blocks').format(num_blocks_remainining)) can_broadcast = self.network is not None can_bump = (is_any_input_ismine or is_swap) and not tx.is_final() else: @@ -1517,11 +1521,11 @@ def get_tx_status(self, tx_hash, tx_mined_info: TxMinedInfo): if height == TX_HEIGHT_FUTURE: num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height() num_blocks_remainining = max(0, num_blocks_remainining) - return 2, f'in {num_blocks_remainining} blocks' + return 2, _('in {} blocks').format(num_blocks_remainining) if conf == 0: tx = self.db.get_transaction(tx_hash) if not tx: - return 2, 'unknown' + return 2, _("unknown") is_final = tx and tx.is_final() fee = self.adb.get_tx_fee(tx_hash) if fee is not None: From a1bfea6121edce58b7d8229ba65632c11a8e2a61 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 May 2023 15:06:37 +0000 Subject: [PATCH 0865/1143] wallet.get_tx_parents: fix: tx should not be its own uncle ``` 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp0. entered for txid='407d03126255cce62a1101075db906587bd492f512166119d3f87b8a1b013497' 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp4.1. parents=['e2d915520f6d42273158a6fd08b38d812bd554aa906d3ed45d103757d45af2bb'] 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp4.2. uncles=[] 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp2. populating cache... _txid='434808dab0c93715bb8b7ce85f73bffd0bdf7c1ba205fe0f704226646971e555' 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp0. entered for txid='434808dab0c93715bb8b7ce85f73bffd0bdf7c1ba205fe0f704226646971e555' 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp3.0. uncle_txid='434808dab0c93715bb8b7ce85f73bffd0bdf7c1ba205fe0f704226646971e555' 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp3.1. reuse_height=-2 reuse_pos=-1 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp3.2. my_height=1338 my_pos=1 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp4.1. parents=['407d03126255cce62a1101075db906587bd492f512166119d3f87b8a1b013497'] 3.06 | I | w/wallet.Standard_Wallet.[default_wallet] | get_tx_parents() cp4.2. uncles=['434808dab0c93715bb8b7ce85f73bffd0bdf7c1ba205fe0f704226646971e555'] 40.82 | E | gui.qt.exception_window.Exception_Hook | exception caught by crash reporter Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 846, in timer_actions self.update_wallet() File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1001, in update_wallet self.update_tabs() File "/home/user/wspace/electrum/electrum/gui/qt/main_window.py", line 1013, in update_tabs self.utxo_list.update() File "/home/user/wspace/electrum/electrum/util.py", line 470, in return lambda *args, **kw_args: do_profile(args, kw_args) File "/home/user/wspace/electrum/electrum/util.py", line 465, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/gui/qt/utxo_list.py", line 115, in update self.refresh_row(name, idx) File "/home/user/wspace/electrum/electrum/gui/qt/utxo_list.py", line 137, in refresh_row num_parents = self.wallet.get_num_parents(txid) File "/home/user/wspace/electrum/electrum/wallet.py", line 897, in get_num_parents self._num_parents[txid] = len(self.get_tx_parents(txid)) File "/home/user/wspace/electrum/electrum/wallet.py", line 910, in get_tx_parents self.get_tx_parents(_txid) File "/home/user/wspace/electrum/electrum/wallet.py", line 938, in get_tx_parents p = self._tx_parents_cache[_txid] KeyError: '434808dab0c93715bb8b7ce85f73bffd0bdf7c1ba205fe0f704226646971e555' ``` --- electrum/wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/wallet.py b/electrum/wallet.py index 653af5ba6..3b6485999 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -929,6 +929,8 @@ def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: for k, v in sent.items(): if k != txin.prevout.to_str(): reuse_txid, reuse_height, reuse_pos = v + if reuse_height <= 0: # exclude not-yet-mined (we need topological ordering) + continue if (reuse_height, reuse_pos) < (my_height, my_pos): uncle_txid, uncle_index = k.split(':') uncles.append(uncle_txid) From 56fa832563225ab3a4c59c7ee9fc21403d90d1ef Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 May 2023 15:08:56 +0000 Subject: [PATCH 0866/1143] wallet.get_tx_parents: explicitly handle missing "from address" (happened to work even without this) --- electrum/address_synchronizer.py | 2 +- electrum/wallet.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 3e490f8f4..0b8578213 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -787,7 +787,7 @@ def get_tx_fee(self, txid: str) -> Optional[int]: self.db.add_num_inputs_to_tx(txid, len(tx.inputs())) return fee - def get_addr_io(self, address): + def get_addr_io(self, address: str): with self.lock, self.transaction_lock: h = self.get_address_history(address).items() received = {} diff --git a/electrum/wallet.py b/electrum/wallet.py index 3b6485999..b1db5f61e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -922,6 +922,8 @@ def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: parents.append(_txid) # detect address reuse addr = self.adb.get_txin_address(txin) + if addr is None: + continue received, sent = self.adb.get_addr_io(addr) if len(sent) > 1: my_txid, my_height, my_pos = sent[txin.prevout.to_str()] From 0be8beb9c44ef94b4ae859e657c85dd34f2d976f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 3 May 2023 22:41:25 +0200 Subject: [PATCH 0867/1143] qml: add secure flag to WalletDetails, set when seed is shown --- electrum/gui/qml/components/WalletDetails.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index 5ec636eda..f0e316712 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -512,4 +512,10 @@ Pane { } } + Binding { + target: AppController + property: 'secureWindow' + value: seedText.visible + } + } From 2eaf3dcc642de9cb45a9b0fa4d766b08e94c541a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 3 May 2023 22:50:58 +0200 Subject: [PATCH 0868/1143] qml: cleanup --- electrum/gui/qml/components/WalletDetails.qml | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index f0e316712..b8d76bfc6 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -16,19 +16,16 @@ Pane { property bool _is2fa: Daemon.currentWallet && Daemon.currentWallet.walletType == '2fa' function enableLightning() { - var dialog = app.messageDialog.createObject(rootItem, - {'title': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) + var dialog = app.messageDialog.createObject(rootItem, { + title: qsTr('Enable Lightning for this wallet?'), + yesno: true + }) dialog.accepted.connect(function() { Daemon.currentWallet.enableLightning() }) dialog.open() } - function changePassword() { - // trigger dialog via wallet (auth then signal) - Daemon.startChangePassword() - } - function importAddressesKeys() { var dialog = importAddressesKeysDialog.createObject(rootItem) dialog.open() @@ -44,7 +41,7 @@ Pane { Layout.fillHeight: true contentHeight: flickableRoot.height - clip:true + clip: true interactive: height < contentHeight Pane { @@ -416,7 +413,7 @@ Pane { Layout.fillWidth: true Layout.preferredWidth: 1 text: qsTr('Change Password') - onClicked: rootItem.changePassword() + onClicked: Daemon.startChangePassword() icon.source: '../../icons/lock.png' } FlatButton { @@ -466,19 +463,30 @@ Pane { } function onWalletDeleteError(code, message) { if (code == 'unpaid_requests') { - var dialog = app.messageDialog.createObject(app, {title: qsTr('Error'), text: message, yesno: true }) + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Error'), + text: message, + yesno: true + }) dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet, true) }) dialog.open() } else if (code == 'balance') { - var dialog = app.messageDialog.createObject(app, {title: qsTr('Error'), text: message, yesno: true }) + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Error'), + text: message, + yesno: true + }) dialog.accepted.connect(function() { Daemon.checkThenDeleteWallet(Daemon.currentWallet, true, true) }) dialog.open() } else { - var dialog = app.messageDialog.createObject(app, {title: qsTr('Error'), text: message }) + var dialog = app.messageDialog.createObject(app, { + title: qsTr('Error'), + text: message + }) dialog.open() } } From 3b31e68a866d7011e51b8eb9407848b7eaf17354 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 May 2023 01:11:35 +0200 Subject: [PATCH 0869/1143] update release notes --- RELEASE-NOTES | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 4ee585fba..0da2c64df 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,21 @@ +# Release 4.4.2 (unreleased) + * Qt GUI: + - fix undefined var check in swap_dialog (#8341) + - really fix "recursion depth exceeded" for utxo privacy analysis (#8315) + * QML GUI: + - fix 2fa callback issue (#8368) + - properly delete wizard components after use (#8357) + - avoid entering loadWallet if daemon is already busy loading (#8355) + - no auto capitalization on import and master key text fields (5600375d) + - remove Qt virtual keyboard and add Seedkeyboard for seed entry (#8371, #8352) + - add runtime toggling of android SECURE_FLAG (#8351) + - restrict cases where server is shown "lagging" (53d61c01) + * fix hardened char "h" vs "'" needed for some hw wallets (#8364, 499f5153) + * fix digitalbitbox(1) support (22b8c4e3) + * fix wrong type for "history_rates" config option (#8367) + * fix issues with wallet.get_tx_parents (a1bfea61, 56fa8325) + + # Release 4.4.1 (April 27, 2023) * Qt GUI: - fix sweeping (#8340) From ae12d236b297b74afc9566625cda1b14aa477a11 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 May 2023 01:14:40 +0200 Subject: [PATCH 0870/1143] qml: fix broadcastFailed signal connections in txdetails (fixes #8384) --- electrum/gui/qml/qetxdetails.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 8ff2cc6b2..914ae96eb 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -322,7 +322,7 @@ def _sign(self, broadcast): try: if broadcast: self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) - self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) + self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed) except Exception: pass @@ -343,7 +343,7 @@ def broadcast(self): assert self._tx.is_complete() try: - self._wallet.broadcastfailed.disconnect(self.onBroadcastFailed) + self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed) except Exception: pass self._wallet.broadcastFailed.connect(self.onBroadcastFailed) @@ -359,7 +359,10 @@ def onBroadcastSucceeded(self, txid): return self._logger.debug('onBroadcastSucceeded') - self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) + try: + self._wallet.broadcastSucceeded.disconnect(self.onBroadcastSucceeded) + except Exception: + pass self._can_broadcast = False self.detailsChanged.emit() From 325a1bbba6e63f57b0efe00389bfcb233ebe2350 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 May 2023 01:17:29 +0200 Subject: [PATCH 0871/1143] followup ae12d236b297b74afc9566625cda1b14aa477a11 --- electrum/gui/qml/qetxdetails.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 914ae96eb..372d75823 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -372,7 +372,10 @@ def onBroadcastFailed(self, txid, code, reason): if txid != self._txid: return - self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed) + try: + self._wallet.broadcastFailed.disconnect(self.onBroadcastFailed) + except Exception: + pass self._can_broadcast = True self.detailsChanged.emit() From 2732a82535823ebc93b00929f4151ec22aeb02e7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 May 2023 01:20:00 +0000 Subject: [PATCH 0872/1143] build: update pinned pyqt5 (partial rerun freeze_packages) User at https://bitcointalk.org/index.php?topic=5450976.0 reported getting a segfault on macos using qt gui, after entering password to open wallet. Shot in the dark, but let's try updating Qt. --- .../requirements-binaries-mac.txt | 56 +++++++++---------- .../requirements-binaries.txt | 56 +++++++++---------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/contrib/deterministic-build/requirements-binaries-mac.txt b/contrib/deterministic-build/requirements-binaries-mac.txt index 9f238fb20..2c66e3f6d 100644 --- a/contrib/deterministic-build/requirements-binaries-mac.txt +++ b/contrib/deterministic-build/requirements-binaries-mac.txt @@ -96,39 +96,39 @@ pip==22.3.1 \ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 -PyQt5==5.15.7 \ - --hash=sha256:08694f0a4c7d4f3d36b2311b1920e6283240ad3b7c09b515e08262e195dcdf37 \ - --hash=sha256:1a793748c60d5aff3850b7abf84d47c1d41edb11231b7d7c16bef602c36be643 \ - --hash=sha256:232fe5b135a095cbd024cf341d928fc672c963f88e6a52b0c605be8177c2fdb5 \ - --hash=sha256:755121a52b3a08cb07275c10ebb96576d36e320e572591db16cfdbc558101594 \ - --hash=sha256:e319c9d8639e0729235c1b09c99afdadad96fa3dbd8392ab561b5ab5946ee6ef +PyQt5==5.15.9 \ + --hash=sha256:883ba5c8a348be78c8be6a3d3ba014c798e679503bce00d76c666c2dc6afe828 \ + --hash=sha256:dc41e8401a90dc3e2b692b411bd5492ab559ae27a27424eed4bd3915564ec4c0 \ + --hash=sha256:dd5ce10e79fbf1df29507d2daf99270f2057cdd25e4de6fbf2052b46c652e3a5 \ + --hash=sha256:e030d795df4cbbfcf4f38b18e2e119bcc9e177ef658a5094b87bb16cac0ce4c5 \ + --hash=sha256:e45c5cc15d4fd26ab5cb0e5cdba60691a3e9086411f8e3662db07a5a4222a696 PyQt5-Qt5==5.15.2 \ --hash=sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a \ --hash=sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962 \ --hash=sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154 \ --hash=sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327 -PyQt5-sip==12.11.0 \ - --hash=sha256:0f77655c62ec91d47c2c99143f248624d44dd2d8a12d016e7c020508ad418aca \ - --hash=sha256:205f3e1b3eea3597d8e878936c1a06e04bd23a59e8b179ee806465d72eea3071 \ - --hash=sha256:3126c84568ab341c12e46ded2230f62a9a78752a70fdab13713f89a71cd44f73 \ - --hash=sha256:4031547dfb679be309094bfa79254f5badc5ddbe66b9ad38e319d84a7d612443 \ - --hash=sha256:42320e7a94b1085ed85d49794ed4ccfe86f1cae80b44a894db908a8aba2bc60e \ - --hash=sha256:43dfe6dd409e713edeb67019b85419a7a0dc9923bfc451d6cf3778471c122532 \ - --hash=sha256:4e5c1559311515291ea0ab0635529f14536954e3b973a7c7890ab7e4de1c2c23 \ - --hash=sha256:51e377789d59196213eddf458e6927f33ba9d217b614d17d20df16c9a8b2c41c \ - --hash=sha256:686071be054e5be6ca5aaaef7960931d4ba917277e839e2e978c7cbe3f43bb6e \ - --hash=sha256:9356260d4feb60dbac0ab66f8a791a0d2cda1bf98c9dec8e575904a045fbf7c5 \ - --hash=sha256:9bca450c5306890cb002fe36bbca18f979dd9e5b810b766dce8e3ce5e66ba795 \ - --hash=sha256:ad21ca0ee8cae2a41b61fc04949dccfab6fe008749627d94e8c7078cb7a73af1 \ - --hash=sha256:afa4ffffc54e306669bf2b66ea37abbc56c5cdda4f3f474d20324e3634302b12 \ - --hash=sha256:b4710fd85b57edef716cc55fae45bfd5bfac6fc7ba91036f1dcc3f331ca0eb39 \ - --hash=sha256:b69a1911f768b489846335e31e49eb34795c6b5a038ca24d894d751e3b0b44da \ - --hash=sha256:bd733667098cac70e89279d9c239106d543fb480def62a44e6366ccb8f68510b \ - --hash=sha256:d12b81c3a08abf7657a2ebc7d3649852a1f327eb2146ebadf45930486d32e920 \ - --hash=sha256:ec1d8ce50be76c5c1d1c86c6dc0ccacc2446172dde98b663a17871f532f9bd44 \ - --hash=sha256:ec5e9ef78852e1f96f86d7e15c9215878422b83dde36d44f1539a3062942f19c \ - --hash=sha256:f1f9e312ff8284d6dfebc5366f6f7d103f84eec23a4da0be0482403933e68660 \ - --hash=sha256:f6b72035da4e8fecbb0bc4a972e30a5674a9ad5608dbddaa517e983782dbf3bf +PyQt5-sip==12.12.1 \ + --hash=sha256:0e30f6c9b99161d8a524d8b7aa5a001be5fe002797151c27414066b838beaa4e \ + --hash=sha256:1364f460ae07fc2f4c42dd7a3b3738611b29f5c033025e5e70b03e2687d4bda4 \ + --hash=sha256:2a344855b9c57d37cf000afa46967961122fb1867faee4f53054ebaa1ce51e24 \ + --hash=sha256:2e3d444f5cb81261c22e7c9d3a9a4484bb9db7a1a3077559100175d36297d1da \ + --hash=sha256:3dad0b2bbe0bae4916e43610186d425cd186469b2e6c7ff853177c113b6af6ed \ + --hash=sha256:51720277a53d99bac0914fb970970c9c2ec1a6ab3b7cc5580909d37d9cc6b152 \ + --hash=sha256:59229c8d30141220a472ba4e4212846e8bf0bed84c32cbeb57f70fe727c6dfc2 \ + --hash=sha256:5a9cbcfe8c15d3a34ef33570f0b0130b8ba68b98fd6ec92c28202b186f3ab870 \ + --hash=sha256:644310dcfed4373075bc576717eea60b0f49899beb5cffb204ddaf5f27cddb85 \ + --hash=sha256:6cb6139b00e347e7d961467d092e67c47a97893bc6ab83104bcaf50bf4815036 \ + --hash=sha256:7afc6ec06e79a3e0a7b447e28ef46dad372bdca32e7eff0dcbac6bc53b69a070 \ + --hash=sha256:7ed598ff1b666f9e5e0214be7840f308f8fb347fe416a2a45fbedab046a7120b \ + --hash=sha256:8a2e48a331024a225128f94f5d0fb8089e924419693b2e03eda4d5dbc4313b52 \ + --hash=sha256:8fdc6e0148abd12d977a1d3828e7b79aae958e83c6cb5adae614916d888a6b10 \ + --hash=sha256:9e21e11eb6fb468affe0d72ff922788c2adc124480bb274941fce93ddb122b8f \ + --hash=sha256:a65f5869f3f35330c920c1b218319140c0b84f8c49a20727b5e3df2acd496833 \ + --hash=sha256:b5b7a6c76fe3eb6b245ac6599c807b18e9a718167878a0b547db1d071a914c08 \ + --hash=sha256:d1378815b15198ce6dddd367fbd81f5c018ce473a89ae938b7a58e1d97f25b10 \ + --hash=sha256:e0241b62f5ca9aaff1037f12e6f5ed68168e7200e14e73f05b632381cee0ff4b \ + --hash=sha256:eee684532876775e1d0fa20d4aae1b568aaa6c732d74e6657ee832e427d46947 \ + --hash=sha256:f5ac060219c127a5b9009a4cfe33086e36c6bb8e26c0b757b31a6c04d29d630d setuptools==65.5.1 \ --hash=sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31 \ --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f diff --git a/contrib/deterministic-build/requirements-binaries.txt b/contrib/deterministic-build/requirements-binaries.txt index 9f238fb20..2c66e3f6d 100644 --- a/contrib/deterministic-build/requirements-binaries.txt +++ b/contrib/deterministic-build/requirements-binaries.txt @@ -96,39 +96,39 @@ pip==22.3.1 \ pycparser==2.21 \ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 -PyQt5==5.15.7 \ - --hash=sha256:08694f0a4c7d4f3d36b2311b1920e6283240ad3b7c09b515e08262e195dcdf37 \ - --hash=sha256:1a793748c60d5aff3850b7abf84d47c1d41edb11231b7d7c16bef602c36be643 \ - --hash=sha256:232fe5b135a095cbd024cf341d928fc672c963f88e6a52b0c605be8177c2fdb5 \ - --hash=sha256:755121a52b3a08cb07275c10ebb96576d36e320e572591db16cfdbc558101594 \ - --hash=sha256:e319c9d8639e0729235c1b09c99afdadad96fa3dbd8392ab561b5ab5946ee6ef +PyQt5==5.15.9 \ + --hash=sha256:883ba5c8a348be78c8be6a3d3ba014c798e679503bce00d76c666c2dc6afe828 \ + --hash=sha256:dc41e8401a90dc3e2b692b411bd5492ab559ae27a27424eed4bd3915564ec4c0 \ + --hash=sha256:dd5ce10e79fbf1df29507d2daf99270f2057cdd25e4de6fbf2052b46c652e3a5 \ + --hash=sha256:e030d795df4cbbfcf4f38b18e2e119bcc9e177ef658a5094b87bb16cac0ce4c5 \ + --hash=sha256:e45c5cc15d4fd26ab5cb0e5cdba60691a3e9086411f8e3662db07a5a4222a696 PyQt5-Qt5==5.15.2 \ --hash=sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a \ --hash=sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962 \ --hash=sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154 \ --hash=sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327 -PyQt5-sip==12.11.0 \ - --hash=sha256:0f77655c62ec91d47c2c99143f248624d44dd2d8a12d016e7c020508ad418aca \ - --hash=sha256:205f3e1b3eea3597d8e878936c1a06e04bd23a59e8b179ee806465d72eea3071 \ - --hash=sha256:3126c84568ab341c12e46ded2230f62a9a78752a70fdab13713f89a71cd44f73 \ - --hash=sha256:4031547dfb679be309094bfa79254f5badc5ddbe66b9ad38e319d84a7d612443 \ - --hash=sha256:42320e7a94b1085ed85d49794ed4ccfe86f1cae80b44a894db908a8aba2bc60e \ - --hash=sha256:43dfe6dd409e713edeb67019b85419a7a0dc9923bfc451d6cf3778471c122532 \ - --hash=sha256:4e5c1559311515291ea0ab0635529f14536954e3b973a7c7890ab7e4de1c2c23 \ - --hash=sha256:51e377789d59196213eddf458e6927f33ba9d217b614d17d20df16c9a8b2c41c \ - --hash=sha256:686071be054e5be6ca5aaaef7960931d4ba917277e839e2e978c7cbe3f43bb6e \ - --hash=sha256:9356260d4feb60dbac0ab66f8a791a0d2cda1bf98c9dec8e575904a045fbf7c5 \ - --hash=sha256:9bca450c5306890cb002fe36bbca18f979dd9e5b810b766dce8e3ce5e66ba795 \ - --hash=sha256:ad21ca0ee8cae2a41b61fc04949dccfab6fe008749627d94e8c7078cb7a73af1 \ - --hash=sha256:afa4ffffc54e306669bf2b66ea37abbc56c5cdda4f3f474d20324e3634302b12 \ - --hash=sha256:b4710fd85b57edef716cc55fae45bfd5bfac6fc7ba91036f1dcc3f331ca0eb39 \ - --hash=sha256:b69a1911f768b489846335e31e49eb34795c6b5a038ca24d894d751e3b0b44da \ - --hash=sha256:bd733667098cac70e89279d9c239106d543fb480def62a44e6366ccb8f68510b \ - --hash=sha256:d12b81c3a08abf7657a2ebc7d3649852a1f327eb2146ebadf45930486d32e920 \ - --hash=sha256:ec1d8ce50be76c5c1d1c86c6dc0ccacc2446172dde98b663a17871f532f9bd44 \ - --hash=sha256:ec5e9ef78852e1f96f86d7e15c9215878422b83dde36d44f1539a3062942f19c \ - --hash=sha256:f1f9e312ff8284d6dfebc5366f6f7d103f84eec23a4da0be0482403933e68660 \ - --hash=sha256:f6b72035da4e8fecbb0bc4a972e30a5674a9ad5608dbddaa517e983782dbf3bf +PyQt5-sip==12.12.1 \ + --hash=sha256:0e30f6c9b99161d8a524d8b7aa5a001be5fe002797151c27414066b838beaa4e \ + --hash=sha256:1364f460ae07fc2f4c42dd7a3b3738611b29f5c033025e5e70b03e2687d4bda4 \ + --hash=sha256:2a344855b9c57d37cf000afa46967961122fb1867faee4f53054ebaa1ce51e24 \ + --hash=sha256:2e3d444f5cb81261c22e7c9d3a9a4484bb9db7a1a3077559100175d36297d1da \ + --hash=sha256:3dad0b2bbe0bae4916e43610186d425cd186469b2e6c7ff853177c113b6af6ed \ + --hash=sha256:51720277a53d99bac0914fb970970c9c2ec1a6ab3b7cc5580909d37d9cc6b152 \ + --hash=sha256:59229c8d30141220a472ba4e4212846e8bf0bed84c32cbeb57f70fe727c6dfc2 \ + --hash=sha256:5a9cbcfe8c15d3a34ef33570f0b0130b8ba68b98fd6ec92c28202b186f3ab870 \ + --hash=sha256:644310dcfed4373075bc576717eea60b0f49899beb5cffb204ddaf5f27cddb85 \ + --hash=sha256:6cb6139b00e347e7d961467d092e67c47a97893bc6ab83104bcaf50bf4815036 \ + --hash=sha256:7afc6ec06e79a3e0a7b447e28ef46dad372bdca32e7eff0dcbac6bc53b69a070 \ + --hash=sha256:7ed598ff1b666f9e5e0214be7840f308f8fb347fe416a2a45fbedab046a7120b \ + --hash=sha256:8a2e48a331024a225128f94f5d0fb8089e924419693b2e03eda4d5dbc4313b52 \ + --hash=sha256:8fdc6e0148abd12d977a1d3828e7b79aae958e83c6cb5adae614916d888a6b10 \ + --hash=sha256:9e21e11eb6fb468affe0d72ff922788c2adc124480bb274941fce93ddb122b8f \ + --hash=sha256:a65f5869f3f35330c920c1b218319140c0b84f8c49a20727b5e3df2acd496833 \ + --hash=sha256:b5b7a6c76fe3eb6b245ac6599c807b18e9a718167878a0b547db1d071a914c08 \ + --hash=sha256:d1378815b15198ce6dddd367fbd81f5c018ce473a89ae938b7a58e1d97f25b10 \ + --hash=sha256:e0241b62f5ca9aaff1037f12e6f5ed68168e7200e14e73f05b632381cee0ff4b \ + --hash=sha256:eee684532876775e1d0fa20d4aae1b568aaa6c732d74e6657ee832e427d46947 \ + --hash=sha256:f5ac060219c127a5b9009a4cfe33086e36c6bb8e26c0b757b31a6c04d29d630d setuptools==65.5.1 \ --hash=sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31 \ --hash=sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f From 97650399cf281783438850451525c4598f0039ea Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 May 2023 10:11:30 +0000 Subject: [PATCH 0873/1143] qml: always ask for the password on wallet-open, even for ks-enc-only wallets This is a hugely hackish -- it uses the kivy approach, which uses this same hack... I am not really content with it but it should be relatively easy to review, and if ok, should hotfix the linked issue. fixes https://github.com/spesmilo/electrum/issues/8374 related https://github.com/spesmilo/electrum/pull/8382 --- electrum/gui/qml/qewallet.py | 2 +- electrum/gui/qml/qewalletdb.py | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 72cba6b7f..6a07ecc27 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -668,7 +668,7 @@ def deleteInvoice(self, key: str): @pyqtSlot(str, result=bool) def verifyPassword(self, password): try: - self.wallet.storage.check_password(password) + self.wallet.check_password(password) return True except InvalidPassword as e: return False diff --git a/electrum/gui/qml/qewalletdb.py b/electrum/gui/qml/qewalletdb.py index ebc88dc44..45a59947e 100644 --- a/electrum/gui/qml/qewalletdb.py +++ b/electrum/gui/qml/qewalletdb.py @@ -1,14 +1,20 @@ import os +from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.logging import get_logger from electrum.storage import WalletStorage, StorageEncryptionVersion from electrum.wallet_db import WalletDB +from electrum.wallet import Wallet from electrum.bip32 import normalize_bip32_derivation, xpub_type from electrum.util import InvalidPassword, WalletFileException from electrum import keystore +if TYPE_CHECKING: + from electrum.simple_config import SimpleConfig + + class QEWalletDB(QObject): _logger = get_logger(__name__) @@ -29,6 +35,7 @@ def __init__(self, parent=None): from .qeapp import ElectrumQmlApplication self.daemon = ElectrumQmlApplication._daemon + self._config = self.daemon.config # type: SimpleConfig self.reset() @@ -144,9 +151,24 @@ def load_storage(self): except InvalidPassword as e: self.validPassword = False self.invalidPassword.emit() + else: # storage not encrypted; but it might still have a keystore pw + # FIXME hack... load both db and full wallet, just to tell if it has keystore pw. + # this also completely ignores db.requires_split(), db.get_action(), etc + db = WalletDB(self._storage.read(), manual_upgrades=False) + wallet = Wallet(db, self._storage, config=self._config) + self.needsPassword = wallet.has_password() + if self.needsPassword: + try: + wallet.check_password('' if not self._password else self._password) + self.validPassword = True + except InvalidPassword as e: + self.validPassword = False + self._storage = None + self.invalidPassword.emit() - if not self._storage.is_past_initial_decryption(): - self._storage = None + if self._storage: + if not self._storage.is_past_initial_decryption(): + self._storage = None def load_db(self): # needs storage accessible From 397019fe19474e94cb6e3fd9ae6072fc4d8d50a6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 May 2023 12:44:38 +0200 Subject: [PATCH 0874/1143] qml: veriyMasterKey don't raise Exception on unsupported wallet_type, log error and provide user feedback --- electrum/gui/qml/qebitcoin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 274f9cd81..c03eecb17 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -107,7 +107,6 @@ def verifySeed(self, seed, seed_variant, wallet_type='standard'): @pyqtSlot(str, str, result=bool) def verifyMasterKey(self, key, wallet_type='standard'): - # FIXME exceptions raised in here are not well-behaved... self.validationMessage = '' if not keystore.is_master_key(key): self.validationMessage = _('Not a master key') @@ -126,7 +125,10 @@ def verifyMasterKey(self, key, wallet_type='standard'): self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) return False return True - raise Exception(f'Unsupported wallet type: {wallet_type}') + else: + self.validationMessage = '%s: %s' % (_('Unsupported wallet type'), wallet_type) + self.logger.error(f'Unsupported wallet type: {wallet_type}') + return False return True @pyqtSlot(str, result=bool) From 9d11aae39434290eed23353b70bfab5f6691db5b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 4 May 2023 13:26:56 +0200 Subject: [PATCH 0875/1143] qml: take internal, external, billing address colors from desktop client and color output addresses accordingly in ConfirmTxDialog, TxDetails, CpfpBumpFeeDialog, RbfBumpFeeDialog and RbfCancelDialog --- electrum/gui/qml/components/ConfirmTxDialog.qml | 8 +++++++- electrum/gui/qml/components/Constants.qml | 6 +++--- electrum/gui/qml/components/CpfpBumpFeeDialog.qml | 8 +++++++- electrum/gui/qml/components/RbfBumpFeeDialog.qml | 8 +++++++- electrum/gui/qml/components/RbfCancelDialog.qml | 8 +++++++- electrum/gui/qml/components/TxDetails.qml | 8 +++++++- electrum/gui/qml/qetxdetails.py | 6 ++++-- electrum/gui/qml/qetxfinalizer.py | 4 +++- 8 files changed, 45 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index e0a630cb4..48d1e13a0 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -194,7 +194,13 @@ ElDialog { wrapMode: Text.Wrap font.pixelSize: constants.fontSizeLarge font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground + color: modelData.is_mine + ? modelData.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : modelData.is_billing + ? constants.colorAddressBilling + : Material.foreground } Label { text: Config.formatSats(modelData.value_sats) diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 43d1180c5..e66f61183 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -41,7 +41,6 @@ Item { property color colorProgress: '#ffffff80' property color colorDone: '#ff80ff80' - property color colorMine: "yellow" property color colorLightningLocal: "blue" property color colorLightningRemote: "yellow" property color colorChannelOpen: "#ff80ff80" @@ -57,11 +56,12 @@ Item { property color colorPiechartParticipant: 'gray' property color colorPiechartSignature: 'yellow' - property color colorAddressExternal: Qt.rgba(0,1,0,0.5) - property color colorAddressInternal: Qt.rgba(1,0.93,0,0.75) + property color colorAddressExternal: "#8af296" //Qt.rgba(0,1,0,0.5) + property color colorAddressInternal: "#ffff00" //Qt.rgba(1,0.93,0,0.75) property color colorAddressUsed: Qt.rgba(0.5,0.5,0.5,1) property color colorAddressUsedWithBalance: Qt.rgba(0.75,0.75,0.75,1) property color colorAddressFrozen: Qt.rgba(0.5,0.5,1,1) + property color colorAddressBilling: "#8cb3f2" function colorAlpha(baseColor, alpha) { return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, alpha) diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index d0a17cbcd..14fb8ad60 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -197,7 +197,13 @@ ElDialog { wrapMode: Text.Wrap font.pixelSize: constants.fontSizeLarge font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground + color: modelData.is_mine + ? modelData.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : modelData.is_billing + ? constants.colorAddressBilling + : Material.foreground } Label { text: Config.formatSats(modelData.value_sats) diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index cdffa49c3..5939e5e36 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -210,7 +210,13 @@ ElDialog { wrapMode: Text.Wrap font.pixelSize: constants.fontSizeLarge font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground + color: modelData.is_mine + ? modelData.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : modelData.is_billing + ? constants.colorAddressBilling + : Material.foreground } Label { text: Config.formatSats(modelData.value_sats) diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index be2fb067b..aa8b84227 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -169,7 +169,13 @@ ElDialog { wrapMode: Text.Wrap font.pixelSize: constants.fontSizeLarge font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground + color: modelData.is_mine + ? modelData.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : modelData.is_billing + ? constants.colorAddressBilling + : Material.foreground } Label { text: Config.formatSats(modelData.value_sats) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 1625d1f6f..4114d2384 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -279,7 +279,13 @@ Pane { wrapMode: Text.Wrap font.pixelSize: constants.fontSizeLarge font.family: FixedFont - color: modelData.is_mine ? constants.colorMine : Material.foreground + color: modelData.is_mine + ? modelData.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : modelData.is_billing + ? constants.colorAddressBilling + : Material.foreground } Label { text: Config.formatSats(modelData.value) diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 372d75823..8bd04c870 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -247,8 +247,10 @@ def update(self, from_txid: bool = False): self._outputs = list(map(lambda x: { 'address': x.get_ui_address_str(), 'value': QEAmount(amount_sat=x.value), - 'is_mine': self._wallet.wallet.is_mine(x.get_ui_address_str()) - }, self._tx.outputs())) + 'is_mine': self._wallet.wallet.is_mine(x.get_ui_address_str()), + 'is_change': self._wallet.wallet.is_change(x.get_ui_address_str()), + 'is_billing': self._wallet.wallet.is_billing_address(x.get_ui_address_str()) + }, self._tx.outputs())) txinfo = self._wallet.wallet.get_tx_info(self._tx) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index ad567333b..d0dba05f7 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -214,7 +214,9 @@ def update_outputs_from_tx(self, tx): outputs.append({ 'address': o.get_ui_address_str(), 'value_sats': o.value, - 'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()) + 'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()), + 'is_change': self._wallet.wallet.is_change(o.get_ui_address_str()), + 'is_billing': self._wallet.wallet.is_billing_address(o.get_ui_address_str()) }) self.outputs = outputs From ce7abd99c139a4cb6e1fd8769b68029d6314d1d8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 4 May 2023 14:01:11 +0200 Subject: [PATCH 0876/1143] update locale --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 9484b83d6..53312c49f 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 9484b83d622bea4a4c9d88cbe16f5b4f87822216 +Subproject commit 53312c49f766e0dc0a7a1ae9f0f8e09e036a486f From ff287e518fcc34010420ce413c95dd790ab544bd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 4 May 2023 14:24:13 +0200 Subject: [PATCH 0877/1143] update version to 4.4.2 + date release notes --- RELEASE-NOTES | 2 +- electrum/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 0da2c64df..e00c7464f 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,4 +1,4 @@ -# Release 4.4.2 (unreleased) +# Release 4.4.2 (May 4, 2023) * Qt GUI: - fix undefined var check in swap_dialog (#8341) - really fix "recursion depth exceeded" for utxo privacy analysis (#8315) diff --git a/electrum/version.py b/electrum/version.py index b9c775ac0..0fd023bc8 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.4.1' # version of the client package -APK_VERSION = '4.4.1.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.4.2' # version of the client package +APK_VERSION = '4.4.2.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From 205f2955f0decb8ffa35f039da5d565a5023e441 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 May 2023 15:05:34 +0000 Subject: [PATCH 0878/1143] network: add another hardcoded signet server --- electrum/servers_regtest.json | 2 +- electrum/servers_signet.json | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/servers_regtest.json b/electrum/servers_regtest.json index 5356f090c..08ae1c9c1 100644 --- a/electrum/servers_regtest.json +++ b/electrum/servers_regtest.json @@ -3,6 +3,6 @@ "pruning": "-", "s": "51002", "t": "51001", - "version": "1.2" + "version": "1.4" } } diff --git a/electrum/servers_signet.json b/electrum/servers_signet.json index aacd9dce6..df12c7c3f 100644 --- a/electrum/servers_signet.json +++ b/electrum/servers_signet.json @@ -3,7 +3,7 @@ "pruning": "-", "s": "51002", "t": "51001", - "version": "1.2" + "version": "1.4" }, "signet-electrumx.wakiyamap.dev": { "pruning": "-", @@ -16,5 +16,10 @@ "s": "60003", "t": "50001", "version": "1.4" + }, + "electrum.emzy.de": { + "pruning": "-", + "s": "53002", + "version": "1.4" } } From 847b3e919326244cef601a1bf948cfcf2b5148d5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 May 2023 15:24:34 +0000 Subject: [PATCH 0879/1143] update release notes --- RELEASE-NOTES | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index e00c7464f..13fc416b2 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -3,12 +3,13 @@ - fix undefined var check in swap_dialog (#8341) - really fix "recursion depth exceeded" for utxo privacy analysis (#8315) * QML GUI: - - fix 2fa callback issue (#8368) + - fix signing txs for 2fa wallets (#8368) + - fix for wallets with encrypted-keystore but unencrypted-storage (#8374) - properly delete wizard components after use (#8357) - avoid entering loadWallet if daemon is already busy loading (#8355) - no auto capitalization on import and master key text fields (5600375d) - remove Qt virtual keyboard and add Seedkeyboard for seed entry (#8371, #8352) - - add runtime toggling of android SECURE_FLAG (#8351) + - add runtime toggling of android SECURE_FLAG, to allow screenshots (#8351) - restrict cases where server is shown "lagging" (53d61c01) * fix hardened char "h" vs "'" needed for some hw wallets (#8364, 499f5153) * fix digitalbitbox(1) support (22b8c4e3) From fea0b84372fadc4262a045bf533f4d20484fd96d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 4 May 2023 20:13:57 +0000 Subject: [PATCH 0880/1143] SECURITY.md: make email addrs more bot-resistant --- SECURITY.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 2fb45e40e..fbb781beb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,12 +2,15 @@ ## Reporting a Vulnerability -The following keys may be used to communicate sensitive information to developers: +To report security issues, send an email to the addresses listed below (not for support). +Please send any report to all emails listed here. +The following GPG keys may be used to communicate sensitive information to developers. -| Name | Email | Fingerprint | -|------|-------|----------------| -| ThomasV | thomasv@electrum.org | 6694 D8DE 7BE8 EE56 31BE D950 2BD5 824B 7F94 70E6 | -| SomberNight | somber.night@protonmail.com | 4AD6 4339 DFA0 5E20 B3F6 AD51 E7B7 48CD AF5E 5ED9 | + +| Name | Email | GPG fingerprint | +|-------------|----------------------------------------|---------------------------------------------------| +| ThomasV | thomasv [AT] electrum [DOT] org | 6694 D8DE 7BE8 EE56 31BE D950 2BD5 824B 7F94 70E6 | +| SomberNight | somber.night [AT] protonmail [DOT] com | 4AD6 4339 DFA0 5E20 B3F6 AD51 E7B7 48CD AF5E 5ED9 | You can import a key by running the following command with that individual’s fingerprint: `gpg --recv-keys ""` From 302049919996575ee46a86a4220aab44137cf0ec Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 May 2023 15:02:34 +0000 Subject: [PATCH 0881/1143] hww: ledger: bump required ledger-bitcoin and adapt to API change --- contrib/deterministic-build/requirements-hw.txt | 12 ++++++------ contrib/requirements/requirements-hw.txt | 3 ++- electrum/plugins/ledger/ledger.py | 8 +++++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index a911568f3..542267c22 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -152,12 +152,12 @@ keepkey==6.3.1 \ --hash=sha256:88e2b5291c85c8e8567732f675697b88241082884aa1aba32257f35ee722fc09 \ --hash=sha256:cef1e862e195ece3e42640a0f57d15a63086fd1dedc8b5ddfcbc9c2657f0bb1e \ --hash=sha256:f369d640c65fec7fd8e72546304cdc768c04224a6b9b00a19dc2cd06fa9d2a6b -ledger-bitcoin==0.1.1 \ - --hash=sha256:15eb5e95e38d45a5f46468890cd69b066acbab855ca8a68073823cf545477f18 \ - --hash=sha256:8d594d54ce3cecd7385e7a4a357fe96b27518fc83b754a0b4339c2325b4a0a3c -ledgercomm==1.1.1 \ - --hash=sha256:1273b99b69bef05ef39fa740c8e09ecd631c24cd3fcdb65dac7903408da509c9 \ - --hash=sha256:ae071e4f3d0897c7ed209c3359cf2438855ac1d9cee4ca48f41bd0506e1c1b2f +ledger-bitcoin==0.2.1 \ + --hash=sha256:01697fab6333ceca4d228eb27f7d12f763e033f620b1bfb5949b20ec0b33b6ca \ + --hash=sha256:1160aa887df9f88539110c3c88535fc838cfff26600a326df23b4a09f09036f1 +ledgercomm==1.1.2 \ + --hash=sha256:8b338f6f0bfedf85eaf26a6af7e41120685dfb3e2956718948c9d83a4b5155bb \ + --hash=sha256:e3a39198b7c01135d7bdb049948ac41ce22cd8d39ddf6fbfc236a228d560a0ce libusb1==3.0.0 \ --hash=sha256:083f75e5d15cb5e2159e64c308c5317284eae926a820d6dce53a9505d18be3b2 \ --hash=sha256:0e652b04cbe85ec8e74f9ee82b49f861fb14b5320ae51399387ad2601ccc0500 \ diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 8b16d7839..05cff1acc 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -15,7 +15,8 @@ keepkey>=6.3.1 # device plugin: ledger # note: btchip-python only needed for "legacy" protocol and HW.1 support btchip-python>=0.1.32 -ledger-bitcoin>=0.1.1,<0.2.0 +ledger-bitcoin>=0.2.0,<0.3.0 +hidapi # device plugin: coldcard ckcc-protocol>=0.7.7 diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 1d17cbf47..9c09599b0 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1235,8 +1235,9 @@ def process_origin(origin: KeyOriginInfo, *, script_addrtype=script_addrtype) -> # For each wallet, sign for __, (__, wallet, wallet_hmac) in wallets.items(): input_sigs = self.client.sign_psbt(psbt, wallet, wallet_hmac) - for idx, pubkey, sig in input_sigs: - tx.add_signature_to_txin(txin_idx=idx, signing_pubkey=pubkey.hex(), sig=sig.hex()) + for idx, part_sig in input_sigs: + tx.add_signature_to_txin( + txin_idx=idx, signing_pubkey=part_sig.pubkey.hex(), sig=part_sig.signature.hex()) except DenyError: pass # cancelled by user except BaseException as e: @@ -1317,7 +1318,8 @@ def show_address(self, sequence, *args, **kwargs): class LedgerPlugin(HW_PluginBase): keystore_class = Ledger_KeyStore - minimum_library = (0, 1, 1) + minimum_library = (0, 2, 0) + maximum_library = (0, 3, 0) DEVICE_IDS = [(0x2581, 0x1807), # HW.1 legacy btchip (0x2581, 0x2b7c), # HW.1 transitional production (0x2581, 0x3b7c), # HW.1 ledger production From 9c471444188f46a1856d98de83897665ade66668 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 May 2023 16:06:16 +0000 Subject: [PATCH 0882/1143] qt: handle expected errors in DSCancelDialog closes https://github.com/spesmilo/electrum/issues/8390 --- electrum/gui/qt/rbf_dialog.py | 4 ++-- electrum/wallet.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/rbf_dialog.py b/electrum/gui/qt/rbf_dialog.py index 97c34cabc..346e473b2 100644 --- a/electrum/gui/qt/rbf_dialog.py +++ b/electrum/gui/qt/rbf_dialog.py @@ -15,7 +15,7 @@ from electrum.i18n import _ from electrum.transaction import PartialTransaction -from electrum.wallet import CannotBumpFee +from electrum.wallet import CannotRBFTx if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -124,7 +124,7 @@ def update_tx(self): else: try: self.tx = self.make_tx(fee_rate) - except CannotBumpFee as e: + except CannotRBFTx as e: self.tx = None self.error = str(e) diff --git a/electrum/wallet.py b/electrum/wallet.py index b1db5f61e..7e4555f31 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -225,12 +225,14 @@ def get_locktime_for_new_transaction(network: 'Network') -> int: return locktime +class CannotRBFTx(Exception): pass -class CannotBumpFee(Exception): + +class CannotBumpFee(CannotRBFTx): def __str__(self): return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self) -class CannotDoubleSpendTx(Exception): +class CannotDoubleSpendTx(CannotRBFTx): def __str__(self): return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self) From a0c43573ab2c996a1738182d8e06051587c41fcf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 May 2023 17:00:18 +0000 Subject: [PATCH 0883/1143] locale/i18n: get default language and set it as early as possible TODO elaborate xxxxx --- electrum/gui/default_lang.py | 40 ++++++++++++++++++++++++++++++++++++ electrum/gui/kivy/i18n.py | 6 ++++++ electrum/gui/kivy/util.py | 15 ++++---------- electrum/gui/qml/__init__.py | 18 ---------------- electrum/gui/qt/__init__.py | 3 +-- electrum/gui/qt/util.py | 4 ---- run_electrum | 18 ++++++++++------ 7 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 electrum/gui/default_lang.py diff --git a/electrum/gui/default_lang.py b/electrum/gui/default_lang.py new file mode 100644 index 000000000..ceb030ca8 --- /dev/null +++ b/electrum/gui/default_lang.py @@ -0,0 +1,40 @@ +# Copyright (C) 2023 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php +# +# Note: try not to import modules from electrum, or at least from GUIs. +# This is to avoid evaluating module-level string-translations before we get +# a chance to set the default language. + +import os +from typing import Optional + +from electrum.i18n import languages + + +jLocale = None +if "ANDROID_DATA" in os.environ: + from jnius import autoclass, cast + jLocale = autoclass("java.util.Locale") + + +def get_default_language(*, gui_name: Optional[str] = None) -> str: + if gui_name == "qt": + from PyQt5.QtCore import QLocale + name = QLocale.system().name() + return name if name in languages else "en_UK" + elif gui_name == "qml": + from PyQt5.QtCore import QLocale + # On Android QLocale does not return the system locale + try: + name = str(jLocale.getDefault().toString()) + except Exception: + name = QLocale.system().name() + return name if name in languages else "en_GB" + elif gui_name == "kivy": + if "ANDROID_DATA" not in os.environ: + return "en_UK" + # FIXME: CJK/Arabic/etc languages do not work at all with kivy due to font issues, + # so it is easiest to just default to English... (see #2032) + return "en_UK" + return "" diff --git a/electrum/gui/kivy/i18n.py b/electrum/gui/kivy/i18n.py index 76cf2a09e..5b33afee2 100644 --- a/electrum/gui/kivy/i18n.py +++ b/electrum/gui/kivy/i18n.py @@ -1,5 +1,10 @@ import gettext +from electrum.logging import get_logger + + +_logger = get_logger(__name__) + class _(str): @@ -35,6 +40,7 @@ def bind(label): @staticmethod def switch_lang(lang): + _logger.info(f"switch_lang() called with {lang=!r}") # get the right locales directory, and instantiate a gettext from electrum.i18n import LOCALE_DIR, set_language locales = gettext.translation('electrum', LOCALE_DIR, languages=[lang], fallback=True) diff --git a/electrum/gui/kivy/util.py b/electrum/gui/kivy/util.py index 7320a1fae..60681025a 100644 --- a/electrum/gui/kivy/util.py +++ b/electrum/gui/kivy/util.py @@ -1,4 +1,6 @@ -from kivy.utils import get_color_from_hex, platform +from kivy.utils import get_color_from_hex + +from electrum.gui.default_lang import get_default_language as _get_default_language def address_colors(wallet, addr): @@ -24,13 +26,4 @@ def address_colors(wallet, addr): def get_default_language() -> str: - if platform != 'android': - return 'en_UK' - # FIXME: CJK/Arabic/etc languages do not work at all with kivy due to font issues, - # so it is easiest to just default to English... (see #2032) - return 'en_UK' - # # try getting the language of the Android OS - # from jnius import autoclass - # Locale = autoclass("java.util.Locale") - # lang = str(Locale.getDefault().toString()) - # return lang if lang else 'en_UK' + return _get_default_language(gui_name="kivy") diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 5d5bf5045..9d36f369b 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -33,9 +33,6 @@ from .qeapp import ElectrumQmlApplication, Exception_Hook -if 'ANDROID_DATA' in os.environ: - from jnius import autoclass, cast - jLocale = autoclass("java.util.Locale") class ElectrumTranslator(QTranslator): def __init__(self, parent=None): @@ -51,12 +48,6 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) - lang = config.get('language','') - if not lang: - lang = self.get_default_language() - self.logger.info(f'setting language {lang}') - set_language(lang) - # uncomment to debug plugin and import tracing # os.environ['QML_IMPORT_TRACE'] = '1' # os.environ['QT_DEBUG_PLUGINS'] = '1' @@ -119,12 +110,3 @@ def main(self): def stop(self): self.logger.info('closing GUI') self.app.quit() - - def get_default_language(self): - # On Android QLocale does not return the system locale - try: - name = str(jLocale.getDefault().toString()) - except Exception: - name = QLocale.system().name() - self.logger.info(f'System default locale: {name}') - return name if name in languages else 'en_GB' diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 890475fe0..326d70bd7 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -65,7 +65,7 @@ from electrum.gui import BaseElectrumGui from .installwizard import InstallWizard, WalletAlreadyOpenInMemory -from .util import get_default_language, read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin +from .util import read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin from .main_window import ElectrumWindow from .network_dialog import NetworkDialog from .stylesheet_patcher import patch_qt_stylesheet @@ -111,7 +111,6 @@ class ElectrumGui(BaseElectrumGui, Logger): @profiler def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): - set_language(config.get('language', get_default_language())) BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) self.logger.info(f"Qt GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}") diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 3ba3bcebc..653d35288 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1117,10 +1117,6 @@ def setIcon(self, icon): self.icon.setPixmap(icon.pixmap(self.icon_size)) self.icon.repaint() # macOS hack for #6269 -def get_default_language(): - name = QLocale.system().name() - return name if name in languages else 'en_UK' - def char_width_in_lineedit() -> int: char_width = QFontMetrics(QLineEdit().font()).averageCharWidth() diff --git a/run_electrum b/run_electrum index 7c363d539..aaf6ea161 100755 --- a/run_electrum +++ b/run_electrum @@ -384,14 +384,22 @@ def main(): sys.exit(1) config = SimpleConfig(config_options) + cmdname = config.get('cmd') # set language as early as possible # Note: we are already too late for strings that are declared in the global scope # of an already imported module. However, the GUI and the plugins at least have - # not been imported yet. - # Note: it is ok to call set_language() again later. E.g. the Qt GUI has additional - # tools to figure out the default language so it will get called again there. - set_language(config.get('language')) + # not been imported yet. (see #4621) + # Note: it is ok to call set_language() again later, but note that any call only applies + # to not-yet-evaluated strings. + if cmdname == 'gui': + from electrum.gui.default_lang import get_default_language + gui_name = config.get('gui', 'qt') + lang = config.get('language') + if not lang: + lang = get_default_language(gui_name=gui_name) + _logger.info(f"get_default_language: detected default as {lang=!r}") + set_language(lang) if config.get('testnet'): constants.set_testnet() @@ -402,8 +410,6 @@ def main(): elif config.get('signet'): constants.set_signet() - cmdname = config.get('cmd') - if cmdname == 'daemon' and config.get("detach"): # detect lockfile. # This is not as good as get_file_descriptor, but that would require the asyncio loop From 3674fc5a916c5921f2c18cae14730662e76d1f09 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 5 May 2023 17:38:41 +0000 Subject: [PATCH 0884/1143] follow-up prev: forgot to amend the commit message :( follow-up a0c43573ab2c996a1738182d8e06051587c41fcf This now runs the more advanced GUI-specific default lang detection before the specific GUI is imported. This means even module-level translated strings will use the correct language, at least in the GUI modules. Before this commit, more UI strings would be translated to German if the user explicitly set the language to German, compared to how many were translated if the language was "default" and got set to German via the default-lang mechanism. This should no longer be the case. related: https://github.com/spesmilo/electrum/issues/4621 From 1e5504283089b504bba0bdd6b0815f6a0fcc6b04 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sat, 6 May 2023 13:26:08 +0000 Subject: [PATCH 0885/1143] wallet: set_frozen_state_of_{addresses,coins} to save_db() to disk fixes https://github.com/spesmilo/electrum/issues/8389 --- electrum/wallet.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 7e4555f31..74e1401ac 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1860,7 +1860,13 @@ def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool: return False return True - def set_frozen_state_of_addresses(self, addrs: Sequence[str], freeze: bool) -> bool: + def set_frozen_state_of_addresses( + self, + addrs: Iterable[str], + freeze: bool, + *, + write_to_disk: bool = True, + ) -> bool: """Set frozen state of the addresses to FREEZE, True or False""" if all(self.is_mine(addr) for addr in addrs): with self._freeze_lock: @@ -1870,10 +1876,18 @@ def set_frozen_state_of_addresses(self, addrs: Sequence[str], freeze: bool) -> b self._frozen_addresses -= set(addrs) self.db.put('frozen_addresses', list(self._frozen_addresses)) util.trigger_callback('status') + if write_to_disk: + self.save_db() return True return False - def set_frozen_state_of_coins(self, utxos: Sequence[str], freeze: bool) -> None: + def set_frozen_state_of_coins( + self, + utxos: Iterable[str], + freeze: bool, + *, + write_to_disk: bool = True, + ) -> None: """Set frozen state of the utxos to FREEZE, True or False""" # basic sanity check that input is not garbage: (see if raises) [TxOutpoint.from_str(utxo) for utxo in utxos] @@ -1881,6 +1895,8 @@ def set_frozen_state_of_coins(self, utxos: Sequence[str], freeze: bool) -> None: for utxo in utxos: self._frozen_coins[utxo] = bool(freeze) util.trigger_callback('status') + if write_to_disk: + self.save_db() def is_address_reserved(self, addr: str) -> bool: # note: atm 'reserved' status is only taken into consideration for 'change addresses' @@ -3133,7 +3149,7 @@ def delete_address(self, address: str) -> None: self.set_label(address, None) if req:= self.get_request_by_addr(address): self.delete_request(req.get_id()) - self.set_frozen_state_of_addresses([address], False) + self.set_frozen_state_of_addresses([address], False, write_to_disk=False) pubkey = self.get_public_key(address) self.db.remove_imported_address(address) if pubkey: From c0917e65af3c96dacbef8b7c48810c4f24e62d70 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 6 May 2023 18:02:47 +0200 Subject: [PATCH 0886/1143] fix #8391: reintroduce recursion, limited to unconfirmed transactions It would be better to have topological order, but this issue needs to be fixed quickly. --- electrum/wallet.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/electrum/wallet.py b/electrum/wallet.py index 74e1401ac..711923f08 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -907,9 +907,11 @@ def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: with self.lock, self.transaction_lock: if self._last_full_history is None: self._last_full_history = self.get_full_history(None, include_lightning=False) - # populate cache in chronological order - for _txid in self._last_full_history.keys(): - self.get_tx_parents(_txid) + # populate cache in chronological order (confirmed tx only) + # todo: get_full_history should return unconfirmed tx topologically sorted + for _txid, tx_item in self._last_full_history.items(): + if tx_item['height'] > 0: + self.get_tx_parents(_txid) result = self._tx_parents_cache.get(txid, None) if result is not None: @@ -941,8 +943,7 @@ def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]: for _txid in parents + uncles: if _txid in self._last_full_history.keys(): - p = self._tx_parents_cache[_txid] - result.update(p) + result.update(self.get_tx_parents(_txid)) result[txid] = parents, uncles self._tx_parents_cache[txid] = result return result From 38ec72527f165705133d440cff23a356c5907ab7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 7 May 2023 00:39:06 +0000 Subject: [PATCH 0887/1143] wallet.get_bolt11_invoice: handle request not having LN part fixes https://github.com/spesmilo/electrum/issues/8402 To reproduce, - create wallet from a zpub - LN is disabled there by default - create a receive request, which won't have a lightning part - enable config var "bip21_lightning" - enable LN - existing request is now ~breaking receive tab --- electrum/lnworker.py | 2 ++ electrum/wallet.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 054fa4c15..0994b4e38 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1801,6 +1801,7 @@ def get_bolt11_invoice( fallback_address: Optional[str], channels: Optional[Sequence[Channel]] = None, ) -> Tuple[LnAddr, str]: + assert isinstance(payment_hash, bytes), f"expected bytes, but got {type(payment_hash)}" pair = self._bolt11_cache.get(payment_hash) if pair: @@ -1856,6 +1857,7 @@ def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: self.wallet.save_db() def get_preimage(self, payment_hash: bytes) -> Optional[bytes]: + assert isinstance(payment_hash, bytes), f"expected bytes, but got {type(payment_hash)}" r = self.preimages.get(payment_hash.hex()) return bytes.fromhex(r) if r else None diff --git a/electrum/wallet.py b/electrum/wallet.py index 711923f08..567bb4fce 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2605,10 +2605,12 @@ def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int def get_bolt11_invoice(self, req: Request) -> str: if not self.lnworker: return '' + if (payment_hash := req.payment_hash) is None: # e.g. req might have been generated before enabling LN + return '' amount_msat = req.get_amount_msat() or None assert (amount_msat is None or amount_msat > 0), amount_msat lnaddr, invoice = self.lnworker.get_bolt11_invoice( - payment_hash=req.payment_hash, + payment_hash=payment_hash, amount_msat=amount_msat, message=req.message, expiry=req.exp, From 209c9f75e01df1045745ed96f3fc78f3a0e42284 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 7 May 2023 21:50:23 +0000 Subject: [PATCH 0888/1143] transaction: fix add_info_from_wallet_and_network fixes https://github.com/spesmilo/electrum/issues/8406 --- electrum/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/transaction.py b/electrum/transaction.py index 82aecaada..22a55709d 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1044,7 +1044,7 @@ def add_info_from_wallet_and_network( Relatedly, this should *not* be called from the network thread. """ # note side-effect: tx is being mutated - from .network import NetworkException + from .network import NetworkException, Network self.add_info_from_wallet(wallet) try: if self.is_missing_info_from_network(): From d12752cc42918c240c6951dd1b8c29b2d6835919 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 8 May 2023 14:53:45 +0200 Subject: [PATCH 0889/1143] qml: add Technical properties header for TxDetails and LightningPaymentDetails --- .../components/LightningPaymentDetails.qml | 5 ++++ electrum/gui/qml/components/TxDetails.qml | 27 +++++++++++-------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index fa490487e..7da44ded0 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -132,6 +132,11 @@ Pane { } } + Heading { + Layout.columnSpan: 2 + text: qsTr('Technical properties') + } + Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 4114d2384..2421cb1c5 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -151,17 +151,6 @@ Pane { text: txdetails.date } - Label { - visible: txdetails.isMined - text: qsTr('Mined at') - color: Material.accentColor - } - - Label { - visible: txdetails.isMined - text: txdetails.shortId - } - Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall @@ -224,6 +213,22 @@ Pane { } } + Heading { + Layout.columnSpan: 2 + text: qsTr('Technical properties') + } + + Label { + visible: txdetails.isMined + text: qsTr('Mined at') + color: Material.accentColor + } + + Label { + visible: txdetails.isMined + text: txdetails.shortId + } + Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall From c9536180c50c230af43aa75bd22c7bdd97cc70b0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 8 May 2023 19:37:33 +0000 Subject: [PATCH 0890/1143] lnutil.LnFeatures: limit max feature bit to 10_000 closes https://github.com/spesmilo/electrum/issues/8403 > In Python 3.10 that worked fine, however in Python 3.11 large integer check https://github.com/python/cpython/issues/95778, so now this throws an error. Apparently this change was deemed a security fix and was backported to all supported branches of CPython (going back to 3.7). i.e. it affects ~all versions of python (if sufficiently updated with bugfix patches), not just 3.11 > Some offending node aliases: > ``` > ergvein-fiatchannels > test-mainnet > arakis > ``` The features bits set by some of these nodes: ``` (1, 7, 8, 11, 13, 14, 17, 19, 23, 27, 45, 32973, 52973) (1, 7, 8, 11, 13, 14, 17, 19, 23, 27, 39, 45, 55, 32973, 52973) ``` > P.S. I see there are a lot of nodes with 253 bytes in their feature vectors. Any idea why that could happen? Note that the valid [merged-into-spec features](https://github.com/lightning/bolts/blob/50b2df24a27879e8329712c275db78876fd022fe/09-features.md) currently only go as high as ~51. However the spec does not specify how to choose feature bits for experimental stuff, so I guess some people are using values in the 50k range. The only limit imposed by the spec on the length of the features bitvector is an implicit one due to the max message size: every msg must be smaller than 65KB, and the features bitvector needs to fit inside the init message, hence it can be up to ~524K bits. (note that the features are not stored in a sparse representation in the init message and in gossip messages, so if many nodes set such high feature bits, that would noticably impact the size of the gossip). ----- Anyway, our current implementation of LnFeatures is subclassing IntFlag, and it looks like it does not work well for such large integers. I've managed to make IntFlags reasonably in python 3.11 by overriding __str__ and __repr__ (note that in cpython it is apparently only the base2<->base10 conversions that are slow, power-of-2 conversions are fast, so we can e.g. use `hex()`). However in python 3.10 and older, enum.py itself seems really slow for bigints, e.g. enum._decompose in python 3.10. Try e.g. this script, which is instant in py3.11 but takes minutes in py3.10: ```py from enum import IntFlag class c(IntFlag): known_flag_1 = 1 << 0 known_flag_2 = 1 << 1 known_flag_3 = 1 << 2 if hasattr(IntFlag, "_numeric_repr_"): # python 3.11+ _numeric_repr_ = hex def __repr__(self): return f"<{self._name_}: {hex(self._value_)}>" def __str__(self): return hex(self._value_) a = c(2**70000-1) q1 = repr(a) q2 = str(a) ``` AFAICT we have two options: either we rewrite LnFeatures so that it does not use IntFlag (and enum.py), or, for the short term as workaround, we could just reject very large feature bits. For now, I've opted to the latter, rejecting feature bits over 10k. (note that another option is bumping the min required python to 3.11, in which case with the overrides added in this commit the performance looks perfectly fine) --- electrum/channel_db.py | 4 ++-- electrum/lnpeer.py | 14 +++++++------- electrum/lnutil.py | 28 +++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/electrum/channel_db.py b/electrum/channel_db.py index 4331a56e5..e7b6e5f9e 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -68,7 +68,7 @@ class ChannelInfo(NamedTuple): @staticmethod def from_msg(payload: dict) -> 'ChannelInfo': features = int.from_bytes(payload['features'], 'big') - validate_features(features) + features = validate_features(features) channel_id = payload['short_channel_id'] node_id_1 = payload['node_id_1'] node_id_2 = payload['node_id_2'] @@ -164,7 +164,7 @@ class NodeInfo(NamedTuple): def from_msg(payload) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]: node_id = payload['node_id'] features = int.from_bytes(payload['features'], "big") - validate_features(features) + features = validate_features(features) addresses = NodeInfo.parse_addresses_field(payload['addresses']) peer_addrs = [] for host, port in addresses: diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 2dd1092cb..d143476da 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -42,7 +42,7 @@ LightningPeerConnectionClosed, HandshakeFailed, RemoteMisbehaving, ShortChannelID, IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage, - ChannelType, LNProtocolWarning) + ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures) from .lnutil import FeeUpdate, channel_id_from_funding_tx from .lntransport import LNTransport, LNTransportBase from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg @@ -352,12 +352,12 @@ def on_init(self, payload): if self._received_init: self.logger.info("ALREADY INITIALIZED BUT RECEIVED INIT") return - self.their_features = LnFeatures(int.from_bytes(payload['features'], byteorder="big")) - their_globalfeatures = int.from_bytes(payload['globalfeatures'], byteorder="big") - self.their_features |= their_globalfeatures - # check transitive dependencies for received features - if not self.their_features.validate_transitive_dependencies(): - raise GracefulDisconnect("remote did not set all dependencies for the features they sent") + _their_features = int.from_bytes(payload['features'], byteorder="big") + _their_features |= int.from_bytes(payload['globalfeatures'], byteorder="big") + try: + self.their_features = validate_features(_their_features) + except IncompatibleOrInsaneFeatures as e: + raise GracefulDisconnect(f"remote sent insane features: {repr(e)}") # check if features are compatible, and set self.features to what we negotiated try: self.features = ln_compare_features(self.features, self.their_features) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index e348659af..4ab8e3949 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -8,6 +8,8 @@ from collections import namedtuple, defaultdict from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence import re +import sys + import attr from aiorpcx import NetAddress @@ -1211,6 +1213,18 @@ def get_names(self) -> Sequence[str]: r.append(feature_name or f"bit_{flag}") return r + if hasattr(IntFlag, "_numeric_repr_"): # python 3.11+ + # performance improvement (avoid base2<->base10), see #8403 + _numeric_repr_ = hex + + def __repr__(self): + # performance improvement (avoid base2<->base10), see #8403 + return f"<{self._name_}: {hex(self._value_)}>" + + def __str__(self): + # performance improvement (avoid base2<->base10), see #8403 + return hex(self._value_) + class ChannelType(IntFlag): OPTION_LEGACY_CHANNEL = 0 @@ -1332,11 +1346,22 @@ def ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnF return our_features -def validate_features(features: int) -> None: +if hasattr(sys, "get_int_max_str_digits"): + # check that the user or other library has not lowered the limit (from default) + assert sys.get_int_max_str_digits() >= 4300, f"sys.get_int_max_str_digits() too low: {sys.get_int_max_str_digits()}" + + +def validate_features(features: int) -> LnFeatures: """Raises IncompatibleOrInsaneFeatures if - a mandatory feature is listed that we don't recognize, or - the features are inconsistent + For convenience, returns the parsed features. """ + if features.bit_length() > 10_000: + # This is an implementation-specific limit for how high feature bits we allow. + # Needed as LnFeatures subclasses IntFlag, and uses ints internally. + # See https://docs.python.org/3/library/stdtypes.html#integer-string-conversion-length-limitation + raise IncompatibleOrInsaneFeatures(f"features bitvector too large: {features.bit_length()=} > 10_000") features = LnFeatures(features) enabled_features = list_enabled_bits(features) for fbit in enabled_features: @@ -1345,6 +1370,7 @@ def validate_features(features: int) -> None: if not features.validate_transitive_dependencies(): raise IncompatibleOrInsaneFeatures(f"not all transitive dependencies are set. " f"features={features}") + return features def derive_payment_secret_from_payment_preimage(payment_preimage: bytes) -> bytes: From b40a608b74907a8e9ac5dba76bf7af6c0c554ff4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 9 May 2023 01:14:11 +0000 Subject: [PATCH 0891/1143] qt: persist addresses tab toolbar "show/hide" state, like in 4.3.4 --- electrum/gui/qt/address_list.py | 3 ++- electrum/gui/qt/history_list.py | 3 ++- electrum/gui/qt/main_window.py | 2 ++ electrum/gui/qt/my_treeview.py | 32 +++++++++++++++++++++++++------- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 2331359ca..7989bbc2c 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -120,10 +120,11 @@ def on_double_click(self, idx): addr = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR) self.main_window.show_address(addr) + CONFIG_KEY_SHOW_TOOLBAR = "show_toolbar_addresses" def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') self.num_addr_label = toolbar.itemAt(0).widget() - menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar(self.config)) + self._toolbar_checkbox = menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar()) menu.addConfig(_('Show Fiat balances'), 'fiat_address', False, callback=self.main_window.app.update_fiat_signal.emit) hbox = self.create_toolbar_buttons() toolbar.insertLayout(1, hbox) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index 284fac817..83c64be51 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -546,10 +546,11 @@ def on_combo(self, x): self.end_button.setText(_('To') + ' ' + self.format_date(self.end_date)) self.hide_rows() + CONFIG_KEY_SHOW_TOOLBAR = "show_toolbar_history" def create_toolbar(self, config): toolbar, menu = self.create_toolbar_with_menu('') self.num_tx_label = toolbar.itemAt(0).widget() - menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar(self.config)) + self._toolbar_checkbox = menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar()) self.menu_fiat = menu.addConfig(_('Show Fiat Values'), 'history_rates', False, callback=self.main_window.app.update_fiat_signal.emit) self.menu_capgains = menu.addConfig(_('Show Capital Gains'), 'history_rates_capital_gains', False, callback=self.main_window.app.update_fiat_signal.emit) self.menu_summary = menu.addAction(_("&Summary"), self.show_summary) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 683944622..cc692ba73 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1339,6 +1339,8 @@ def create_list_tab(self, l): if toolbar: vbox.addLayout(toolbar) vbox.addWidget(l) + if toolbar: + l.show_toolbar() return w def create_addresses_tab(self): diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index b10674fde..c31c2ee15 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -51,7 +51,7 @@ QFileDialog, QWidget, QToolButton, QTreeView, QPlainTextEdit, QHeaderView, QApplication, QToolTip, QTreeWidget, QStyledItemDelegate, QMenu, QStyleOptionViewItem, QLayout, QLayoutItem, QAbstractButton, - QGraphicsEffect, QGraphicsScene, QGraphicsPixmapItem, QSizePolicy) + QGraphicsEffect, QGraphicsScene, QGraphicsPixmapItem, QSizePolicy, QAction) from electrum.i18n import _, languages from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path @@ -63,6 +63,7 @@ from .util import read_QIcon if TYPE_CHECKING: + from electrum import SimpleConfig from .main_window import ElectrumWindow @@ -73,13 +74,13 @@ def __init__(self, config): self.setToolTipsVisible(True) self.config = config - def addToggle(self, text: str, callback, *, tooltip=''): + def addToggle(self, text: str, callback, *, tooltip='') -> QAction: m = self.addAction(text, callback) m.setCheckable(True) m.setToolTip(tooltip) return m - def addConfig(self, text:str, name:str, default:bool, *, tooltip='', callback=None): + def addConfig(self, text: str, name: str, default: bool, *, tooltip='', callback=None) -> QAction: b = self.config.get(name, default) m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback)) m.setCheckable(True) @@ -94,7 +95,7 @@ def _do_toggle_config(self, name, default, callback): callback() -def create_toolbar_with_menu(config, title): +def create_toolbar_with_menu(config: 'SimpleConfig', title): menu = MyMenu(config) toolbar_button = QToolButton() toolbar_button.setIcon(read_QIcon("preferences.png")) @@ -401,7 +402,15 @@ def create_toolbar_buttons(self): def create_toolbar_with_menu(self, title): return create_toolbar_with_menu(self.config, title) - def show_toolbar(self, state, config=None): + CONFIG_KEY_SHOW_TOOLBAR = None # type: Optional[str] + _toolbar_checkbox = None # type: Optional[QAction] + def show_toolbar(self, state: bool = None): + if state is None: # get value from config + if self.config and self.CONFIG_KEY_SHOW_TOOLBAR: + state = self.config.get(self.CONFIG_KEY_SHOW_TOOLBAR, None) + if state is None: + return + assert isinstance(state, bool), state if state == self.toolbar_shown: return self.toolbar_shown = state @@ -409,9 +418,18 @@ def show_toolbar(self, state, config=None): b.setVisible(state) if not state: self.on_hide_toolbar() + if self._toolbar_checkbox is not None: + # update the cb state now, in case the checkbox was not what triggered us + self._toolbar_checkbox.setChecked(state) - def toggle_toolbar(self, config=None): - self.show_toolbar(not self.toolbar_shown, config) + def on_hide_toolbar(self): + pass + + def toggle_toolbar(self): + new_state = not self.toolbar_shown + self.show_toolbar(new_state) + if self.config and self.CONFIG_KEY_SHOW_TOOLBAR: + self.config.set_key(self.CONFIG_KEY_SHOW_TOOLBAR, new_state) def add_copy_menu(self, menu: QMenu, idx) -> QMenu: cc = menu.addMenu(_("Copy")) From 0e0c7980ddc6e80ce774ba8c55352f741c376569 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 8 May 2023 13:36:45 +0200 Subject: [PATCH 0892/1143] qml: implement bip39 account detection --- .../qml/components/BIP39RecoveryDialog.qml | 142 ++++++++++++++++++ .../qml/components/wizard/WCBIP39Refine.qml | 36 ++++- electrum/gui/qml/qeapp.py | 2 + electrum/gui/qml/qebip39recovery.py | 129 ++++++++++++++++ electrum/gui/qml/util.py | 82 +++++++++- 5 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 electrum/gui/qml/components/BIP39RecoveryDialog.qml create mode 100644 electrum/gui/qml/qebip39recovery.py diff --git a/electrum/gui/qml/components/BIP39RecoveryDialog.qml b/electrum/gui/qml/components/BIP39RecoveryDialog.qml new file mode 100644 index 000000000..8585a0f04 --- /dev/null +++ b/electrum/gui/qml/components/BIP39RecoveryDialog.qml @@ -0,0 +1,142 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +import "controls" + +ElDialog { + id: dialog + title: qsTr("Detect BIP39 accounts") + + property string seed + property string seedExtraWords + property string walletType + + property string derivationPath + property string scriptType + + z: 1 // raise z so it also covers wizard dialog + + anchors.centerIn: parent + + padding: 0 + + width: parent.width * 4/5 + height: parent.height * 4/5 + + ColumnLayout { + id: rootLayout + width: parent.width + height: parent.height + + InfoTextArea { + Layout.fillWidth: true + Layout.margins: constants.paddingMedium + + text: bip39RecoveryListModel.state == Bip39RecoveryListModel.Scanning + ? qsTr('Scanning for accounts...') + : bip39RecoveryListModel.state == Bip39RecoveryListModel.Success + ? listview.count > 0 + ? qsTr('Choose an account to restore.') + : qsTr('No existing accounts found.') + : bip39RecoveryListModel.state == Bip39RecoveryListModel.Failed + ? qsTr('Recovery failed') + : qsTr('Recovery cancelled') + iconStyle: bip39RecoveryListModel.state == Bip39RecoveryListModel.Scanning + ? InfoTextArea.IconStyle.Spinner + : bip39RecoveryListModel.state == Bip39RecoveryListModel.Success + ? InfoTextArea.IconStyle.Info + : InfoTextArea.IconStyle.Error + } + + Frame { + id: accountsFrame + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge + Layout.leftMargin: constants.paddingMedium + Layout.rightMargin: constants.paddingMedium + + verticalPadding: 0 + horizontalPadding: 0 + background: PaneInsetBackground {} + + ColumnLayout { + spacing: 0 + anchors.fill: parent + + ListView { + id: listview + Layout.preferredWidth: parent.width + Layout.fillHeight: true + clip: true + model: bip39RecoveryListModel + + delegate: ItemDelegate { + width: ListView.view.width + height: itemLayout.height + + onClicked: { + dialog.derivationPath = model.derivation_path + dialog.scriptType = model.script_type + dialog.doAccept() + } + + GridLayout { + id: itemLayout + columns: 2 + rowSpacing: 0 + + anchors { + left: parent.left + right: parent.right + leftMargin: constants.paddingMedium + rightMargin: constants.paddingMedium + } + + Label { + Layout.columnSpan: 2 + text: model.description + } + Label { + text: qsTr('script type') + color: Material.accentColor + } + Label { + Layout.fillWidth: true + text: model.script_type + } + Label { + text: qsTr('derivation path') + color: Material.accentColor + } + Label { + Layout.fillWidth: true + text: model.derivation_path + } + Item { + Layout.columnSpan: 2 + Layout.preferredHeight: constants.paddingLarge + Layout.preferredWidth: 1 + } + } + } + + ScrollIndicator.vertical: ScrollIndicator { } + } + } + } + } + + Bip39RecoveryListModel { + id: bip39RecoveryListModel + } + + Component.onCompleted: { + bip39RecoveryListModel.startScan(walletType, seed, seedExtraWords) + } +} diff --git a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml index 5ee048063..a55a89328 100644 --- a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml +++ b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml @@ -1,9 +1,11 @@ import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.0 import org.electrum 1.0 +import ".." import "../controls" WizardComponent { @@ -86,10 +88,33 @@ WizardComponent { Label { text: qsTr('Script type and Derivation path') } - Button { - text: qsTr('Detect Existing Accounts') - enabled: false + Pane { + Layout.alignment: Qt.AlignHCenter + padding: 0 visible: !isMultisig + + FlatButton { + text: qsTr('Detect Existing Accounts') + onClicked: { + var dialog = bip39recoveryDialog.createObject(mainLayout, { + walletType: wizard_data['wallet_type'], + seed: wizard_data['seed'], + seedExtraWords: wizard_data['seed_extra_words'] + }) + dialog.accepted.connect(function () { + // select matching script type button and set derivation path + for (var i = 0; i < scripttypegroup.buttons.length; i++) { + var btn = scripttypegroup.buttons[i] + if (btn.visible && btn.scripttype == dialog.scriptType) { + btn.checked = true + derivationpathtext.text = dialog.derivationPath + return + } + } + }) + dialog.open() + } + } } Label { @@ -157,6 +182,11 @@ WizardComponent { id: bitcoin } + Component { + id: bip39recoveryDialog + BIP39RecoveryDialog { } + } + Component.onCompleted: { isMultisig = wizard_data['wallet_type'] == 'multisig' if (isMultisig) { diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index e670f1087..c4f3c29c4 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -40,6 +40,7 @@ from .qeswaphelper import QESwapHelper from .qewizard import QENewWalletWizard, QEServerConnectWizard from .qemodelfilter import QEFilterProxyModel +from .qebip39recovery import QEBip39RecoveryListModel if TYPE_CHECKING: from electrum.simple_config import SimpleConfig @@ -339,6 +340,7 @@ def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: ' qmlRegisterType(QETxRbfFeeBumper, 'org.electrum', 1, 0, 'TxRbfFeeBumper') qmlRegisterType(QETxCpfpFeeBumper, 'org.electrum', 1, 0, 'TxCpfpFeeBumper') qmlRegisterType(QETxCanceller, 'org.electrum', 1, 0, 'TxCanceller') + qmlRegisterType(QEBip39RecoveryListModel, 'org.electrum', 1, 0, 'Bip39RecoveryListModel') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard', 'QNewWalletWizard can only be used as property') diff --git a/electrum/gui/qml/qebip39recovery.py b/electrum/gui/qml/qebip39recovery.py new file mode 100644 index 000000000..f5632e1a8 --- /dev/null +++ b/electrum/gui/qml/qebip39recovery.py @@ -0,0 +1,129 @@ +import asyncio +import concurrent + +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot +from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex, Q_ENUMS + +from electrum import Network, keystore +from electrum.bip32 import BIP32Node +from electrum.bip39_recovery import account_discovery +from electrum.logging import get_logger + +from .util import TaskThread + +class QEBip39RecoveryListModel(QAbstractListModel): + _logger = get_logger(__name__) + + class State: + Idle = -1 + Scanning = 0 + Success = 1 + Failed = 2 + Cancelled = 3 + + Q_ENUMS(State) + + recoveryFailed = pyqtSignal() + stateChanged = pyqtSignal() + # userinfoChanged = pyqtSignal() + + # define listmodel rolemap + _ROLE_NAMES=('description', 'derivation_path', 'script_type') + _ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) + _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) + + def __init__(self, config, parent=None): + super().__init__(parent) + self._accounts = [] + self._thread = None + self._root_seed = None + self._state = QEBip39RecoveryListModel.State.Idle + # self._busy = False + # self._userinfo = '' + + def rowCount(self, index): + return len(self._accounts) + + def roleNames(self): + return self._ROLE_MAP + + def data(self, index, role): + account = self._accounts[index.row()] + role_index = role - Qt.UserRole + value = account[self._ROLE_NAMES[role_index]] + if isinstance(value, (bool, list, int, str)) or value is None: + return value + return str(value) + + def clear(self): + self.beginResetModel() + self._accounts = [] + self.endResetModel() + + # @pyqtProperty(str, notify=userinfoChanged) + # def userinfo(self): + # return self._userinfo + + @pyqtProperty(int, notify=stateChanged) + def state(self): + return self._state + + @state.setter + def state(self, state: State): + if state != self._state: + self._state = state + self.stateChanged.emit() + + @pyqtSlot(str, str) + @pyqtSlot(str, str, str) + def startScan(self, wallet_type: str, seed: str, seed_extra_words: str = None): + if not seed or not wallet_type: + return + + assert wallet_type == 'standard' + + self._root_seed = keystore.bip39_to_seed(seed, seed_extra_words) + + self.clear() + + self._thread = TaskThread(self) + network = Network.get_instance() + coro = account_discovery(network, self.get_account_xpub) + self.state = QEBip39RecoveryListModel.State.Scanning + fut = asyncio.run_coroutine_threadsafe(coro, network.asyncio_loop) + self._thread.add( + fut.result, + on_success=self.on_recovery_success, + on_error=self.on_recovery_error, + cancel=fut.cancel, + ) + + def addAccount(self, account): + self._logger.debug(f'addAccount {account!r}') + self.beginInsertRows(QModelIndex(), len(self._accounts), len(self._accounts)) + self._accounts.append(account) + self.endInsertRows() + + def on_recovery_success(self, accounts): + self.state = QEBip39RecoveryListModel.State.Success + + for account in accounts: + self.addAccount(account) + + self._thread.stop() + + def on_recovery_error(self, exc_info): + e = exc_info[1] + if isinstance(e, concurrent.futures.CancelledError): + self.state = QEBip39RecoveryListModel.State.Cancelled + return + self._logger.error(f"recovery error", exc_info=exc_info) + self.state = QEBip39RecoveryListModel.State.Failed + self._thread.stop() + + def get_account_xpub(self, account_path): + root_node = BIP32Node.from_rootseed(self._root_seed, xtype='standard') + account_node = root_node.subkey_at_private_derivation(account_path) + account_xpub = account_node.to_xpub() + return account_xpub + diff --git a/electrum/gui/qml/util.py b/electrum/gui/qml/util.py index 19db9c537..17e75186c 100644 --- a/electrum/gui/qml/util.py +++ b/electrum/gui/qml/util.py @@ -1,10 +1,16 @@ +import sys +import queue + from functools import wraps from time import time +from typing import Callable, Optional, NamedTuple -from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSignal, QThread +from electrum.logging import Logger from electrum.util import EventListener, event_listener + class QtEventListener(EventListener): qt_callback_signal = pyqtSignal(tuple) @@ -21,6 +27,7 @@ def on_qt_callback_signal(self, args): func = args[0] return func(self, *args[1:]) + # decorator for members of the QtEventListener class def qt_event_listener(func): func = event_listener(func) @@ -29,6 +36,7 @@ def decorator(self, *args): self.qt_callback_signal.emit( (func,) + args) return decorator + # return delay in msec when expiry time string should be updated # returns 0 when expired or expires > 1 day away (no updates needed) def status_update_timer_interval(exp): @@ -47,3 +55,75 @@ def status_update_timer_interval(exp): interval = 1000 * 60 * 60 return interval + +# TODO: copied from desktop client, this could be moved to a set of common code. +class TaskThread(QThread, Logger): + '''Thread that runs background tasks. Callbacks are guaranteed + to happen in the context of its parent.''' + + class Task(NamedTuple): + task: Callable + cb_success: Optional[Callable] + cb_done: Optional[Callable] + cb_error: Optional[Callable] + cancel: Optional[Callable] = None + + doneSig = pyqtSignal(object, object, object) + + def __init__(self, parent, on_error=None): + QThread.__init__(self, parent) + Logger.__init__(self) + self.on_error = on_error + self.tasks = queue.Queue() + self._cur_task = None # type: Optional[TaskThread.Task] + self._stopping = False + self.doneSig.connect(self.on_done) + self.start() + + def add(self, task, on_success=None, on_done=None, on_error=None, *, cancel=None): + if self._stopping: + self.logger.warning(f"stopping or already stopped but tried to add new task.") + return + on_error = on_error or self.on_error + task_ = TaskThread.Task(task, on_success, on_done, on_error, cancel=cancel) + self.tasks.put(task_) + + def run(self): + while True: + if self._stopping: + break + task = self.tasks.get() # type: TaskThread.Task + self._cur_task = task + if not task or self._stopping: + break + try: + result = task.task() + self.doneSig.emit(result, task.cb_done, task.cb_success) + except BaseException: + self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) + + def on_done(self, result, cb_done, cb_result): + # This runs in the parent's thread. + if cb_done: + cb_done() + if cb_result: + cb_result(result) + + def stop(self): + self._stopping = True + # try to cancel currently running task now. + # if the task does not implement "cancel", we will have to wait until it finishes. + task = self._cur_task + if task and task.cancel: + task.cancel() + # cancel the remaining tasks in the queue + while True: + try: + task = self.tasks.get_nowait() + except queue.Empty: + break + if task and task.cancel: + task.cancel() + self.tasks.put(None) # in case the thread is still waiting on the queue + self.exit() + self.wait() From cb9ba819ec12d93a545eb2286f129337a4bcbcc7 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 9 May 2023 15:39:38 +0200 Subject: [PATCH 0893/1143] qml: remove Qt Vkbd leftovers --- .../Styles/Electrum/ElectrumKeyPanel.qml | 16 - .../Electrum/images/backspace-868482.svg | 23 - .../Styles/Electrum/images/check-868482.svg | 8 - .../Styles/Electrum/images/enter-868482.svg | 13 - .../Styles/Electrum/images/globe-868482.svg | 26 - .../Electrum/images/handwriting-868482.svg | 18 - .../Electrum/images/hidekeyboard-868482.svg | 55 - .../Styles/Electrum/images/search-868482.svg | 14 - .../images/selectionhandle-bottom.svg | 201 ---- .../Styles/Electrum/images/shift-80c342.svg | 12 - .../Styles/Electrum/images/shift-868482.svg | 12 - .../Styles/Electrum/images/shift-c5d6b6.svg | 12 - .../Electrum/images/textmode-868482.svg | 33 - .../VirtualKeyboard/Styles/Electrum/style.qml | 1041 ----------------- electrum/gui/qml/components/main.qml | 1 - 15 files changed, 1485 deletions(-) delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg delete mode 100644 electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml deleted file mode 100644 index c21f91f50..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/ElectrumKeyPanel.qml +++ /dev/null @@ -1,16 +0,0 @@ -import QtQuick 2.7 -import QtQuick.VirtualKeyboard 2.1 -import QtQuick.VirtualKeyboard.Styles 2.1 - -import org.electrum 1.0 - -KeyPanel { - id: keyPanel - Connections { - target: keyPanel.control - function onPressedChanged() { - if (keyPanel.control.pressed) - AppController.haptic() - } - } -} diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg deleted file mode 100644 index 764c3c68e..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/backspace-868482.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg deleted file mode 100644 index 544fec504..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/check-868482.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg deleted file mode 100644 index 88c148666..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/enter-868482.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg deleted file mode 100644 index 7cb9b7947..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/globe-868482.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg deleted file mode 100644 index 65d378747..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/handwriting-868482.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg deleted file mode 100644 index 31e680a11..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/hidekeyboard-868482.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg deleted file mode 100644 index 4aff84996..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/search-868482.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg deleted file mode 100644 index 312e3ab50..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/selectionhandle-bottom.svg +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg deleted file mode 100644 index d39a2230d..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-80c342.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg deleted file mode 100644 index 95b6d5044..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-868482.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg deleted file mode 100644 index 22f9d5de2..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/shift-c5d6b6.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg deleted file mode 100644 index 515f5c797..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/images/textmode-868482.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml b/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml deleted file mode 100644 index 506f0ff53..000000000 --- a/electrum/gui/qml/QtQuick/VirtualKeyboard/Styles/Electrum/style.qml +++ /dev/null @@ -1,1041 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the Qt Virtual Keyboard module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 or (at your option) any later version -** approved by the KDE Free Qt Foundation. The licenses are as published by -** the Free Software Foundation and appearing in the file LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.7 -import QtQuick.VirtualKeyboard 2.1 -import QtQuick.VirtualKeyboard.Styles 2.1 - -import QtQuick.Controls.Material 2.0 - -KeyboardStyle { - id: currentStyle - - readonly property bool compactSelectionList: [InputEngine.InputMode.Pinyin, InputEngine.InputMode.Cangjie, InputEngine.InputMode.Zhuyin].indexOf(InputContext.inputEngine.inputMode) !== -1 - readonly property string fontFamily: "Sans" - readonly property real keyBackgroundMargin: Math.round(13 * scaleHint) - readonly property real keyContentMargin: Math.round(45 * scaleHint) - readonly property real keyIconScale: scaleHint * 0.6 - readonly property string resourcePrefix: '' - - readonly property string inputLocale: InputContext.locale - property color inputLocaleIndicatorColor: "white" - property Timer inputLocaleIndicatorHighlightTimer: Timer { - interval: 1000 - onTriggered: inputLocaleIndicatorColor = "gray" - } - onInputLocaleChanged: { - inputLocaleIndicatorColor = 'red' //"white" - inputLocaleIndicatorHighlightTimer.restart() - } - - keyboardDesignWidth: 2560 - keyboardDesignHeight: 1440 - keyboardRelativeLeftMargin: 32 / keyboardDesignWidth - keyboardRelativeRightMargin: 32 / keyboardDesignWidth - keyboardRelativeTopMargin: 10 / keyboardDesignHeight - keyboardRelativeBottomMargin: 28 / keyboardDesignHeight - - keyboardBackground: Rectangle { - color: constants.colorAlpha(Material.accentColor, 0.5) //mutedForeground //'red' //"black" - } - - keyPanel: ElectrumKeyPanel { - id: keyPanel - Rectangle { - id: keyBackground - radius: 5 - color: "#383533" - anchors.fill: keyPanel - anchors.margins: keyBackgroundMargin - Text { - id: keySmallText - text: control.smallText - visible: control.smallTextVisible - color: "gray" - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: keyContentMargin / 3 - font { - family: fontFamily - weight: Font.Normal - pixelSize: 38 * scaleHint * 2 - capitalization: control.uppercased ? Font.AllUppercase : Font.MixedCase - } - } - Text { - id: keyText - text: control.displayText - color: "white" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - anchors.leftMargin: keyContentMargin - anchors.topMargin: keyContentMargin - anchors.rightMargin: keyContentMargin - anchors.bottomMargin: keyContentMargin - font { - family: fontFamily - weight: Font.Normal - pixelSize: 52 * scaleHint * 2 - capitalization: control.uppercased ? Font.AllUppercase : Font.MixedCase - } - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: keyBackground - opacity: 0.75 - } - PropertyChanges { - target: keyText - opacity: 0.5 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: keyBackground - opacity: 0.75 - } - PropertyChanges { - target: keyText - opacity: 0.05 - } - } - ] - } - - backspaceKeyPanel: ElectrumKeyPanel { - id: backspaceKeyPanel - Rectangle { - id: backspaceKeyBackground - radius: 5 - color: "#23211E" - anchors.fill: backspaceKeyPanel - anchors.margins: keyBackgroundMargin - Image { - id: backspaceKeyIcon - anchors.centerIn: parent - sourceSize.width: 159 * keyIconScale - sourceSize.height: 88 * keyIconScale - smooth: false - source: resourcePrefix + "images/backspace-868482.svg" - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: backspaceKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: backspaceKeyIcon - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: backspaceKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: backspaceKeyIcon - opacity: 0.2 - } - } - ] - } - - languageKeyPanel: ElectrumKeyPanel { - id: languageKeyPanel - Rectangle { - id: languageKeyBackground - radius: 5 - color: "#35322f" - anchors.fill: languageKeyPanel - anchors.margins: keyBackgroundMargin - Image { - id: languageKeyIcon - anchors.centerIn: parent - sourceSize.width: 144 * keyIconScale - sourceSize.height: 144 * keyIconScale - smooth: false - source: resourcePrefix + "images/globe-868482.svg" - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: languageKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: languageKeyIcon - opacity: 0.75 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: languageKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: languageKeyIcon - opacity: 0.2 - } - } - ] - } - - enterKeyPanel: ElectrumKeyPanel { - id: enterKeyPanel - Rectangle { - id: enterKeyBackground - radius: 5 - color: "#1e1b18" - anchors.fill: enterKeyPanel - anchors.margins: keyBackgroundMargin - Image { - id: enterKeyIcon - visible: enterKeyText.text.length === 0 - anchors.centerIn: parent - readonly property size enterKeyIconSize: { - switch (control.actionId) { - case EnterKeyAction.Go: - case EnterKeyAction.Send: - case EnterKeyAction.Next: - case EnterKeyAction.Done: - return Qt.size(170, 119) - case EnterKeyAction.Search: - return Qt.size(148, 148) - default: - return Qt.size(211, 80) - } - } - sourceSize.width: enterKeyIconSize.width * keyIconScale - sourceSize.height: enterKeyIconSize.height * keyIconScale - smooth: false - source: { - switch (control.actionId) { - case EnterKeyAction.Go: - case EnterKeyAction.Send: - case EnterKeyAction.Next: - case EnterKeyAction.Done: - return resourcePrefix + "images/check-868482.svg" - case EnterKeyAction.Search: - return resourcePrefix + "images/search-868482.svg" - default: - return resourcePrefix + "images/enter-868482.svg" - } - } - } - Text { - id: enterKeyText - visible: text.length !== 0 - text: control.actionId !== EnterKeyAction.None ? control.displayText : "" - clip: true - fontSizeMode: Text.HorizontalFit - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: "#80c342" - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint - capitalization: Font.AllUppercase - } - anchors.fill: parent - anchors.margins: Math.round(42 * scaleHint) - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: enterKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: enterKeyIcon - opacity: 0.6 - } - PropertyChanges { - target: enterKeyText - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: enterKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: enterKeyIcon - opacity: 0.2 - } - PropertyChanges { - target: enterKeyText - opacity: 0.2 - } - } - ] - } - - hideKeyPanel: ElectrumKeyPanel { - id: hideKeyPanel - Rectangle { - id: hideKeyBackground - radius: 5 - color: "#1e1b18" - anchors.fill: hideKeyPanel - anchors.margins: keyBackgroundMargin - Image { - id: hideKeyIcon - anchors.centerIn: parent - sourceSize.width: 144 * keyIconScale - sourceSize.height: 127 * keyIconScale - smooth: false - source: resourcePrefix + "images/hidekeyboard-868482.svg" - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: hideKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: hideKeyIcon - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: hideKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: hideKeyIcon - opacity: 0.2 - } - } - ] - } - - shiftKeyPanel: ElectrumKeyPanel { - id: shiftKeyPanel - Rectangle { - id: shiftKeyBackground - radius: 5 - color: "#1e1b18" - anchors.fill: shiftKeyPanel - anchors.margins: keyBackgroundMargin - Image { - id: shiftKeyIcon - anchors.centerIn: parent - sourceSize.width: 144 * keyIconScale - sourceSize.height: 134 * keyIconScale - smooth: false - source: resourcePrefix + "images/shift-868482.svg" - } - states: [ - State { - name: "capsLockActive" - when: InputContext.capsLockActive - PropertyChanges { - target: shiftKeyBackground - color: "#5a892e" - } - PropertyChanges { - target: shiftKeyIcon - source: resourcePrefix + "images/shift-c5d6b6.svg" - } - }, - State { - name: "shiftActive" - when: InputContext.shiftActive - PropertyChanges { - target: shiftKeyIcon - source: resourcePrefix + "images/shift-80c342.svg" - } - } - ] - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: shiftKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: shiftKeyIcon - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: shiftKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: shiftKeyIcon - opacity: 0.2 - } - } - ] - } - - spaceKeyPanel: ElectrumKeyPanel { - id: spaceKeyPanel - Rectangle { - id: spaceKeyBackground - radius: 5 - color: "#35322f" - anchors.fill: spaceKeyPanel - anchors.margins: keyBackgroundMargin - Text { - id: spaceKeyText - text: Qt.locale(InputContext.locale).nativeLanguageName - color: currentStyle.inputLocaleIndicatorColor - Behavior on color { PropertyAnimation { duration: 250 } } - anchors.centerIn: parent - font { - family: fontFamily - weight: Font.Normal - pixelSize: 48 * scaleHint * 1.5 - } - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: spaceKeyBackground - opacity: 0.80 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: spaceKeyBackground - opacity: 0.8 - } - } - ] - } - - symbolKeyPanel: ElectrumKeyPanel { - id: symbolKeyPanel - Rectangle { - id: symbolKeyBackground - radius: 5 - color: "#1e1b18" - anchors.fill: symbolKeyPanel - anchors.margins: keyBackgroundMargin - Text { - id: symbolKeyText - text: control.displayText - color: "white" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - anchors.margins: keyContentMargin - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint * 2 - capitalization: Font.AllUppercase - } - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: symbolKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: symbolKeyText - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: symbolKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: symbolKeyText - opacity: 0.2 - } - } - ] - } - - modeKeyPanel: ElectrumKeyPanel { - id: modeKeyPanel - Rectangle { - id: modeKeyBackground - radius: 5 - color: "#1e1b18" - anchors.fill: modeKeyPanel - anchors.margins: keyBackgroundMargin - Text { - id: modeKeyText - text: control.displayText - color: "white" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - anchors.margins: keyContentMargin - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint - capitalization: Font.AllUppercase - } - } - Rectangle { - id: modeKeyIndicator - implicitHeight: parent.height * 0.1 - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.leftMargin: parent.width * 0.4 - anchors.rightMargin: parent.width * 0.4 - anchors.bottomMargin: parent.height * 0.12 - color: "#80c342" - radius: 3 - visible: control.mode - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: modeKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: modeKeyText - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: modeKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: modeKeyText - opacity: 0.2 - } - } - ] - } - - handwritingKeyPanel: ElectrumKeyPanel { - id: handwritingKeyPanel - Rectangle { - id: hwrKeyBackground - radius: 5 - color: "#35322f" - anchors.fill: handwritingKeyPanel - anchors.margins: keyBackgroundMargin - Image { - id: hwrKeyIcon - anchors.centerIn: parent - readonly property size hwrKeyIconSize: keyboard.handwritingMode ? Qt.size(124, 96) : Qt.size(156, 104) - sourceSize.width: hwrKeyIconSize.width * keyIconScale - sourceSize.height: hwrKeyIconSize.height * keyIconScale - smooth: false - source: resourcePrefix + (keyboard.handwritingMode ? "images/textmode-868482.svg" : "images/handwriting-868482.svg") - } - } - states: [ - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: hwrKeyBackground - opacity: 0.80 - } - PropertyChanges { - target: hwrKeyIcon - opacity: 0.6 - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: hwrKeyBackground - opacity: 0.8 - } - PropertyChanges { - target: hwrKeyIcon - opacity: 0.2 - } - } - ] - } - - characterPreviewMargin: 0 - characterPreviewDelegate: Item { - property string text - id: characterPreview - Rectangle { - id: characterPreviewBackground - anchors.fill: parent - color: "#5d5b59" - radius: 5 - Text { - id: characterPreviewText - color: "white" - text: characterPreview.text - fontSizeMode: Text.HorizontalFit - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - anchors.margins: Math.round(48 * scaleHint) - font { - family: fontFamily - weight: Font.Normal - pixelSize: 82 * scaleHint * 2 - } - } - } - } - - alternateKeysListItemWidth: 99 * scaleHint * 2 - alternateKeysListItemHeight: 150 * scaleHint * 2 - alternateKeysListDelegate: Item { - id: alternateKeysListItem - width: alternateKeysListItemWidth - height: alternateKeysListItemHeight - Text { - id: listItemText - text: model.text - color: "#868482" - font { - family: fontFamily - weight: Font.Normal - pixelSize: 52 * scaleHint * 2 - } - anchors.centerIn: parent - } - states: State { - name: "current" - when: alternateKeysListItem.ListView.isCurrentItem - PropertyChanges { - target: listItemText - color: "white" - } - } - } - alternateKeysListHighlight: Rectangle { - color: "#5d5b59" - radius: 5 - } - alternateKeysListBackground: Rectangle { - color: "#1e1b18" - radius: 5 - } - - selectionListHeight: 85 * scaleHint * 2 - selectionListDelegate: SelectionListItem { - id: selectionListItem - width: Math.round(selectionListLabel.width + selectionListLabel.anchors.leftMargin * 2) - Text { - id: selectionListLabel - anchors.left: parent.left - anchors.leftMargin: Math.round((compactSelectionList ? 50 : 140) * scaleHint) - anchors.verticalCenter: parent.verticalCenter - text: decorateText(display, wordCompletionLength) - color: "#80c342" - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint * 2 - } - function decorateText(text, wordCompletionLength) { - if (wordCompletionLength > 0) { - return text.slice(0, -wordCompletionLength) + '' + text.slice(-wordCompletionLength) + '' - } - return text - } - } - Rectangle { - id: selectionListSeparator - width: 4 * scaleHint - height: 36 * scaleHint - radius: 2 - color: "#35322f" - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.left - } - states: State { - name: "current" - when: selectionListItem.ListView.isCurrentItem - PropertyChanges { - target: selectionListLabel - color: "white" - } - } - } - selectionListBackground: Rectangle { - color: "#1e1b18" - } - selectionListAdd: Transition { - NumberAnimation { property: "y"; from: wordCandidateView.height; duration: 200 } - NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 200 } - } - selectionListRemove: Transition { - NumberAnimation { property: "y"; to: -wordCandidateView.height; duration: 200 } - NumberAnimation { property: "opacity"; to: 0; duration: 200 } - } - - navigationHighlight: Rectangle { - color: "transparent" - border.color: "yellow" - border.width: 5 - } - - traceInputKeyPanelDelegate: TraceInputKeyPanel { - id: traceInputKeyPanel - traceMargins: keyBackgroundMargin - Rectangle { - id: traceInputKeyPanelBackground - radius: 5 - color: "#35322f" - anchors.fill: traceInputKeyPanel - anchors.margins: keyBackgroundMargin - Text { - id: hwrInputModeIndicator - visible: control.patternRecognitionMode === InputEngine.PatternRecognitionMode.Handwriting - text: { - switch (InputContext.inputEngine.inputMode) { - case InputEngine.InputMode.Numeric: - if (["ar", "fa"].indexOf(InputContext.locale.substring(0, 2)) !== -1) - return "\u0660\u0661\u0662" - // Fallthrough - case InputEngine.InputMode.Dialable: - return "123" - case InputEngine.InputMode.Greek: - return "ΑΒΓ" - case InputEngine.InputMode.Cyrillic: - return "АБВ" - case InputEngine.InputMode.Arabic: - if (InputContext.locale.substring(0, 2) === "fa") - return "\u0627\u200C\u0628\u200C\u067E" - return "\u0623\u200C\u0628\u200C\u062C" - case InputEngine.InputMode.Hebrew: - return "\u05D0\u05D1\u05D2" - case InputEngine.InputMode.ChineseHandwriting: - return "中文" - case InputEngine.InputMode.JapaneseHandwriting: - return "日本語" - case InputEngine.InputMode.KoreanHandwriting: - return "한국어" - case InputEngine.InputMode.Thai: - return "กขค" - default: - return "Abc" - } - } - color: "white" - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: keyContentMargin - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint - capitalization: { - if (InputContext.capsLockActive) - return Font.AllUppercase - if (InputContext.shiftActive) - return Font.MixedCase - return Font.AllLowercase - } - } - } - } - Canvas { - id: traceInputKeyGuideLines - anchors.fill: traceInputKeyPanelBackground - opacity: 0.1 - onPaint: { - var ctx = getContext("2d") - ctx.lineWidth = 1 - ctx.strokeStyle = Qt.rgba(0xFF, 0xFF, 0xFF) - ctx.clearRect(0, 0, width, height) - var i - var margin = Math.round(30 * scaleHint) - if (control.horizontalRulers) { - for (i = 0; i < control.horizontalRulers.length; i++) { - ctx.beginPath() - var y = Math.round(control.horizontalRulers[i]) - var rightMargin = Math.round(width - margin) - if (i + 1 === control.horizontalRulers.length) { - ctx.moveTo(margin, y) - ctx.lineTo(rightMargin, y) - } else { - var dashLen = Math.round(20 * scaleHint) - for (var dash = margin, dashCount = 0; - dash < rightMargin; dash += dashLen, dashCount++) { - if ((dashCount & 1) === 0) { - ctx.moveTo(dash, y) - ctx.lineTo(Math.min(dash + dashLen, rightMargin), y) - } - } - } - ctx.stroke() - } - } - if (control.verticalRulers) { - for (i = 0; i < control.verticalRulers.length; i++) { - ctx.beginPath() - ctx.moveTo(control.verticalRulers[i], margin) - ctx.lineTo(control.verticalRulers[i], Math.round(height - margin)) - ctx.stroke() - } - } - } - Connections { - target: control - onHorizontalRulersChanged: traceInputKeyGuideLines.requestPaint() - onVerticalRulersChanged: traceInputKeyGuideLines.requestPaint() - } - } - } - - traceCanvasDelegate: TraceCanvas { - id: traceCanvas - onAvailableChanged: { - if (!available) - return - var ctx = getContext("2d") - if (parent.canvasType === "fullscreen") { - ctx.lineWidth = 10 - ctx.strokeStyle = Qt.rgba(0, 0, 0) - } else { - ctx.lineWidth = 10 * scaleHint - ctx.strokeStyle = Qt.rgba(0xFF, 0xFF, 0xFF) - } - ctx.lineCap = "round" - ctx.fillStyle = ctx.strokeStyle - } - autoDestroyDelay: 800 - onTraceChanged: if (trace === null) opacity = 0 - Behavior on opacity { PropertyAnimation { easing.type: Easing.OutCubic; duration: 150 } } - } - - popupListDelegate: SelectionListItem { - property real cursorAnchor: popupListLabel.x + popupListLabel.width - id: popupListItem - width: popupListLabel.width + popupListLabel.anchors.leftMargin * 2 - height: popupListLabel.height + popupListLabel.anchors.topMargin * 2 - Text { - id: popupListLabel - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: popupListLabel.height / 2 - anchors.topMargin: popupListLabel.height / 3 - text: decorateText(display, wordCompletionLength) - color: "#5CAA15" - font { - family: fontFamily - weight: Font.Normal - pixelSize: Qt.inputMethod.cursorRectangle.height * 0.8 - } - function decorateText(text, wordCompletionLength) { - if (wordCompletionLength > 0) { - return text.slice(0, -wordCompletionLength) + '' + text.slice(-wordCompletionLength) + '' - } - return text - } - } - states: State { - name: "current" - when: popupListItem.ListView.isCurrentItem - PropertyChanges { - target: popupListLabel - color: "black" - } - } - } - - popupListBackground: Item { - Rectangle { - width: parent.width - height: parent.height - color: "white" - border { - width: 1 - color: "#929495" - } - } - } - - popupListAdd: Transition { - NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 200 } - } - - popupListRemove: Transition { - NumberAnimation { property: "opacity"; to: 0; duration: 200 } - } - - languagePopupListEnabled: true - - languageListDelegate: SelectionListItem { - id: languageListItem - width: languageNameTextMetrics.width * 17 - height: languageNameTextMetrics.height + languageListLabel.anchors.topMargin + languageListLabel.anchors.bottomMargin - Text { - id: languageListLabel - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: languageNameTextMetrics.height / 2 - anchors.rightMargin: anchors.leftMargin - anchors.topMargin: languageNameTextMetrics.height / 3 - anchors.bottomMargin: anchors.topMargin - text: languageNameFormatter.elidedText - // color: "#5CAA15" - color: constants.mutedForeground - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint * 2 - } - } - TextMetrics { - id: languageNameTextMetrics - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint * 2 - } - text: "X" - } - TextMetrics { - id: languageNameFormatter - font { - family: fontFamily - weight: Font.Normal - pixelSize: 44 * scaleHint * 2 - } - elide: Text.ElideRight - elideWidth: languageListItem.width - languageListLabel.anchors.leftMargin - languageListLabel.anchors.rightMargin - text: displayName - } - states: State { - name: "current" - when: languageListItem.ListView.isCurrentItem - PropertyChanges { - target: languageListLabel - color: 'white' - } - } - } - - languageListBackground: Rectangle { - color: constants.lighterBackground - - border { - width: 1 - color: "#929495" - } - } - - languageListAdd: Transition { - NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 200 } - } - - languageListRemove: Transition { - NumberAnimation { property: "opacity"; to: 0; duration: 200 } - } - - selectionHandle: Image { - sourceSize.width: 20 - source: resourcePrefix + "images/selectionhandle-bottom.svg" - } - - fullScreenInputContainerBackground: Rectangle { - color: "#FFF" - } - - fullScreenInputBackground: Rectangle { - color: "#FFF" - } - - fullScreenInputMargins: Math.round(15 * scaleHint) - - fullScreenInputPadding: Math.round(30 * scaleHint) - - fullScreenInputCursor: Rectangle { - width: 1 - color: "#000" - visible: parent.blinkStatus - } - - fullScreenInputFont.pixelSize: 58 * scaleHint -} diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 1dd04544a..7ecd533bc 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -7,7 +7,6 @@ import QtQuick.Window 2.15 import QtQml 2.6 import QtMultimedia 5.6 -import QtQuick.VirtualKeyboard 2.15 import org.electrum 1.0 From 4df6052567cb7a53f609ae343c575cf3fae9f73f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 9 May 2023 15:43:11 +0200 Subject: [PATCH 0894/1143] qml: add validators for BtcField and FiatField controls --- electrum/exchange_rate.py | 3 +++ electrum/gui/qml/components/controls/BtcField.qml | 6 +++++- electrum/gui/qml/components/controls/FiatField.qml | 6 +++++- electrum/gui/qml/qeconfig.py | 13 +++++++++++-- electrum/gui/qml/qefx.py | 11 ++++++++++- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index ea0a79ca7..f3c67712d 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -567,6 +567,9 @@ def ccy_amount_str(self, amount, *, add_thousands_sep: bool = False, ccy=None) - return text return text[:dp_loc] + util.DECIMAL_POINT + text[dp_loc+1:] + def ccy_precision(self, ccy=None) -> int: + return CCY_PRECISIONS.get(self.ccy if ccy is None else ccy, 2) + async def run(self): while True: # every few minutes, refresh spot price diff --git a/electrum/gui/qml/components/controls/BtcField.qml b/electrum/gui/qml/components/controls/BtcField.qml index 3afeaa029..4414dd709 100644 --- a/electrum/gui/qml/components/controls/BtcField.qml +++ b/electrum/gui/qml/components/controls/BtcField.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.15 import QtQuick.Controls 2.0 import org.electrum 1.0 @@ -11,6 +11,10 @@ TextField { font.family: FixedFont placeholderText: qsTr('Amount') inputMethodHints: Qt.ImhDigitsOnly + validator: RegularExpressionValidator { + regularExpression: Config.btcAmountRegex + } + property Amount textAsSats onTextChanged: { textAsSats = Config.unitsToSats(amount.text) diff --git a/electrum/gui/qml/components/controls/FiatField.qml b/electrum/gui/qml/components/controls/FiatField.qml index fff8150e0..47cb53694 100644 --- a/electrum/gui/qml/components/controls/FiatField.qml +++ b/electrum/gui/qml/components/controls/FiatField.qml @@ -1,4 +1,4 @@ -import QtQuick 2.6 +import QtQuick 2.15 import QtQuick.Controls 2.0 import org.electrum 1.0 @@ -11,6 +11,10 @@ TextField { font.family: FixedFont placeholderText: qsTr('Amount') inputMethodHints: Qt.ImhDigitsOnly + validator: RegularExpressionValidator { + regularExpression: Daemon.fx.fiatAmountRegex + } + onTextChanged: { if (amountFiat.activeFocus) btcfield.text = text == '' diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index b71811c94..5f1735fa8 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -2,11 +2,11 @@ from decimal import Decimal from typing import TYPE_CHECKING -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression from electrum.i18n import set_language, languages from electrum.logging import get_logger -from electrum.util import DECIMAL_POINT_DEFAULT, format_satoshis +from electrum.util import DECIMAL_POINT_DEFAULT, base_unit_name_to_decimal_point from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING from .qetypes import QEAmount @@ -82,6 +82,15 @@ def baseUnit(self, unit): self.config.set_base_unit(unit) self.baseUnitChanged.emit() + @pyqtProperty('QRegularExpression', notify=baseUnitChanged) + def btcAmountRegex(self): + decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit()) + exp = '[0-9]{0,8}' + if decimal_point: + exp += '\\.' + exp += '[0-9]{0,%d}' % decimal_point + return QRegularExpression(exp) + thousandsSeparatorChanged = pyqtSignal() @pyqtProperty(bool, notify=thousandsSeparatorChanged) def thousandsSeparator(self): diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py index ce69cc304..c742d990d 100644 --- a/electrum/gui/qml/qefx.py +++ b/electrum/gui/qml/qefx.py @@ -1,7 +1,7 @@ from datetime import datetime from decimal import Decimal -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression from electrum.bitcoin import COIN from electrum.exchange_rate import FxThread @@ -60,6 +60,15 @@ def fiatCurrency(self, currency): self.fiatCurrencyChanged.emit() self.rateSourcesChanged.emit() + @pyqtProperty('QRegularExpression', notify=fiatCurrencyChanged) + def fiatAmountRegex(self): + decimals = self.fx.ccy_precision() + exp = '[0-9]*' + if decimals: + exp += '\\.' + exp += '[0-9]{0,%d}' % decimals + return QRegularExpression(exp) + historicRatesChanged = pyqtSignal() @pyqtProperty(bool, notify=historicRatesChanged) def historicRates(self): From daf1f3741973853010c1ed9d1a2a82c402fa2040 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 8 May 2023 14:36:54 +0200 Subject: [PATCH 0895/1143] qml: add share toolbutton for outputs in TxDetails --- electrum/gui/qml/components/TxDetails.qml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 2421cb1c5..8cc1d98fb 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -302,6 +302,17 @@ Pane { font.pixelSize: constants.fontSizeMedium color: Material.accentColor } + ToolButton { + icon.source: '../../icons/share.png' + icon.color: 'transparent' + onClicked: { + var dialog = app.genericShareDialog.createObject(root, { + title: qsTr('Tx Output'), + text: modelData.address + }) + dialog.open() + } + } } } } From ab19ece4e08b513bee09937ab71fb976f8026615 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 8 May 2023 15:24:17 +0200 Subject: [PATCH 0896/1143] qml: refactor TxOutput into reusable component --- .../gui/qml/components/ConfirmTxDialog.qml | 31 ++--------- .../gui/qml/components/CpfpBumpFeeDialog.qml | 31 ++--------- .../gui/qml/components/RbfBumpFeeDialog.qml | 31 ++--------- .../gui/qml/components/RbfCancelDialog.qml | 31 ++--------- electrum/gui/qml/components/TxDetails.qml | 41 +------------- .../gui/qml/components/controls/TxOutput.qml | 54 +++++++++++++++++++ electrum/gui/qml/qetxfinalizer.py | 2 +- 7 files changed, 69 insertions(+), 152 deletions(-) create mode 100644 electrum/gui/qml/components/controls/TxOutput.qml diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 48d1e13a0..8fd8e0075 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -182,37 +182,12 @@ ElDialog { Repeater { model: finalizer.outputs - delegate: TextHighlightPane { + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine - ? modelData.is_change - ? constants.colorAddressInternal - : constants.colorAddressExternal - : modelData.is_billing - ? constants.colorAddressBilling - : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor - } - } + allowShare: false + model: modelData } } } diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index 14fb8ad60..a7a8a34a0 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -185,37 +185,12 @@ ElDialog { Repeater { model: cpfpfeebumper.valid ? cpfpfeebumper.outputs : [] - delegate: TextHighlightPane { + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine - ? modelData.is_change - ? constants.colorAddressInternal - : constants.colorAddressExternal - : modelData.is_billing - ? constants.colorAddressBilling - : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor - } - } + allowShare: false + model: modelData } } } diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 5939e5e36..dc0dedc3d 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -198,37 +198,12 @@ ElDialog { Repeater { model: rbffeebumper.valid ? rbffeebumper.outputs : [] - delegate: TextHighlightPane { + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine - ? modelData.is_change - ? constants.colorAddressInternal - : constants.colorAddressExternal - : modelData.is_billing - ? constants.colorAddressBilling - : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor - } - } + allowShare: false + model: modelData } } } diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index aa8b84227..315a36362 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -157,37 +157,12 @@ ElDialog { Repeater { model: txcanceller.valid ? txcanceller.outputs : [] - delegate: TextHighlightPane { + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine - ? modelData.is_change - ? constants.colorAddressInternal - : constants.colorAddressExternal - : modelData.is_billing - ? constants.colorAddressBilling - : Material.foreground - } - Label { - text: Config.formatSats(modelData.value_sats) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor - } - } + allowShare: false + model: modelData } } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 8cc1d98fb..99409e392 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -272,48 +272,11 @@ Pane { Repeater { model: txdetails.outputs - delegate: TextHighlightPane { + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true - RowLayout { - width: parent.width - Label { - text: modelData.address - Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: modelData.is_mine - ? modelData.is_change - ? constants.colorAddressInternal - : constants.colorAddressExternal - : modelData.is_billing - ? constants.colorAddressBilling - : Material.foreground - } - Label { - text: Config.formatSats(modelData.value) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor - } - ToolButton { - icon.source: '../../icons/share.png' - icon.color: 'transparent' - onClicked: { - var dialog = app.genericShareDialog.createObject(root, { - title: qsTr('Tx Output'), - text: modelData.address - }) - dialog.open() - } - } - } + model: modelData } } } diff --git a/electrum/gui/qml/components/controls/TxOutput.qml b/electrum/gui/qml/components/controls/TxOutput.qml new file mode 100644 index 000000000..fcfd5d670 --- /dev/null +++ b/electrum/gui/qml/components/controls/TxOutput.qml @@ -0,0 +1,54 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.0 +import QtQuick.Controls.Material 2.0 + +import org.electrum 1.0 + +TextHighlightPane { + id: root + + property variant model + property bool allowShare: true + + RowLayout { + width: parent.width + Label { + text: model.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + color: model.is_mine + ? model.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : model.is_billing + ? constants.colorAddressBilling + : Material.foreground + } + Label { + text: Config.formatSats(model.value) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } + ToolButton { + visible: allowShare + icon.source: Qt.resolvedUrl('../../../icons/share.png') + icon.color: 'transparent' + onClicked: { + var dialog = app.genericShareDialog.createObject(app, { + title: qsTr('Tx Output'), + text: model.address + }) + dialog.open() + } + } + } +} + diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index d0dba05f7..1af467533 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -213,7 +213,7 @@ def update_outputs_from_tx(self, tx): for o in tx.outputs(): outputs.append({ 'address': o.get_ui_address_str(), - 'value_sats': o.value, + 'value': o.value, 'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()), 'is_change': self._wallet.wallet.is_change(o.get_ui_address_str()), 'is_billing': self._wallet.wallet.is_billing_address(o.get_ui_address_str()) From a8a0b36fdb3277c3fad6bf6d161418c4645c038b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 9 May 2023 16:15:09 +0200 Subject: [PATCH 0897/1143] qml: code style, imports --- electrum/gui/qml/qebip39recovery.py | 8 +------ electrum/gui/qml/qechannelopener.py | 33 +-------------------------- electrum/gui/qml/qeserverlistmodel.py | 8 ++----- electrum/gui/qml/qetxfinalizer.py | 11 +++++---- 4 files changed, 11 insertions(+), 49 deletions(-) diff --git a/electrum/gui/qml/qebip39recovery.py b/electrum/gui/qml/qebip39recovery.py index f5632e1a8..26a409ee3 100644 --- a/electrum/gui/qml/qebip39recovery.py +++ b/electrum/gui/qml/qebip39recovery.py @@ -11,6 +11,7 @@ from .util import TaskThread + class QEBip39RecoveryListModel(QAbstractListModel): _logger = get_logger(__name__) @@ -38,8 +39,6 @@ def __init__(self, config, parent=None): self._thread = None self._root_seed = None self._state = QEBip39RecoveryListModel.State.Idle - # self._busy = False - # self._userinfo = '' def rowCount(self, index): return len(self._accounts) @@ -60,10 +59,6 @@ def clear(self): self._accounts = [] self.endResetModel() - # @pyqtProperty(str, notify=userinfoChanged) - # def userinfo(self): - # return self._userinfo - @pyqtProperty(int, notify=stateChanged) def state(self): return self._state @@ -126,4 +121,3 @@ def get_account_xpub(self, account_path): account_node = root_node.subkey_at_private_derivation(account_path) account_xpub = account_node.to_xpub() return account_xpub - diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 833930ced..fff270c00 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -7,7 +7,7 @@ from electrum.i18n import _ from electrum.gui import messages from electrum.util import bfh -from electrum.lnutil import extract_nodeid, LNPeerAddr, ln_dummy_address, ConnStringFormatError +from electrum.lnutil import extract_nodeid, ln_dummy_address, ConnStringFormatError from electrum.lnworker import hardcoded_trampoline_nodes from electrum.logging import get_logger @@ -219,37 +219,6 @@ def open_thread(): self.channelOpening.emit(conn_str) threading.Thread(target=open_thread, daemon=True).start() - # TODO: it would be nice to show this before broadcasting - #if chan.has_onchain_backup(): - #self.maybe_show_funding_tx(chan, funding_tx) - #else: - #title = _('Save backup') - #help_text = messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL - #data = lnworker.export_channel_backup(chan.channel_id) - #popup = QRDialog( - #title, data, - #show_text=False, - #text_for_clipboard=data, - #help_text=help_text, - #close_button_text=_('OK'), - #on_close=lambda: self.maybe_show_funding_tx(chan, funding_tx)) - #popup.open() - - - #def maybe_show_funding_tx(self, chan, funding_tx): - #n = chan.constraints.funding_txn_minimum_depth - #message = '\n'.join([ - #_('Channel established.'), - #_('Remote peer ID') + ':' + chan.node_id.hex(), - #_('This channel will be usable after {} confirmations').format(n) - #]) - #if not funding_tx.is_complete(): - #message += '\n\n' + _('Please sign and broadcast the funding transaction') - #self.app.show_info(message) - - #if not funding_tx.is_complete(): - #self.app.tx_dialog(funding_tx) - @pyqtSlot(str, result=str) def channelBackup(self, cid): return self._wallet.wallet.lnworker.export_channel_backup(bfh(cid)) diff --git a/electrum/gui/qml/qeserverlistmodel.py b/electrum/gui/qml/qeserverlistmodel.py index b5733d0bd..aee729d7b 100644 --- a/electrum/gui/qml/qeserverlistmodel.py +++ b/electrum/gui/qml/qeserverlistmodel.py @@ -1,15 +1,13 @@ -from abc import abstractmethod - from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex -from electrum.i18n import _ from electrum.logging import get_logger from electrum.util import Satoshis, format_time from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL from electrum import blockchain -from .util import QtEventListener, qt_event_listener, event_listener +from .util import QtEventListener, qt_event_listener + class QEServerListModel(QAbstractListModel, QtEventListener): _logger = get_logger(__name__) @@ -108,7 +106,6 @@ def initModel(self): server['address'] = i.server.to_friendly_name() server['height'] = i.tip - #self._logger.debug(f'adding server: {repr(server)}') servers.append(server) # disconnected servers @@ -132,7 +129,6 @@ def initModel(self): server['name'] = s.net_addr_str() server['address'] = server['name'] - # self._logger.debug(f'adding server: {repr(server)}') servers.append(server) self.beginInsertRows(QModelIndex(), 0, len(servers) - 1) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 1af467533..73bd2e305 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -9,13 +9,13 @@ from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction from electrum.util import NotEnoughFunds, profiler from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP -from electrum.network import NetworkException from electrum.plugin import run_hook from .qewallet import QEWallet from .qetypes import QEAmount from .util import QtEventListener, event_listener + class FeeSlider(QObject): def __init__(self, parent=None): super().__init__(parent) @@ -125,6 +125,7 @@ def save_config(self): def update(self): raise NotImplementedError() + class TxFeeSlider(FeeSlider): def __init__(self, parent=None): super().__init__(parent) @@ -220,6 +221,7 @@ def update_outputs_from_tx(self, tx): }) self.outputs = outputs + class QETxFinalizer(TxFeeSlider): _logger = get_logger(__name__) @@ -402,6 +404,7 @@ def getSerializedTx(self): txqr = self._tx.to_qr_data() return [str(self._tx), txqr[0], txqr[1]] + # mixin for watching an existing TX based on its txid for verified event # requires self._wallet to contain a QEWallet instance # exposes txid qt property @@ -448,6 +451,7 @@ def get_tx(self): def tx_verified(self): pass + class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin): _logger = get_logger(__name__) @@ -578,6 +582,7 @@ def update(self): def getNewTx(self): return str(self._tx) + class QETxCanceller(TxFeeSlider, TxMonMixin): _logger = get_logger(__name__) @@ -612,7 +617,6 @@ def oldfeeRate(self, oldfeerate): self._oldfee_rate = oldfeerate self.oldfeeRateChanged.emit() - def get_tx(self): assert self._txid self._orig_tx = self._wallet.wallet.db.get_transaction(self._txid) @@ -674,6 +678,7 @@ def update(self): def getNewTx(self): return str(self._tx) + class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin): _logger = get_logger(__name__) @@ -743,7 +748,6 @@ def outputAmount(self): def totalSize(self): return self._total_size - def get_tx(self): assert self._txid self._parent_tx = self._wallet.wallet.db.get_transaction(self._txid) @@ -806,7 +810,6 @@ def update(self): self.warning = _('Max fee exceeded') return - comb_fee = fee + self._parent_fee comb_feerate = comb_fee / self._total_size From 9e13246be845b4810f625a4986c03ec9d4b3c676 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 9 May 2023 16:17:04 +0000 Subject: [PATCH 0898/1143] ledger: fix Ledger_Client_Legacy.sign_transaction() fix signing txs when using old "bitcoin app" (pre-2.1) on the ledger device ``` 33.36 | W | transaction | heyheyhey. cp1. include_sigs=True force_legacy=False use_segwit_ser=True 33.36 | W | transaction | heyheyhey. cp2. branch1 33.37 | E | plugins.ledger.ledger | Traceback (most recent call last): File "...\electrum\electrum\plugins\ledger\ledger.py", line 669, in sign_transaction rawTx = tx.serialize_to_network() File "...\electrum\electrum\transaction.py", line 945, in serialize_to_network witness = ''.join(self.serialize_witness(x, estimate_size=estimate_size) for x in inputs) File "...\electrum\electrum\transaction.py", line 945, in witness = ''.join(self.serialize_witness(x, estimate_size=estimate_size) for x in inputs) File "...\electrum\electrum\transaction.py", line 839, in serialize_witness sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs) File "...\electrum\electrum\descriptor.py", line 378, in satisfy sol = self._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) File "...\electrum\electrum\descriptor.py", line 574, in _satisfy_inner raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") electrum.descriptor.MissingSolutionPiece: no sig for 033e92e55923ea25809790f292ee9bd410355ee02492472d9a1ff1b364874d0679 33.38 | I | plugins.ledger.ledger | no sig for 033e92e55923ea25809790f292ee9bd410355ee02492472d9a1ff1b364874d0679 ``` fixes https://github.com/spesmilo/electrum/issues/8365 regression from https://github.com/spesmilo/electrum/pull/8230 --- electrum/plugins/ledger/ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 9c09599b0..950297229 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -666,7 +666,7 @@ def is_txin_legacy_multisig(txin: PartialTxInput) -> bool: # Sign all inputs firstTransaction = True inputIndex = 0 - rawTx = tx.serialize_to_network() + rawTx = tx.serialize_to_network(include_sigs=False) if self.is_hw1(): self.dongleObject.enableAlternate2fa(False) if segwitTransaction: From 6fade55dd6a981b79be234bbfa6021e79cf7b047 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 May 2023 12:22:48 +0000 Subject: [PATCH 0899/1143] bolts: do not disconnect when receiving/sending "warning" messages follow https://github.com/lightning/bolts/pull/1075 --- electrum/lnpeer.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index d143476da..7d034a06d 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -102,7 +102,7 @@ def __init__( self.reply_channel_range = asyncio.Queue() # gossip uses a single queue to preserve message order self.gossip_queue = asyncio.Queue() - self.ordered_message_queues = defaultdict(asyncio.Queue) # for messages that are ordered + self.ordered_message_queues = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue] # for messages that are ordered self.temp_id_to_id = {} # type: Dict[bytes, Optional[bytes]] # to forward error messages self.funding_created_sent = set() # for channels in PREOPENING self.funding_signed_sent = set() # for channels in PREOPENING @@ -242,24 +242,18 @@ def process_message(self, message: bytes): def on_warning(self, payload): chan_id = payload.get("channel_id") err_bytes = payload['data'] + is_known_chan_id = (chan_id in self.channels) or (chan_id in self.temp_id_to_id) self.logger.info(f"remote peer sent warning [DO NOT TRUST THIS MESSAGE]: " - f"{error_text_bytes_to_safe_str(err_bytes)}. chan_id={chan_id.hex()}") - if chan_id in self.channels: - self.ordered_message_queues[chan_id].put_nowait((None, {'warning': err_bytes})) - elif chan_id in self.temp_id_to_id: - chan_id = self.temp_id_to_id[chan_id] or chan_id - self.ordered_message_queues[chan_id].put_nowait((None, {'warning': err_bytes})) - else: - # if no existing channel is referred to by channel_id: - # - MUST ignore the message. - return - raise GracefulDisconnect + f"{error_text_bytes_to_safe_str(err_bytes)}. chan_id={chan_id.hex()}. " + f"{is_known_chan_id=}") def on_error(self, payload): chan_id = payload.get("channel_id") err_bytes = payload['data'] + is_known_chan_id = (chan_id in self.channels) or (chan_id in self.temp_id_to_id) self.logger.info(f"remote peer sent error [DO NOT TRUST THIS MESSAGE]: " - f"{error_text_bytes_to_safe_str(err_bytes)}. chan_id={chan_id.hex()}") + f"{error_text_bytes_to_safe_str(err_bytes)}. chan_id={chan_id.hex()}. " + f"{is_known_chan_id=}") if chan_id in self.channels: self.schedule_force_closing(chan_id) self.ordered_message_queues[chan_id].put_nowait((None, {'error': err_bytes})) @@ -278,7 +272,7 @@ def on_error(self, payload): return raise GracefulDisconnect - async def send_warning(self, channel_id: bytes, message: str = None, *, close_connection=True): + async def send_warning(self, channel_id: bytes, message: str = None, *, close_connection=False): """Sends a warning and disconnects if close_connection. Note: @@ -335,15 +329,14 @@ def on_ping(self, payload): def on_pong(self, payload): self.pong_event.set() - async def wait_for_message(self, expected_name, channel_id): + async def wait_for_message(self, expected_name: str, channel_id: bytes): q = self.ordered_message_queues[channel_id] name, payload = await asyncio.wait_for(q.get(), LN_P2P_NETWORK_TIMEOUT) - # raise exceptions for errors/warnings, so that the caller sees them - if (err_bytes := (payload.get("error") or payload.get("warning"))) is not None: - err_type = "error" if payload.get("error") else "warning" + # raise exceptions for errors, so that the caller sees them + if (err_bytes := payload.get("error")) is not None: err_text = error_text_bytes_to_safe_str(err_bytes) raise GracefulDisconnect( - f"remote peer sent {err_type} [DO NOT TRUST THIS MESSAGE]: {err_text}") + f"remote peer sent error [DO NOT TRUST THIS MESSAGE]: {err_text}") if name != expected_name: raise Exception(f"Received unexpected '{name}'") return payload From 19759281ef307c71c61a77d47699373fea184065 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 10 May 2023 13:06:38 +0000 Subject: [PATCH 0900/1143] qml: follow-up BtcField validator: take base unit fully into account Not just for the fractional part, but also for the integer part. follow-up 4df6052567cb7a53f609ae343c575cf3fae9f73f --- electrum/gui/qml/qeconfig.py | 8 ++++++-- electrum/util.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 5f1735fa8..93e45460d 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -4,6 +4,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression +from electrum.bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC from electrum.i18n import set_language, languages from electrum.logging import get_logger from electrum.util import DECIMAL_POINT_DEFAULT, base_unit_name_to_decimal_point @@ -85,8 +86,11 @@ def baseUnit(self, unit): @pyqtProperty('QRegularExpression', notify=baseUnitChanged) def btcAmountRegex(self): decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit()) - exp = '[0-9]{0,8}' - if decimal_point: + max_digits_before_dp = ( + len(str(TOTAL_COIN_SUPPLY_LIMIT_IN_BTC)) + + (base_unit_name_to_decimal_point("BTC") - decimal_point)) + exp = '[0-9]{0,%d}' % max_digits_before_dp + if decimal_point > 0: exp += '\\.' exp += '[0-9]{0,%d}' % decimal_point return QRegularExpression(exp) diff --git a/electrum/util.py b/electrum/util.py index 15da9e832..05131e812 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -104,6 +104,7 @@ def decimal_point_to_base_unit_name(dp: int) -> str: def base_unit_name_to_decimal_point(unit_name: str) -> int: + """Returns the max number of digits allowed after the decimal point.""" # e.g. "BTC" -> 8 try: return base_units[unit_name] From f40d603e6404c3df8d8daef2f15d77286d5c842b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 10 May 2023 17:16:35 +0200 Subject: [PATCH 0901/1143] qml: styling bip39 refine and recovery --- .../qml/components/BIP39RecoveryDialog.qml | 15 +++- .../qml/components/wizard/WCBIP39Refine.qml | 74 ++++++++++--------- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qml/components/BIP39RecoveryDialog.qml b/electrum/gui/qml/components/BIP39RecoveryDialog.qml index 8585a0f04..89b23061d 100644 --- a/electrum/gui/qml/components/BIP39RecoveryDialog.qml +++ b/electrum/gui/qml/components/BIP39RecoveryDialog.qml @@ -88,7 +88,7 @@ ElDialog { GridLayout { id: itemLayout - columns: 2 + columns: 3 rowSpacing: 0 anchors { @@ -98,9 +98,20 @@ ElDialog { rightMargin: constants.paddingMedium } + Item { + Layout.columnSpan: 3 + Layout.preferredHeight: constants.paddingLarge + Layout.preferredWidth: 1 + } + Image { + Layout.rowSpan: 3 + source: Qt.resolvedUrl('../../icons/wallet.png') + } Label { Layout.columnSpan: 2 + Layout.fillWidth: true text: model.description + wrapMode: Text.Wrap } Label { text: qsTr('script type') @@ -119,7 +130,7 @@ ElDialog { text: model.derivation_path } Item { - Layout.columnSpan: 2 + Layout.columnSpan: 3 Layout.preferredHeight: constants.paddingLarge Layout.preferredWidth: 1 } diff --git a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml index a55a89328..20d7c4b14 100644 --- a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml +++ b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml @@ -85,38 +85,6 @@ WizardComponent { id: mainLayout width: parent.width - Label { - text: qsTr('Script type and Derivation path') - } - Pane { - Layout.alignment: Qt.AlignHCenter - padding: 0 - visible: !isMultisig - - FlatButton { - text: qsTr('Detect Existing Accounts') - onClicked: { - var dialog = bip39recoveryDialog.createObject(mainLayout, { - walletType: wizard_data['wallet_type'], - seed: wizard_data['seed'], - seedExtraWords: wizard_data['seed_extra_words'] - }) - dialog.accepted.connect(function () { - // select matching script type button and set derivation path - for (var i = 0; i < scripttypegroup.buttons.length; i++) { - var btn = scripttypegroup.buttons[i] - if (btn.visible && btn.scripttype == dialog.scriptType) { - btn.checked = true - derivationpathtext.text = dialog.derivationPath - return - } - } - }) - dialog.open() - } - } - } - Label { text: qsTr('Choose the type of addresses in your wallet.') } @@ -164,17 +132,55 @@ WizardComponent { } InfoTextArea { - Layout.preferredWidth: parent.width + Layout.fillWidth: true text: qsTr('You can override the suggested derivation path.') + ' ' + qsTr('If you are not sure what this is, leave this field unchanged.') } + Label { + text: qsTr('Derivation path') + } + TextField { id: derivationpathtext Layout.fillWidth: true - placeholderText: qsTr('Derivation path') + Layout.leftMargin: constants.paddingMedium onTextChanged: validate() } + + Pane { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingLarge + padding: 0 + visible: !isMultisig + background: Rectangle { + color: Qt.lighter(Material.dialogColor, 1.5) + } + + FlatButton { + text: qsTr('Detect Existing Accounts') + onClicked: { + var dialog = bip39recoveryDialog.createObject(mainLayout, { + walletType: wizard_data['wallet_type'], + seed: wizard_data['seed'], + seedExtraWords: wizard_data['seed_extra_words'] + }) + dialog.accepted.connect(function () { + // select matching script type button and set derivation path + for (var i = 0; i < scripttypegroup.buttons.length; i++) { + var btn = scripttypegroup.buttons[i] + if (btn.visible && btn.scripttype == dialog.scriptType) { + btn.checked = true + derivationpathtext.text = dialog.derivationPath + return + } + } + }) + dialog.open() + } + } + } + } } From d2cf21fc2bcf79f07b7e41178cd3e4ca9e3d9f68 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 May 2023 09:55:19 +0000 Subject: [PATCH 0902/1143] qml wizard: enforce homogeneous master keys in multisig - {xpub, Ypub, Zpub} categories cannot be mixed - old mpk must not be used in multisig --- electrum/bip32.py | 3 +- .../gui/qml/components/NewWalletWizard.qml | 4 ++ .../qml/components/wizard/WCHaveMasterKey.qml | 6 ++- .../gui/qml/components/wizard/WCHaveSeed.qml | 5 ++- electrum/gui/qml/qebitcoin.py | 32 +++++++++------ electrum/gui/qml/qewizard.py | 12 ++++-- electrum/wizard.py | 39 ++++++++++++++++--- 7 files changed, 77 insertions(+), 24 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 248abef21..85cc27fdb 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -289,7 +289,8 @@ def calc_fingerprint_of_this_node(self) -> bytes: return hash_160(self.eckey.get_public_key_bytes(compressed=True))[0:4] -def xpub_type(x): +def xpub_type(x: str): + assert x is not None return BIP32Node.from_xkey(x).xtype diff --git a/electrum/gui/qml/components/NewWalletWizard.qml b/electrum/gui/qml/components/NewWalletWizard.qml index efb8e5867..e7f5e53d3 100644 --- a/electrum/gui/qml/components/NewWalletWizard.qml +++ b/electrum/gui/qml/components/NewWalletWizard.qml @@ -33,6 +33,10 @@ Wizard { walletwizard.path = wiz.path walletwizard.walletCreated() } + function onCreateError(error) { + var dialog = app.messageDialog.createObject(app, { text: error }) + dialog.open() + } } } diff --git a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml index cf6452432..cea7bfb46 100644 --- a/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml +++ b/electrum/gui/qml/components/wizard/WCHaveMasterKey.qml @@ -42,10 +42,14 @@ WizardComponent { if (cosigner) { applyMasterKey(key) - if (wiz.hasDuplicateKeys(wizard_data)) { + if (wiz.hasDuplicateMasterKeys(wizard_data)) { validationtext.text = qsTr('Error: duplicate master public key') return false } + if (wiz.hasHeterogeneousMasterKeys(wizard_data)) { + validationtext.text = qsTr('Error: master public key types do not match') + return false + } } return valid = true diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 35069f37c..29229641e 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -65,9 +65,12 @@ WizardComponent { return } else { apply() - if (wiz.hasDuplicateKeys(wizard_data)) { + if (wiz.hasDuplicateMasterKeys(wizard_data)) { validationtext.text = qsTr('Error: duplicate master public key') return + } else if (wiz.hasHeterogeneousMasterKeys(wizard_data)) { + validationtext.text = qsTr('Error: master public key types do not match') + return } else { valid = true } diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index c03eecb17..95897d39f 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -113,22 +113,30 @@ def verifyMasterKey(self, key, wallet_type='standard'): return False k = keystore.from_master_key(key) - if isinstance(k, keystore.Xpub): # has xpub # TODO are these checks useful? - t1 = xpub_type(k.xpub) - if wallet_type == 'standard': - if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: - self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) - return False - return True - elif wallet_type == 'multisig': - if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: + if wallet_type == 'standard': + if isinstance(k, keystore.Xpub): # has bip32 xpub + t1 = xpub_type(k.xpub) + if t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']: # disallow Ypub/Zpub self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) return False - return True + elif isinstance(k, keystore.Old_KeyStore): + pass else: - self.validationMessage = '%s: %s' % (_('Unsupported wallet type'), wallet_type) - self.logger.error(f'Unsupported wallet type: {wallet_type}') + self._logger.error(f"unexpected keystore type: {type(keystore)}") + return False + elif wallet_type == 'multisig': + if not isinstance(k, keystore.Xpub): # old mpk? + self.validationMessage = '%s: %s' % (_('Wrong key type'), "not bip32") return False + t1 = xpub_type(k.xpub) + if t1 not in ['standard', 'p2wsh', 'p2wsh-p2sh']: # disallow ypub/zpub + self.validationMessage = '%s: %s' % (_('Wrong key type'), t1) + return False + else: + self.validationMessage = '%s: %s' % (_('Unsupported wallet type'), wallet_type) + self._logger.error(f'Unsupported wallet type: {wallet_type}') + return False + # looks okay return True @pyqtSlot(str, result=bool) diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 5b26fa488..43d0108ce 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -83,10 +83,16 @@ def is_single_password(self): return self._daemon.singlePasswordEnabled @pyqtSlot('QJSValue', result=bool) - def hasDuplicateKeys(self, js_data): - self._logger.info('Checking for duplicate keys') + def hasDuplicateMasterKeys(self, js_data): + self._logger.info('Checking for duplicate masterkeys') data = js_data.toVariant() - return self.has_duplicate_keys(data) + return self.has_duplicate_masterkeys(data) + + @pyqtSlot('QJSValue', result=bool) + def hasHeterogeneousMasterKeys(self, js_data): + self._logger.info('Checking for heterogeneous masterkeys') + data = js_data.toVariant() + return self.has_heterogeneous_masterkeys(data) @pyqtSlot('QJSValue', bool, str) def createStorage(self, js_data, single_password_enabled, single_password): diff --git a/electrum/wizard.py b/electrum/wizard.py index 072aa4534..65ac64244 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -305,18 +305,38 @@ def has_all_cosigner_data(self, wizard_data): return True - def has_duplicate_keys(self, wizard_data): + def has_duplicate_masterkeys(self, wizard_data) -> bool: + """Multisig wallets need distinct master keys. If True, need to prevent wallet-creation.""" xpubs = [] xpubs.append(self.keystore_from_data(wizard_data).get_master_public_key()) for cosigner in wizard_data['multisig_cosigner_data']: data = wizard_data['multisig_cosigner_data'][cosigner] xpubs.append(self.keystore_from_data(data).get_master_public_key()) - - while len(xpubs): - xpub = xpubs.pop() - if xpub in xpubs: + assert xpubs + return len(xpubs) != len(set(xpubs)) + + def has_heterogeneous_masterkeys(self, wizard_data) -> bool: + """Multisig wallets need homogeneous master keys. + All master keys need to be bip32, and e.g. Ypub cannot be mixed with Zpub. + If True, need to prevent wallet-creation. + """ + xpubs = [] + xpubs.append(self.keystore_from_data(wizard_data).get_master_public_key()) + for cosigner in wizard_data['multisig_cosigner_data']: + data = wizard_data['multisig_cosigner_data'][cosigner] + xpubs.append(self.keystore_from_data(data).get_master_public_key()) + assert xpubs + try: + k_xpub_type = xpub_type(xpubs[0]) + except Exception: + return True # maybe old_mpk? + for xpub in xpubs: + try: + my_xpub_type = xpub_type(xpub) + except Exception: + return True # maybe old_mpk? + if my_xpub_type != k_xpub_type: return True - return False def keystore_from_data(self, data): @@ -422,10 +442,17 @@ def create_storage(self, path, data): db.put('x3/', data['x3/']) db.put('use_trustedcoin', True) elif data['wallet_type'] == 'multisig': + if not isinstance(k, keystore.Xpub): + raise Exception(f"unexpected keystore(main) type={type(k)} in multisig. not bip32.") + k_xpub_type = xpub_type(k.xpub) db.put('wallet_type', '%dof%d' % (data['multisig_signatures'],data['multisig_participants'])) db.put('x1/', k.dump()) for cosigner in data['multisig_cosigner_data']: cosigner_keystore = self.keystore_from_data(data['multisig_cosigner_data'][cosigner]) + if not isinstance(cosigner_keystore, keystore.Xpub): + raise Exception(f"unexpected keystore(cosigner) type={type(cosigner_keystore)} in multisig. not bip32.") + if k_xpub_type != xpub_type(cosigner_keystore.xpub): + raise Exception("multisig wallet needs to have homogeneous xpub types") if data['encrypt'] and cosigner_keystore.may_have_password(): cosigner_keystore.update_password(None, data['password']) db.put(f'x{cosigner}/', cosigner_keystore.dump()) From f8ce6c65642cc4b1f61441d91070fc8cce2136c2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 10 May 2023 17:39:57 +0200 Subject: [PATCH 0903/1143] qml: small screen fixes --- electrum/gui/qml/components/TxDetails.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 99409e392..567cfe136 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -87,6 +87,7 @@ Pane { FormattedAmount { visible: !txdetails.isUnrelated + Layout.preferredWidth: 1 Layout.fillWidth: true amount: txdetails.lnAmount.isEmpty ? txdetails.amount : txdetails.lnAmount } @@ -107,9 +108,12 @@ Pane { } Label { + Layout.preferredWidth: 1 + Layout.fillWidth: true visible: txdetails.feeRateStr != "" text: qsTr('Transaction fee rate') color: Material.accentColor + wrapMode: Text.Wrap } Label { From 68fb996d200a9a48f93891bb0bdd038c48d4a5ab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 May 2023 13:48:54 +0000 Subject: [PATCH 0904/1143] wallet_db version 52: break non-homogeneous multisig wallets - case 1: in version 4.4.1, 4.4.2, the qml GUI wizard allowed creating multisig wallets with an old_mpk as cosigner. - case 2: in version 4.4.0, 4.4.1, 4.4.2, the qml GUI wizard allowed creating multisig wallets with mixed xpub/Ypub/Zpub. The corresponding missing input validation was a bug in the wizard, it was unintended behaviour. Validation was added in d2cf21fc2bcf79f07b7e41178cd3e4ca9e3d9f68. Note however that there might be users who created such wallet files. Re case 1 wallet files: there is no version of Electrum that allows spending from such a wallet. Coins received at addresses are not burned, however it is technically challenging to spend them. (unless the multisig can spend without needing the old_mpk cosigner in the quorum). Re case 2 wallet files: it is possible to create a corresponding spending wallet for such a multisig, however it is a bit tricky. The script type for the addresses in such a heterogeneous xpub wallet is based on the xpub_type of the first keystore. So e.g. given a wallet file [Yprv1, Zpub2] it will have sh(wsh()) scripts, and the cosigner should create a wallet file [Ypub1, Zprv2] (same order). Technically case 2 wallet files could be "fixed" automatically by converting the xpub types as part of a wallet_db upgrade. However if the wallet files also contain seeds, those cannot be converted ("standard" vs "segwit" electrum seed). Case 1 wallet files are not possible to "fix" automatically as the cosigner using the old_mpk is not bip32 based. It is unclear if there are *any* users out there affected by this. I suspect for case 1 it is very likely there are none (not many people have pre-2.0 electrum seeds which were never supported as part of a multisig who would also now try to create a multisig using them); for case 2 however there might be. This commit breaks both case 1 and case 2 wallets: these wallet files can no longer be opened in new Electrum, an error message is shown and the crash reporter opens. If any potential users opt to send crash reports, at least we will know they exist and can help them recover. --- electrum/gui/qml/components/main.qml | 2 +- electrum/gui/qml/qedaemon.py | 2 +- electrum/gui/qml/qewalletdb.py | 60 ++++++++++++++------------ electrum/gui/qt/__init__.py | 10 ++++- electrum/util.py | 5 ++- electrum/wallet.py | 6 +++ electrum/wallet_db.py | 64 ++++++++++++++++++++++++++-- 7 files changed, 113 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index 7ecd533bc..e49f88ec9 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -495,7 +495,7 @@ ApplicationWindow } function onWalletOpenError(error) { console.log('wallet open error') - var dialog = app.messageDialog.createObject(app, {'text': error}) + var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), 'text': error }) dialog.open() } function onAuthRequired(method, authMessage) { diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 6f4a50b5a..037d40d57 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -223,7 +223,7 @@ def load_wallet_task(): self._backendWalletLoaded.emit(local_password) except WalletFileException as e: - self._logger.error(str(e)) + self._logger.error(f"load_wallet_task errored opening wallet: {e!r}") self.walletOpenError.emit(str(e)) finally: self._loading = False diff --git a/electrum/gui/qml/qewalletdb.py b/electrum/gui/qml/qewalletdb.py index 45a59947e..7b49970d2 100644 --- a/electrum/gui/qml/qewalletdb.py +++ b/electrum/gui/qml/qewalletdb.py @@ -8,7 +8,7 @@ from electrum.wallet_db import WalletDB from electrum.wallet import Wallet from electrum.bip32 import normalize_bip32_derivation, xpub_type -from electrum.util import InvalidPassword, WalletFileException +from electrum.util import InvalidPassword, WalletFileException, send_exception_to_crash_reporter from electrum import keystore if TYPE_CHECKING: @@ -120,9 +120,16 @@ def ready(self): @pyqtSlot() def verify(self): - self.load_storage() - if self._storage: - self.load_db() + try: + self._load_storage() + if self._storage: + self._load_db() + except WalletFileException as e: + self._logger.error(f"verify errored: {repr(e)}") + self._storage = None + self.walletOpenProblem.emit(str(e)) + if e.should_report_crash: + send_exception_to_crash_reporter(e) @pyqtSlot() def doSplit(self): @@ -134,7 +141,8 @@ def doSplit(self): self.splitFinished.emit() - def load_storage(self): + def _load_storage(self): + """can raise WalletFileException""" self._storage = WalletStorage(self._path) if not self._storage.file_exists(): self._logger.warning('file does not exist') @@ -170,27 +178,23 @@ def load_storage(self): if not self._storage.is_past_initial_decryption(): self._storage = None - def load_db(self): + def _load_db(self): + """can raise WalletFileException""" # needs storage accessible - try: - self._db = WalletDB(self._storage.read(), manual_upgrades=True) - if self._db.requires_split(): - self._logger.warning('wallet requires split') - self._requiresSplit = True - self.requiresSplitChanged.emit() - return - if self._db.get_action(): - self._logger.warning('action pending. QML version doesn\'t support continuation of wizard') - return - - if self._db.requires_upgrade(): - self._logger.warning('wallet requires upgrade, upgrading') - self._db.upgrade() - self._db.write(self._storage) - - self._ready = True - self.readyChanged.emit() - except WalletFileException as e: - self._logger.error(f'{repr(e)}') - self._storage = None - self.walletOpenProblem.emit(str(e)) + self._db = WalletDB(self._storage.read(), manual_upgrades=True) + if self._db.requires_split(): + self._logger.warning('wallet requires split') + self._requiresSplit = True + self.requiresSplitChanged.emit() + return + if self._db.get_action(): + self._logger.warning('action pending. QML version doesn\'t support continuation of wizard') + return + + if self._db.requires_upgrade(): + self._logger.warning('wallet requires upgrade, upgrading') + self._db.upgrade() + self._db.write(self._storage) + + self._ready = True + self.readyChanged.emit() diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 326d70bd7..e8d82d0ab 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -342,10 +342,13 @@ def start_new_window( wallet = self.daemon.load_wallet(path, None) except Exception as e: self.logger.exception('') + err_text = str(e) if isinstance(e, WalletFileException) else repr(e) custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), - text=_('Cannot load wallet') + ' (1):\n' + repr(e)) + text=_('Cannot load wallet') + ' (1):\n' + err_text) + if isinstance(e, WalletFileException) and e.should_report_crash: + send_exception_to_crash_reporter(e) # if app is starting, still let wizard appear if not app_is_starting: return @@ -364,10 +367,13 @@ def start_new_window( window = self._create_window_for_wallet(wallet) except Exception as e: self.logger.exception('') + err_text = str(e) if isinstance(e, WalletFileException) else repr(e) custom_message_box(icon=QMessageBox.Warning, parent=None, title=_('Error'), - text=_('Cannot load wallet') + '(2) :\n' + repr(e)) + text=_('Cannot load wallet') + '(2) :\n' + err_text) + if isinstance(e, WalletFileException) and e.should_report_crash: + send_exception_to_crash_reporter(e) if app_is_starting: # If we raise in this context, there are no more fallbacks, we will shut down. # Worst case scenario, we might have gotten here without user interaction, diff --git a/electrum/util.py b/electrum/util.py index 05131e812..c091fa196 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -187,7 +187,10 @@ def __str__(self): return _("Failed to export to file.") + "\n" + self.message -class WalletFileException(Exception): pass +class WalletFileException(Exception): + def __init__(self, message='', *, should_report_crash: bool = False): + Exception.__init__(self, message) + self.should_report_crash = should_report_crash class BitcoinException(Exception): pass diff --git a/electrum/wallet.py b/electrum/wallet.py index 567bb4fce..1ac27628d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -3532,6 +3532,12 @@ def __init__(self, db, storage, *, config): self.wallet_type = db.get('wallet_type') self.m, self.n = multisig_type(self.wallet_type) Deterministic_Wallet.__init__(self, db, storage, config=config) + # sanity checks + for ks in self.get_keystores(): + if not isinstance(ks, keystore.Xpub): + raise Exception(f"unexpected keystore type={type(ks)} in multisig") + if bip32.xpub_type(self.keystore.xpub) != bip32.xpub_type(ks.xpub): + raise Exception(f"multisig wallet needs to have homogeneous xpub types") def get_public_keys(self, address): return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)] diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index fd17f21c7..1336b609e 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -57,7 +57,7 @@ OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 51 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 52 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -81,6 +81,12 @@ def to_str(self) -> str: return f"using {ver}, on {date_str}" +# note: subclassing WalletFileException for some specific cases +# allows the crash reporter to distinguish them and open +# separate tracking issues +class WalletFileExceptionVersion51(WalletFileException): pass + + class WalletDB(JsonDB): def __init__(self, raw, *, manual_upgrades: bool): @@ -220,6 +226,7 @@ def upgrade(self): self._convert_version_49() self._convert_version_50() self._convert_version_51() + self._convert_version_52() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() @@ -1016,6 +1023,38 @@ def _convert_version_51(self): item['payment_hash'] = payment_hash self.data['seed_version'] = 51 + def _detect_insane_version_51(self) -> int: + """Returns 0 if file okay, + error code 1: multisig wallet has old_mpk + error code 2: multisig wallet has mixed Ypub/Zpub + """ + assert self.get('seed_version') == 51 + xpub_type = None + for ks_name in ['x{}/'.format(i) for i in range(1, 16)]: # having any such field <=> multisig wallet + ks = self.data.get(ks_name, None) + if ks is None: continue + ks_type = ks.get('type') + if ks_type == "old": + return 1 # error + assert ks_type in ("bip32", "hardware"), f"unexpected {ks_type=}" + xpub = ks.get('xpub') or None + assert xpub is not None + assert isinstance(xpub, str) + if xpub_type is None: # first iter + xpub_type = xpub[0:4] + if xpub[0:4] != xpub_type: + return 2 # error + # looks okay + return 0 + + def _convert_version_52(self): + if not self._is_upgrade_method_needed(51, 51): + return + if (error_code := self._detect_insane_version_51()) != 0: + # should not get here; get_seed_version should have caught this + raise Exception(f'unsupported wallet file: version_51 with error {error_code}') + self.data['seed_version'] = 52 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return @@ -1071,9 +1110,11 @@ def get_seed_version(self): raise WalletFileException('This version of Electrum is too old to open this wallet.\n' '(highest supported storage version: {}, version of this file: {})' .format(FINAL_SEED_VERSION, seed_version)) - if seed_version==14 and self.get('seed_type') == 'segwit': + if seed_version == 14 and self.get('seed_type') == 'segwit': self._raise_unsupported_version(seed_version) - if seed_version >=12: + if seed_version == 51 and self._detect_insane_version_51(): + self._raise_unsupported_version(seed_version) + if seed_version >= 12: return seed_version if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]: self._raise_unsupported_version(seed_version) @@ -1092,6 +1133,23 @@ def _raise_unsupported_version(self, seed_version): else: # creation was complete if electrum was run from source msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet." + if seed_version == 51: + error_code = self._detect_insane_version_51() + assert error_code != 0 + msg += f" ({error_code=})" + if error_code == 1: + msg += "\nThis is a multisig wallet containing an old_mpk (pre-bip32 master public key)." + msg += "\nPlease contact us to help recover it by opening an issue on GitHub." + elif error_code == 2: + msg += ("\nThis is a multisig wallet containing mixed xpub/Ypub/Zpub." + "\nThe script type is determined by the type of the first keystore." + "\nTo recover, you should re-create the wallet with matching type " + "(converted if needed) master keys." + "\nOr you can contact us to help recover it by opening an issue on GitHub.") + else: + raise Exception(f"unexpected {error_code=}") + raise WalletFileExceptionVersion51(msg, should_report_crash=True) + # generic exception raise WalletFileException(msg) def _add_db_creation_metadata(self): From fab94c802825609069a3f12df4b037da49130a60 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 11 May 2023 16:52:05 +0200 Subject: [PATCH 0905/1143] release notes for 4.4.3: note about wallet upgrade to seed version 52 --- RELEASE-NOTES | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 13fc416b2..e6ff04ae4 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,13 @@ +# Release 4.4.3 (May 11, 2023) + + * Intentionally break multisig wallets that have heterogeneous master + keys. Versions 4.4.0 to 4.4.2 of Electrum for Android did not check + that master keys used the same script type. This may result in the + creation of multisig wallets that that cannot be spent from with + any existing version of Electrum. It is not sure whether any users + are affected by this; if there are any, we will publish + instructions on how to spend those coins (#8417, #8418). + # Release 4.4.2 (May 4, 2023) * Qt GUI: - fix undefined var check in swap_dialog (#8341) From 5d8dda5170684ea07e140c92d7d962d845ca2d9c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 May 2023 15:11:45 +0000 Subject: [PATCH 0906/1143] update locale --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 53312c49f..3966f7c5d 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 53312c49f766e0dc0a7a1ae9f0f8e09e036a486f +Subproject commit 3966f7c5df7b3d367eb8b0f1aa88d123aba34426 From ed3d039fa2656eae74fe2bbdb8eadbb993cafd47 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 May 2023 15:12:51 +0000 Subject: [PATCH 0907/1143] prepare release 4.4.3 --- RELEASE-NOTES | 18 +++++++++++++++++- electrum/version.py | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index e6ff04ae4..71b0fed53 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,5 +1,4 @@ # Release 4.4.3 (May 11, 2023) - * Intentionally break multisig wallets that have heterogeneous master keys. Versions 4.4.0 to 4.4.2 of Electrum for Android did not check that master keys used the same script type. This may result in the @@ -7,6 +6,23 @@ any existing version of Electrum. It is not sure whether any users are affected by this; if there are any, we will publish instructions on how to spend those coins (#8417, #8418). + * Qt GUI: + - handle expected errors in DSCancelDialog (#8390) + - persist addresses tab toolbar "show/hide" state (b40a608b) + * QML GUI: + - implement bip39 account detection (0e0c7980) + - add share toolbutton for outputs in TxDetails (#8410) + * Hardware wallets: + - Ledger: + - fix old bitcoin app support (<2.1): "no sig for ..." (#8365) + - bump req ledger-bitcoin (0.2.0+), adapt to API change (30204991) + * Lightning: + - limit max feature bit we accept to 10_000 (#8403) + - do not disconnect on "warning" messages (6fade55d) + * fix wallet.get_tx_parents for chain of unconf txs (#8391) + * locale: translate more strings when using "default" lang (a0c43573) + * wallet: persist frozen state of addresses to disk right away (#8389) + # Release 4.4.2 (May 4, 2023) * Qt GUI: diff --git a/electrum/version.py b/electrum/version.py index 0fd023bc8..3135c1310 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.4.2' # version of the client package -APK_VERSION = '4.4.2.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.4.3' # version of the client package +APK_VERSION = '4.4.3.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From a6f3ee6364e4ad667ceb146bf0c951659cecd6f5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 11 May 2023 17:14:42 +0200 Subject: [PATCH 0908/1143] minor --- RELEASE-NOTES | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 71b0fed53..787767ad6 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,10 +1,10 @@ # Release 4.4.3 (May 11, 2023) * Intentionally break multisig wallets that have heterogeneous master keys. Versions 4.4.0 to 4.4.2 of Electrum for Android did not check - that master keys used the same script type. This may result in the - creation of multisig wallets that that cannot be spent from with - any existing version of Electrum. It is not sure whether any users - are affected by this; if there are any, we will publish + that master keys used the same script type. This may have resulted + in the creation of multisig wallets that that cannot be spent from + with any existing version of Electrum. It is not sure whether any + users are affected by this; if there are any, we will publish instructions on how to spend those coins (#8417, #8418). * Qt GUI: - handle expected errors in DSCancelDialog (#8390) From 89225a9f4160d5db9b326fac05a11b9bd187eaee Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 12 May 2023 10:53:02 +0200 Subject: [PATCH 0909/1143] qml: show result dialog after password change --- electrum/gui/qml/components/WalletDetails.qml | 21 +++++++++++-------- electrum/gui/qml/qedaemon.py | 7 +++---- electrum/gui/qml/qewallet.py | 8 ++++--- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qml/components/WalletDetails.qml b/electrum/gui/qml/components/WalletDetails.qml index b8d76bfc6..15a277774 100644 --- a/electrum/gui/qml/components/WalletDetails.qml +++ b/electrum/gui/qml/components/WalletDetails.qml @@ -450,14 +450,12 @@ Pane { infotext: qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') }) dialog.accepted.connect(function() { - Daemon.setPassword(dialog.password) - }) - dialog.open() - } - function onPasswordChangeFailed() { - var dialog = app.messageDialog.createObject(app, { - title: qsTr('Error'), - text: qsTr('Password change failed') + var success = Daemon.setPassword(dialog.password) + var done_dialog = app.messageDialog.createObject(app, { + title: success ? qsTr('Success') : qsTr('Error'), + text: success ? qsTr('Password changed') : qsTr('Password change failed') + }) + done_dialog.open() }) dialog.open() } @@ -501,7 +499,12 @@ Pane { infotext: qsTr('If you forget your password, you\'ll need to restore from seed. Please make sure you have your seed stored safely') }) dialog.accepted.connect(function() { - Daemon.currentWallet.setPassword(dialog.password) + var success = Daemon.currentWallet.setPassword(dialog.password) + var done_dialog = app.messageDialog.createObject(app, { + title: success ? qsTr('Success') : qsTr('Error'), + text: success ? qsTr('Password changed') : qsTr('Password change failed') + }) + done_dialog.open() }) dialog.open() } diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 037d40d57..093d6a124 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -128,7 +128,6 @@ class QEDaemon(AuthMixin, QObject): newWalletWizardChanged = pyqtSignal() serverConnectWizardChanged = pyqtSignal() loadingChanged = pyqtSignal() - passwordChangeFailed = pyqtSignal() requestNewPassword = pyqtSignal() walletLoaded = pyqtSignal([str,str], arguments=['name','path']) @@ -322,14 +321,14 @@ def startChangePassword(self): else: self.currentWallet.requestNewPassword.emit() - @pyqtSlot(str) + @pyqtSlot(str, result=bool) def setPassword(self, password): assert self._use_single_password assert password if not self.daemon.update_password_for_directory(old_password=self._password, new_password=password): - self.passwordChangeFailed.emit() - return + return False self._password = password + return True @pyqtProperty(QENewWalletWizard, notify=newWalletWizardChanged) def newWalletWizard(self): diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index 6a07ecc27..a52a67a8b 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -673,7 +673,7 @@ def verifyPassword(self, password): except InvalidPassword as e: return False - @pyqtSlot(str) + @pyqtSlot(str, result=bool) def setPassword(self, password): if password == '': password = None @@ -682,16 +682,18 @@ def setPassword(self, password): # HW wallet not supported yet if storage.is_encrypted_with_hw_device(): - return + return False current_password = self.password if self.password != '' else None try: - self._logger.info(f'PW change from {current_password} to {password}') + self._logger.info('setting new password') self.wallet.update_password(current_password, password, encrypt_storage=True) self.password = password + return True except InvalidPassword as e: self._logger.exception(repr(e)) + return False @pyqtSlot(str) def importAddresses(self, addresslist): From 17a89efd3c19cbff1fbf76d24e5c246d6e0ed935 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 12 May 2023 13:53:51 +0200 Subject: [PATCH 0910/1143] lnurl: fix lightning address regex --- electrum/lnurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnurl.py b/electrum/lnurl.py index 27d020cb1..67e942790 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -153,6 +153,6 @@ def lightning_address_to_url(address: str) -> Optional[str]: """Converts an email-type lightning address to a decoded lnurl. see https://github.com/fiatjaf/lnurl-rfc/blob/luds/16.md """ - if re.match(r"[^@]+@[^@]+\.[^@]+", address): + if re.match(r"^[^@]+@[^.@]+(\.[^.@]+)+$", address): username, domain = address.split("@") return f"https://{domain}/.well-known/lnurlp/{username}" From 3115ce2f538d7aadeccbf43b72178b9864cf58ce Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 11:38:43 +0200 Subject: [PATCH 0911/1143] qml: show historic fiat amounts when enabled and applicable --- .../gui/qml/components/LightningPaymentDetails.qml | 2 ++ electrum/gui/qml/components/TxDetails.qml | 2 ++ .../gui/qml/components/controls/FormattedAmount.qml | 8 +++++++- electrum/gui/qml/qelnpaymentdetails.py | 11 +++++++++++ electrum/gui/qml/qetxdetails.py | 6 ++++++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/LightningPaymentDetails.qml b/electrum/gui/qml/components/LightningPaymentDetails.qml index 7da44ded0..e3b15db71 100644 --- a/electrum/gui/qml/components/LightningPaymentDetails.qml +++ b/electrum/gui/qml/components/LightningPaymentDetails.qml @@ -60,6 +60,7 @@ Pane { FormattedAmount { amount: lnpaymentdetails.amount + timestamp: lnpaymentdetails.timestamp } Label { @@ -71,6 +72,7 @@ Pane { FormattedAmount { visible: lnpaymentdetails.amount.msatsInt < 0 amount: lnpaymentdetails.fee + timestamp: lnpaymentdetails.timestamp } Label { diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 567cfe136..cf4624b3d 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -90,6 +90,7 @@ Pane { Layout.preferredWidth: 1 Layout.fillWidth: true amount: txdetails.lnAmount.isEmpty ? txdetails.amount : txdetails.lnAmount + timestamp: txdetails.timestamp } Label { @@ -104,6 +105,7 @@ Pane { FormattedAmount { Layout.fillWidth: true amount: txdetails.fee + timestamp: txdetails.timestamp } } diff --git a/electrum/gui/qml/components/controls/FormattedAmount.qml b/electrum/gui/qml/components/controls/FormattedAmount.qml index 04e342d89..ac5e375b9 100644 --- a/electrum/gui/qml/components/controls/FormattedAmount.qml +++ b/electrum/gui/qml/components/controls/FormattedAmount.qml @@ -10,6 +10,8 @@ GridLayout { property bool showAlt: true property bool singleLine: true property bool valid: true + property bool historic: Daemon.fx.historicRates + property int timestamp: 0 columns: !valid ? 1 @@ -42,7 +44,11 @@ GridLayout { function setFiatValue() { if (showAlt) - fiatLabel.text = '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' + if (historic && timestamp) + fiatLabel.text = '(' + Daemon.fx.fiatValueHistoric(amount, timestamp) + ' ' + Daemon.fx.fiatCurrency + ')' + else + fiatLabel.text = '(' + Daemon.fx.fiatValue(amount) + ' ' + Daemon.fx.fiatCurrency + ')' + } onAmountChanged: setFiatValue() diff --git a/electrum/gui/qml/qelnpaymentdetails.py b/electrum/gui/qml/qelnpaymentdetails.py index 3ea29eb50..49fef7dde 100644 --- a/electrum/gui/qml/qelnpaymentdetails.py +++ b/electrum/gui/qml/qelnpaymentdetails.py @@ -6,6 +6,7 @@ from .qetypes import QEAmount from .qewallet import QEWallet + class QELnPaymentDetails(QObject): _logger = get_logger(__name__) @@ -16,9 +17,14 @@ def __init__(self, parent=None): self._wallet = None self._key = None + self._label = '' self._date = None + self._timestamp = 0 self._fee = QEAmount() self._amount = QEAmount() + self._status = '' + self._phash = '' + self._preimage = '' walletChanged = pyqtSignal() @pyqtProperty(QEWallet, notify=walletChanged) @@ -64,6 +70,10 @@ def status(self): def date(self): return self._date + @pyqtProperty(int, notify=detailsChanged) + def timestamp(self): + return self._timestamp + @pyqtProperty(str, notify=detailsChanged) def paymentHash(self): return self._phash @@ -93,6 +103,7 @@ def update(self): self._amount.msatsInt = int(tx['amount_msat']) self._label = tx['label'] self._date = format_time(tx['timestamp']) + self._timestamp = tx['timestamp'] self._status = 'settled' # TODO: other states? get_lightning_history is deciding the filter for us :( self._phash = tx['payment_hash'] self._preimage = tx['preimage'] diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 8bd04c870..8732f9776 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -57,6 +57,7 @@ def __init__(self, parent=None): self._mempool_depth = '' self._date = '' + self._timestamp = 0 self._confirmations = 0 self._header_hash = '' self._short_id = "" @@ -172,6 +173,10 @@ def mempoolDepth(self): def date(self): return self._date + @pyqtProperty(int, notify=detailsChanged) + def timestamp(self): + return self._timestamp + @pyqtProperty(int, notify=detailsChanged) def confirmations(self): return self._confirmations @@ -307,6 +312,7 @@ def update(self, from_txid: bool = False): def update_mined_status(self, tx_mined_info: TxMinedInfo): self._mempool_depth = '' self._date = format_time(tx_mined_info.timestamp) + self._timestamp = tx_mined_info.timestamp self._confirmations = tx_mined_info.conf self._header_hash = tx_mined_info.header_hash self._short_id = tx_mined_info.short_id() or "" From 04c907895556dc0041a98e3586568b8f0bb68ff1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 11:56:40 +0200 Subject: [PATCH 0912/1143] qml: show pay_invoice error to user --- electrum/gui/qml/components/WalletMainView.qml | 6 ++++++ electrum/gui/qml/qewallet.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index f20c3ea16..8adeb2d89 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -306,6 +306,12 @@ Item { }) dialog.open() } + function onPaymentFailed(invoice_id, message) { + var dialog = app.messageDialog.createObject(app, { + text: message + }) + dialog.open() + } } Component { diff --git a/electrum/gui/qml/qewallet.py b/electrum/gui/qml/qewallet.py index a52a67a8b..8baa2a550 100644 --- a/electrum/gui/qml/qewallet.py +++ b/electrum/gui/qml/qewallet.py @@ -609,7 +609,8 @@ def pay_thread(): fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) fut.result() except Exception as e: - self.paymentFailed.emit(invoice.get_id(), repr(e)) + self._logger.error(f'pay_invoice failed! {e!r}') + self.paymentFailed.emit(invoice.get_id(), str(e)) threading.Thread(target=pay_thread, daemon=True).start() From cb13eee8a3f3fca10776e050af0bcd8b0b62c15b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 13:43:57 +0200 Subject: [PATCH 0913/1143] qml: let ElCombobox determine implicitWidth based on the dimensions of all modelitems --- .../qml/components/controls/ElComboBox.qml | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/ElComboBox.qml b/electrum/gui/qml/components/controls/ElComboBox.qml index a1e878f26..2137a4e42 100644 --- a/electrum/gui/qml/components/controls/ElComboBox.qml +++ b/electrum/gui/qml/components/controls/ElComboBox.qml @@ -1,14 +1,42 @@ -import QtQuick.Controls 2.0 +import QtQuick 2.15 +import QtQuick.Controls 2.15 ComboBox { id: cb + + property int implicitChildrenWidth: 64 + // make combobox implicit width a multiple of 32, so it aligns with others - implicitWidth: Math.ceil(contentItem.implicitWidth/32)*32 + constants.paddingXXLarge + implicitWidth: Math.ceil(implicitChildrenWidth/32)*32 + 2 * constants.paddingXLarge + // redefine contentItem, as the default crops the text easily contentItem: Label { + id: contentLabel text: cb.currentText padding: constants.paddingLarge rightPadding: constants.paddingXXLarge font.pixelSize: constants.fontSizeMedium } + + // determine widest element and store in implicitChildrenWidth + function updateImplicitWidth() { + console.log('updating implicit width') + console.log(cb.count) + for (let i = 0; i < cb.count; i++) { + var txt = cb.textAt(i) + var txtwidth = fontMetrics.advanceWidth(txt) + console.log(txt + ' is ' + txtwidth + ' wide') + if (txtwidth > cb.implicitChildrenWidth) { + cb.implicitChildrenWidth = txtwidth + } + } + } + + FontMetrics { + id: fontMetrics + font: contentLabel.font + } + + Component.onCompleted: updateImplicitWidth() + onModelChanged: updateImplicitWidth() } From 4f252a438c499aad930636d0efa9e30fd11f3f4f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 14:45:21 +0200 Subject: [PATCH 0914/1143] qml: validate duplicate master key in WCBIP39Refine for BIP39 cosigner seeds (fixes #8432) --- .../qml/components/wizard/WCBIP39Refine.qml | 21 +++++++++++++++++++ .../gui/qml/components/wizard/WCHaveSeed.qml | 21 ++++++++++++------- electrum/gui/qml/qewizard.py | 2 +- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml index 20d7c4b14..647c23c33 100644 --- a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml +++ b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml @@ -43,11 +43,24 @@ WizardComponent { function validate() { valid = false + validationtext.text = '' + var p = isMultisig ? getMultisigScriptTypePurposeDict() : getScriptTypePurposeDict() if (!scripttypegroup.checkedButton.scripttype in p) return if (!bitcoin.verifyDerivationPath(derivationpathtext.text)) return + + if (isMultisig && cosigner) { + apply() + if (wiz.hasDuplicateMasterKeys(wizard_data)) { + validationtext.text = qsTr('Error: duplicate master public key') + return + } else if (wiz.hasHeterogeneousMasterKeys(wizard_data)) { + validationtext.text = qsTr('Error: master public key types do not match') + return + } + } valid = true } @@ -148,6 +161,13 @@ WizardComponent { onTextChanged: validate() } + InfoTextArea { + id: validationtext + Layout.fillWidth: true + visible: text + iconStyle: InfoTextArea.IconStyle.Error + } + Pane { Layout.alignment: Qt.AlignHCenter Layout.topMargin: constants.paddingLarge @@ -199,6 +219,7 @@ WizardComponent { participants = wizard_data['multisig_participants'] if ('multisig_current_cosigner' in wizard_data) cosigner = wizard_data['multisig_current_cosigner'] + validate() } } } diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index 29229641e..c8fa5c797 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -64,13 +64,18 @@ WizardComponent { valid = validSeed return } else { - apply() - if (wiz.hasDuplicateMasterKeys(wizard_data)) { - validationtext.text = qsTr('Error: duplicate master public key') - return - } else if (wiz.hasHeterogeneousMasterKeys(wizard_data)) { - validationtext.text = qsTr('Error: master public key types do not match') - return + // bip39 validate after derivation path is known + if (seed_variant_cb.currentValue == 'electrum') { + apply() + if (wiz.hasDuplicateMasterKeys(wizard_data)) { + validationtext.text = qsTr('Error: duplicate master public key') + return + } else if (wiz.hasHeterogeneousMasterKeys(wizard_data)) { + validationtext.text = qsTr('Error: master public key types do not match') + return + } else { + valid = true + } } else { valid = true } @@ -165,6 +170,7 @@ WizardComponent { InfoTextArea { id: infotext + visible: !cosigner Layout.fillWidth: true Layout.columnSpan: 2 Layout.bottomMargin: constants.paddingLarge @@ -214,7 +220,6 @@ WizardComponent { Bitcoin { id: bitcoin onSeedTypeChanged: seedtext.indicatorText = bitcoin.seedType - onValidationMessageChanged: validationtext.text = validationMessage } function startValidationTimer() { diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 43d0108ce..55486e644 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -60,7 +60,6 @@ def __init__(self, daemon, parent = None): 'bip39_refine': { 'gui': 'WCBIP39Refine' }, 'have_master_key': { 'gui': 'WCHaveMasterKey' }, 'multisig': { 'gui': 'WCMultisig' }, - # 'multisig_show_masterpubkey': { 'gui': 'WCShowMasterPubkey' }, 'multisig_cosigner_keystore': { 'gui': 'WCCosignerKeystore' }, 'multisig_cosigner_key': { 'gui': 'WCHaveMasterKey' }, 'multisig_cosigner_seed': { 'gui': 'WCHaveSeed' }, @@ -117,6 +116,7 @@ def createStorage(self, js_data, single_password_enabled, single_password): self._logger.error(f"createStorage errored: {e!r}") self.createError.emit(str(e)) + class QEServerConnectWizard(ServerConnectWizard, QEAbstractWizard): def __init__(self, daemon, parent = None): From 8d0fa2706517e63995d099d09fa53a9177fa497b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 16:45:15 +0200 Subject: [PATCH 0915/1143] qml: remove unnecessary assert (#8420) --- electrum/gui/qml/qeaddressdetails.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/gui/qml/qeaddressdetails.py b/electrum/gui/qml/qeaddressdetails.py index 0b23dcbbf..096ba506f 100644 --- a/electrum/gui/qml/qeaddressdetails.py +++ b/electrum/gui/qml/qeaddressdetails.py @@ -125,5 +125,4 @@ def update(self): if self._wallet.derivationPrefix: self._derivationPath = self._derivationPath.replace('m', self._wallet.derivationPrefix) self._numtx = self._wallet.wallet.adb.get_address_history_len(self._address) - assert self._numtx == self.historyModel.rowCount(0) self.detailsChanged.emit() From 229afdd8879dae13e07e436b90eaf4a084748352 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 12 May 2023 15:31:06 +0200 Subject: [PATCH 0916/1143] qml: styling LnurlPayRequestDialog --- .../qml/components/LnurlPayRequestDialog.qml | 69 ++++++++++++++----- .../qml/components/controls/InfoTextArea.qml | 13 ++-- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index e815729f7..6d1effeb7 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -21,20 +21,24 @@ ElDialog { ColumnLayout { width: parent.width + spacing: 0 GridLayout { + id: rootLayout columns: 2 Layout.fillWidth: true Layout.leftMargin: constants.paddingLarge Layout.rightMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge Label { text: qsTr('Provider') color: Material.accentColor } Label { + Layout.fillWidth: true text: invoiceParser.lnurlData['domain'] } Label { @@ -42,8 +46,8 @@ ElDialog { color: Material.accentColor } Label { - text: invoiceParser.lnurlData['metadata_plaintext'] Layout.fillWidth: true + text: invoiceParser.lnurlData['metadata_plaintext'] wrapMode: Text.Wrap } @@ -52,36 +56,69 @@ ElDialog { color: Material.accentColor } - BtcField { - id: amountBtc - text: Config.formatSats(invoiceParser.lnurlData['min_sendable_sat']) - enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] - color: Material.foreground // override gray-out on disabled - fiatfield: null - Layout.preferredWidth: parent.width /3 - onTextAsSatsChanged: { - invoiceParser.amountOverride = textAsSats + RowLayout { + Layout.fillWidth: true + BtcField { + id: amountBtc + Layout.preferredWidth: rootLayout.width /3 + text: Config.formatSats(invoiceParser.lnurlData['min_sendable_sat']) + enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] + color: Material.foreground // override gray-out on disabled + fiatfield: amountFiat + onTextAsSatsChanged: { + invoiceParser.amountOverride = textAsSats + } + } + Label { + text: Config.baseUnit + color: Material.accentColor + } + } + + Item { visible: Daemon.fx.enabled; Layout.preferredWidth: 1; Layout.preferredHeight: 1 } + + RowLayout { + visible: Daemon.fx.enabled + FiatField { + id: amountFiat + Layout.preferredWidth: rootLayout.width / 3 + btcfield: amountBtc + } + Label { + text: Daemon.fx.fiatCurrency + color: Material.accentColor } } - Label { + + InfoTextArea { Layout.columnSpan: 2 - text: invoiceParser.lnurlData['min_sendable_sat'] == invoiceParser.lnurlData['max_sendable_sat'] - ? '' - : qsTr('Amount must be between %1 and %2').arg(Config.formatSats(invoiceParser.lnurlData['min_sendable_sat'])).arg(Config.formatSats(invoiceParser.lnurlData['max_sendable_sat'])) + Config.baseUnit + Layout.fillWidth: true + compact: true + visible: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] + text: qsTr('Amount must be between %1 and %2 %3').arg(Config.formatSats(invoiceParser.lnurlData['min_sendable_sat'])).arg(Config.formatSats(invoiceParser.lnurlData['max_sendable_sat'])).arg(Config.baseUnit) } + Label { + Layout.columnSpan: 2 + visible: invoiceParser.lnurlData['comment_allowed'] > 0 + text: qsTr('Message') + color: Material.accentColor + } TextArea { id: comment - visible: invoiceParser.lnurlData['comment_allowed'] > 0 Layout.columnSpan: 2 - Layout.preferredWidth: parent.width + Layout.fillWidth: true + Layout.leftMargin: constants.paddingLarge Layout.minimumHeight: 80 + visible: invoiceParser.lnurlData['comment_allowed'] > 0 wrapMode: TextEdit.Wrap placeholderText: qsTr('Enter an (optional) message for the receiver') color: text.length > invoiceParser.lnurlData['comment_allowed'] ? constants.colorError : Material.foreground } Label { + Layout.columnSpan: 2 + Layout.leftMargin: constants.paddingLarge visible: invoiceParser.lnurlData['comment_allowed'] > 0 text: qsTr('%1 characters remaining').arg(Math.max(0, (invoiceParser.lnurlData['comment_allowed'] - comment.text.length) )) color: constants.mutedForeground diff --git a/electrum/gui/qml/components/controls/InfoTextArea.qml b/electrum/gui/qml/components/controls/InfoTextArea.qml index 9b4db966f..c3a6a27f6 100644 --- a/electrum/gui/qml/components/controls/InfoTextArea.qml +++ b/electrum/gui/qml/components/controls/InfoTextArea.qml @@ -18,6 +18,7 @@ TextHighlightPane { property alias text: infotext.text property int iconStyle: InfoTextArea.IconStyle.Info property alias textFormat: infotext.textFormat + property bool compact: false borderColor: iconStyle == InfoTextArea.IconStyle.Info ? constants.colorInfo @@ -30,15 +31,15 @@ TextHighlightPane { : iconStyle == InfoTextArea.IconStyle.Done ? constants.colorDone : constants.colorInfo - padding: constants.paddingXLarge + padding: compact ? constants.paddingMedium : constants.paddingXLarge RowLayout { width: parent.width - spacing: constants.paddingLarge + spacing: compact ? constants.paddingMedium : constants.paddingLarge Image { - Layout.preferredWidth: constants.iconSizeMedium - Layout.preferredHeight: constants.iconSizeMedium + Layout.preferredWidth: compact ? constants.iconSizeSmall : constants.iconSizeMedium + Layout.preferredHeight: compact ? constants.iconSizeSmall : constants.iconSizeMedium visible: iconStyle != InfoTextArea.IconStyle.Spinner && iconStyle != InfoTextArea.IconStyle.None source: iconStyle == InfoTextArea.IconStyle.Info ? "../../../icons/info.png" @@ -56,8 +57,8 @@ TextHighlightPane { } Item { - Layout.preferredWidth: constants.iconSizeMedium - Layout.preferredHeight: constants.iconSizeMedium + Layout.preferredWidth: compact ? constants.iconSizeSmall : constants.iconSizeMedium + Layout.preferredHeight: compact ? constants.iconSizeSmall : constants.iconSizeMedium visible: iconStyle == InfoTextArea.IconStyle.Spinner BusyIndicator { From 5881eb3035db115513b5630472db794215b0a908 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 12 May 2023 15:49:42 +0200 Subject: [PATCH 0917/1143] qml: LnurlPayRequestDialog validate amount between indicated boundaries --- electrum/gui/qml/components/LnurlPayRequestDialog.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index 6d1effeb7..6cc2030df 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -17,7 +17,10 @@ ElDialog { padding: 0 - property bool valid: comment.text.length <= invoiceParser.lnurlData['comment_allowed'] + property bool commentValid: comment.text.length <= invoiceParser.lnurlData['comment_allowed'] + property bool amountValid: amountBtc.textAsSats.satsInt >= parseInt(invoiceParser.lnurlData['min_sendable_sat']) + && amountBtc.textAsSats.satsInt <= parseInt(invoiceParser.lnurlData['max_sendable_sat']) + property bool valid: commentValid && amountValid ColumnLayout { width: parent.width From 1dd129c3e8b3f7442e95f91dd24323d9fada30af Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 16:51:49 +0200 Subject: [PATCH 0918/1143] qml: remove logging ElCombobox --- electrum/gui/qml/components/controls/ElComboBox.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/electrum/gui/qml/components/controls/ElComboBox.qml b/electrum/gui/qml/components/controls/ElComboBox.qml index 2137a4e42..48366956f 100644 --- a/electrum/gui/qml/components/controls/ElComboBox.qml +++ b/electrum/gui/qml/components/controls/ElComboBox.qml @@ -20,12 +20,9 @@ ComboBox { // determine widest element and store in implicitChildrenWidth function updateImplicitWidth() { - console.log('updating implicit width') - console.log(cb.count) for (let i = 0; i < cb.count; i++) { var txt = cb.textAt(i) var txtwidth = fontMetrics.advanceWidth(txt) - console.log(txt + ' is ' + txtwidth + ' wide') if (txtwidth > cb.implicitChildrenWidth) { cb.implicitChildrenWidth = txtwidth } From a7d3240bc2498433bd92397d9bd01169c665daca Mon Sep 17 00:00:00 2001 From: Ali Sherief Date: Tue, 16 May 2023 06:27:20 +0000 Subject: [PATCH 0919/1143] Remove Localbitcoins provider --- electrum/currencies.json | 79 --------------------------------------- electrum/exchange_rate.py | 8 ---- 2 files changed, 87 deletions(-) diff --git a/electrum/currencies.json b/electrum/currencies.json index 78970b610..d9d4fdac7 100644 --- a/electrum/currencies.json +++ b/electrum/currencies.json @@ -803,85 +803,6 @@ "JPY", "USD" ], - "LocalBitcoins": [ - "AED", - "ARS", - "AUD", - "BAM", - "BDT", - "BGN", - "BOB", - "BRL", - "BWP", - "BYN", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CZK", - "DKK", - "DOP", - "EGP", - "ETH", - "EUR", - "GBP", - "GEL", - "GHS", - "GTQ", - "HKD", - "HNL", - "HRK", - "HUF", - "IDR", - "ILS", - "INR", - "IRR", - "JOD", - "JPY", - "KES", - "KRW", - "KZT", - "LKR", - "LTC", - "MAD", - "MXN", - "MYR", - "NGN", - "NOK", - "NZD", - "OMR", - "PAB", - "PEN", - "PHP", - "PKR", - "PLN", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SEK", - "SGD", - "SZL", - "THB", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "VES", - "VND", - "XAF", - "XMR", - "XRP", - "ZAR" - ], "MercadoBitcoin": [ "BRL" ], diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index f3c67712d..9266cf01a 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -387,14 +387,6 @@ async def get_rates(self, ccy): for k, v in json['result'].items()) -class LocalBitcoins(ExchangeBase): - - async def get_rates(self, ccy): - json = await self.get_json('localbitcoins.com', - '/bitcoinaverage/ticker-all-currencies/') - return dict([(r, to_decimal(json[r]['rates']['last'])) for r in json]) - - class MercadoBitcoin(ExchangeBase): async def get_rates(self, ccy): From 68eaa680f84eef84cbc5c688d2d37325710d33bf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 May 2023 12:28:59 +0000 Subject: [PATCH 0920/1143] CLI/RPC: better error msg when running daemon on Windows `-d` is not supported, due to missing os.fork related: https://github.com/spesmilo/electrum/issues/5511 --- run_electrum | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/run_electrum b/run_electrum index aaf6ea161..0f32d954f 100755 --- a/run_electrum +++ b/run_electrum @@ -419,7 +419,13 @@ def main(): print_stderr("Run 'electrum stop' to stop the daemon.") sys.exit(1) # fork before creating the asyncio event loop - pid = os.fork() + try: + pid = os.fork() + except AttributeError as e: + print_stderr(f"Error: {e!r}") + print_stderr("Running daemon in detached mode (-d) is not supported on this platform.") + print_stderr("Try running the daemon in the foreground (without -d).") + sys.exit(1) if pid: print_stderr("starting daemon (PID %d)" % pid) sys.exit(0) From ac8a7a07849ea64a9c70f42c5f68e5e4d2b7152f Mon Sep 17 00:00:00 2001 From: accumulator Date: Tue, 16 May 2023 15:08:26 +0200 Subject: [PATCH 0921/1143] channel_db: raise specific exception when channelDB not loaded, allowing lnworker to mark payment as failed. (#8431) On mobile, it can take a while before channelDB is loaded. If payment is attempted before the DB is fully loaded, this would result in a payment failure, but also leaves the payment attempt in IN_PROGRESS state. This patch adds a more specific ChannelDBNotLoaded exception class, so we can handle this case more gracefully, since we know the payment didn't succeed. --- electrum/channel_db.py | 9 ++++++--- electrum/lnworker.py | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/channel_db.py b/electrum/channel_db.py index e7b6e5f9e..e557261bf 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -38,7 +38,7 @@ from .sql_db import SqlDB, sql from . import constants, util -from .util import profiler, get_headers_dir, is_ip_address, json_normalize +from .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException from .logging import Logger from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID, validate_features, IncompatibleOrInsaneFeatures, InvalidGossipMsg) @@ -59,6 +59,9 @@ FLAG_DIRECTION = 1 << 0 +class ChannelDBNotLoaded(UserFacingException): pass + + class ChannelInfo(NamedTuple): short_channel_id: ShortChannelID node1_id: bytes @@ -375,7 +378,7 @@ def get_last_good_address(self, node_id: bytes) -> Optional[LNPeerAddr]: def get_recent_peers(self): if not self.data_loaded.is_set(): - raise Exception("channelDB data not loaded yet!") + raise ChannelDBNotLoaded("channelDB data not loaded yet!") with self.lock: ret = [self.get_last_good_address(node_id) for node_id in self._recent_peers] @@ -842,7 +845,7 @@ def get_channels_for_node( ) -> Set[ShortChannelID]: """Returns the set of short channel IDs where node_id is one of the channel participants.""" if not self.data_loaded.is_set(): - raise Exception("channelDB data not loaded yet!") + raise ChannelDBNotLoaded("channelDB data not loaded yet!") relevant_channels = self._channels_for_node.get(node_id) or set() relevant_channels = set(relevant_channels) # copy # add our own channels # TODO maybe slow? diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 0994b4e38..2abe2662c 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -79,7 +79,7 @@ from .crypto import pw_encode_with_version_and_mac, pw_decode_with_version_and_mac from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage from .lnchannel import ChannelBackup -from .channel_db import UpdateStatus +from .channel_db import UpdateStatus, ChannelDBNotLoaded from .channel_db import get_mychannel_info, get_mychannel_policy from .submarine_swaps import SwapManager from .channel_db import ChannelInfo, Policy @@ -1202,6 +1202,9 @@ async def pay_invoice( except PaymentFailure as e: self.logger.info(f'payment failure: {e!r}') reason = str(e) + except ChannelDBNotLoaded as e: + self.logger.info(f'payment failure: {e!r}') + reason = str(e) finally: self.logger.info(f"pay_invoice ending session for RHASH={payment_hash.hex()}. {success=}") if success: From fc7c5dde6e8383ff49e3acabd05fc1619552508c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 May 2023 14:41:26 +0000 Subject: [PATCH 0922/1143] qt SwapDialog: propagate errors from _create_tx fixes https://github.com/spesmilo/electrum/issues/8430 --- electrum/gui/qt/main_window.py | 8 ++++++-- electrum/gui/qt/swap_dialog.py | 37 ++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index cc692ba73..6c568fe09 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -99,7 +99,7 @@ from .confirm_tx_dialog import ConfirmTxDialog from .rbf_dialog import BumpFeeDialog, DSCancelDialog from .qrreader import scan_qrcode -from .swap_dialog import SwapDialog +from .swap_dialog import SwapDialog, InvalidSwapParameters from .balance_dialog import BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED, COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING if TYPE_CHECKING: @@ -1126,7 +1126,11 @@ def get_pairs_thread(): self.show_error(str(e)) return d = SwapDialog(self, is_reverse=is_reverse, recv_amount_sat=recv_amount_sat, channels=channels) - return d.run() + try: + return d.run() + except InvalidSwapParameters as e: + self.show_error(str(e)) + return @qt_event_listener def on_event_request_status(self, wallet, key, status): diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 4f52756c6..a4686032c 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -20,17 +20,18 @@ if TYPE_CHECKING: from .main_window import ElectrumWindow -CANNOT_RECEIVE_WARNING = """ -The requested amount is higher than what you can receive in your currently open channels. +CANNOT_RECEIVE_WARNING = _( +"""The requested amount is higher than what you can receive in your currently open channels. If you continue, your funds will be locked until the remote server can find a path to pay you. If the swap cannot be performed after 24h, you will be refunded. -Do you want to continue? -""" +Do you want to continue?""" +) -class SwapDialog(WindowModalDialog, QtEventListener): +class InvalidSwapParameters(Exception): pass + - tx: Optional[PartialTransaction] +class SwapDialog(WindowModalDialog, QtEventListener): def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=None, channels=None): WindowModalDialog.__init__(self, window, _('Submarine Swap')) @@ -231,7 +232,7 @@ def update(self): self.server_fee_label.repaint() # macOS hack for #6269 self.needs_tx_update = True - def update_fee(self, tx): + def update_fee(self, tx: Optional[PartialTransaction]) -> None: """Updates self.fee_label. No other side-effects.""" if self.is_reverse: sm = self.swap_manager @@ -243,6 +244,7 @@ def update_fee(self, tx): self.fee_label.repaint() # macOS hack for #6269 def run(self): + """Can raise InvalidSwapParameters.""" if not self.exec_(): return if self.is_reverse: @@ -273,24 +275,23 @@ def update_tx(self) -> None: return is_max = self.max_button.isChecked() if is_max: - tx = self._create_tx('!') + tx = self._create_tx_safe('!') self._spend_max_forward_swap(tx) else: onchain_amount = self.send_amount_e.get_amount() - tx = self._create_tx(onchain_amount) + tx = self._create_tx_safe(onchain_amount) self.update_fee(tx) - def _create_tx(self, onchain_amount: Union[int, str, None]) -> Optional[PartialTransaction]: - if self.is_reverse: - return + def _create_tx(self, onchain_amount: Union[int, str, None]) -> PartialTransaction: + assert not self.is_reverse if onchain_amount is None: - return + raise InvalidSwapParameters("onchain_amount is None") coins = self.window.get_coins() if onchain_amount == '!': max_amount = sum(c.value_sats() for c in coins) max_swap_amount = self.swap_manager.max_amount_forward_swap() if max_swap_amount is None: - return None + raise InvalidSwapParameters("swap_manager.max_amount_forward_swap() is None") if max_amount > max_swap_amount: onchain_amount = max_swap_amount outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)] @@ -299,9 +300,15 @@ def _create_tx(self, onchain_amount: Union[int, str, None]) -> Optional[PartialT coins=coins, outputs=outputs) except (NotEnoughFunds, NoDynamicFeeEstimates) as e: - return + raise InvalidSwapParameters(str(e)) from e return tx + def _create_tx_safe(self, onchain_amount: Union[int, str, None]) -> Optional[PartialTransaction]: + try: + return self._create_tx(onchain_amount=onchain_amount) + except InvalidSwapParameters: + return None + def update_ok_button(self): """Updates self.ok_button. No other side-effects.""" send_amount = self.send_amount_e.get_amount() From fd41308c6b616a32e766528392ced282ed9416d7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 16 May 2023 15:10:12 +0000 Subject: [PATCH 0923/1143] network: log original error text in catch_server_exceptions related: https://github.com/spesmilo/electrum/issues/8439 --- electrum/network.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/electrum/network.py b/electrum/network.py index 90d66a344..0def020ab 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -891,12 +891,18 @@ async def make_reliable_wrapper(self: 'Network', *args, **kwargs): return make_reliable_wrapper def catch_server_exceptions(func): + """Decorator that wraps server errors in UntrustedServerReturnedError, + to avoid showing untrusted arbitrary text to users. + """ @functools.wraps(func) async def wrapper(self, *args, **kwargs): try: return await func(self, *args, **kwargs) except aiorpcx.jsonrpc.CodeMessageError as e: - raise UntrustedServerReturnedError(original_exception=e) from e + wrapped_exc = UntrustedServerReturnedError(original_exception=e) + # log (sanitized) untrusted error text now, to ease debugging + self.logger.debug(f"got error from server for {func.__qualname__}: {wrapped_exc!r}") + raise wrapped_exc from e return wrapper @best_effort_reliable From e9475345e40130e399a0e7479d764f48716d5335 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 17 May 2023 15:19:41 +0000 Subject: [PATCH 0924/1143] qml wizard: "confirm seed" screen to normalize whitespaces fixes https://github.com/spesmilo/electrum/issues/8442 --- electrum/base_wizard.py | 6 ++++- .../qml/components/wizard/WCConfirmSeed.qml | 2 +- electrum/gui/qml/qewizard.py | 6 +++++ electrum/mnemonic.py | 10 +++++++++ electrum/tests/test_mnemonic.py | 22 ++++++++++++++++++- 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py index 7a68f2bae..acb77ec04 100644 --- a/electrum/base_wizard.py +++ b/electrum/base_wizard.py @@ -719,7 +719,11 @@ def request_passphrase(self, seed, opt_passphrase): def confirm_seed(self, seed, passphrase): f = lambda x: self.confirm_passphrase(seed, passphrase) - self.confirm_seed_dialog(run_next=f, seed=seed if self.config.get('debug_seed') else '', test=lambda x: x==seed) + self.confirm_seed_dialog( + run_next=f, + seed=seed if self.config.get('debug_seed') else '', + test=lambda x: mnemonic.is_matching_seed(seed=seed, seed_again=x), + ) def confirm_passphrase(self, seed, passphrase): f = lambda x: self.run('create_keystore', seed, x) diff --git a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml index ab04a44ae..17d262e5d 100644 --- a/electrum/gui/qml/components/wizard/WCConfirmSeed.qml +++ b/electrum/gui/qml/components/wizard/WCConfirmSeed.qml @@ -13,7 +13,7 @@ WizardComponent { valid: false function checkValid() { - var seedvalid = confirm.text == wizard_data['seed'] + var seedvalid = wizard.wiz.isMatchingSeed(wizard_data['seed'], confirm.text) var customwordsvalid = customwordstext.text == wizard_data['seed_extra_words'] valid = seedvalid && (wizard_data['seed_extend'] ? customwordsvalid : true) } diff --git a/electrum/gui/qml/qewizard.py b/electrum/gui/qml/qewizard.py index 55486e644..6020a5d96 100644 --- a/electrum/gui/qml/qewizard.py +++ b/electrum/gui/qml/qewizard.py @@ -4,8 +4,10 @@ from PyQt5.QtQml import QQmlApplicationEngine from electrum.logging import get_logger +from electrum import mnemonic from electrum.wizard import NewWalletWizard, ServerConnectWizard + class QEAbstractWizard(QObject): _logger = get_logger(__name__) @@ -93,6 +95,10 @@ def hasHeterogeneousMasterKeys(self, js_data): data = js_data.toVariant() return self.has_heterogeneous_masterkeys(data) + @pyqtSlot(str, str, result=bool) + def isMatchingSeed(self, seed, seed_again): + return mnemonic.is_matching_seed(seed=seed, seed_again=seed_again) + @pyqtSlot('QJSValue', bool, str) def createStorage(self, js_data, single_password_enabled, single_password): self._logger.info('Creating wallet from wizard data') diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py index 4134575ac..30f5c0fb4 100644 --- a/electrum/mnemonic.py +++ b/electrum/mnemonic.py @@ -90,6 +90,16 @@ def normalize_text(seed: str) -> str: return seed +def is_matching_seed(*, seed: str, seed_again: str) -> bool: + """Compare two seeds for equality, as used in "confirm seed" screen in wizard. + Not just for electrum seeds, but other types (e.g. bip39) as well. + Note: we don't use normalize_text, as that is specific to electrum seeds. + """ + seed = " ".join(seed.split()) + seed_again = " ".join(seed_again.split()) + return seed == seed_again + + _WORDLIST_CACHE = {} # type: Dict[str, Wordlist] diff --git a/electrum/tests/test_mnemonic.py b/electrum/tests/test_mnemonic.py index 6273388e6..870e16bf8 100644 --- a/electrum/tests/test_mnemonic.py +++ b/electrum/tests/test_mnemonic.py @@ -7,7 +7,7 @@ from electrum import slip39 from electrum import old_mnemonic from electrum.util import bfh -from electrum.mnemonic import is_new_seed, is_old_seed, seed_type +from electrum.mnemonic import is_new_seed, is_old_seed, seed_type, is_matching_seed from electrum.version import SEED_PREFIX_SW, SEED_PREFIX from . import ElectrumTestCase @@ -193,6 +193,26 @@ def test_seed_type(self): with self.subTest(msg=f"seed_type_subcase_{idx}", seed_words=seed_words): self.assertEqual(_type, seed_type(seed_words), msg=seed_words) + def test_is_matching_seed(self): + self.assertTrue(is_matching_seed(seed="9dk", seed_again="9dk ")) + self.assertTrue(is_matching_seed(seed="9dk", seed_again=" 9dk")) + self.assertTrue(is_matching_seed(seed="9dk", seed_again=" 9dk ")) + self.assertTrue(is_matching_seed(seed="when blade focus", seed_again="when blade focus ")) + self.assertTrue(is_matching_seed(seed="when blade focus", seed_again=" when blade focus ")) + self.assertTrue(is_matching_seed(seed=" when blade focus ", seed_again=" when blade focus ")) + self.assertTrue(is_matching_seed( + seed=" when blade focus ", + seed_again= + """ when blade + + focus """)) + + self.assertFalse(is_matching_seed(seed="when blade focus", seed_again="wen blade focus")) + self.assertFalse(is_matching_seed(seed="when blade focus", seed_again="when bladefocus")) + self.assertFalse(is_matching_seed(seed="when blade focus", seed_again="when blAde focus")) + self.assertFalse(is_matching_seed(seed="when blade focus", seed_again="when bl4de focus")) + self.assertFalse(is_matching_seed(seed="when blade focus", seed_again="when bla4de focus")) + class Test_slip39(ElectrumTestCase): """ Test SLIP39 test vectors. """ From 9f045e546f315ae3fa05bedf31570b9389f23de4 Mon Sep 17 00:00:00 2001 From: Nikita Zhavoronkov Date: Sat, 20 May 2023 04:29:44 +0600 Subject: [PATCH 0925/1143] Add 3xpl.com to the list of explorers --- electrum/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/util.py b/electrum/util.py index c091fa196..cd1a9966f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -884,6 +884,8 @@ def age( return _("in over {} years").format(round(distance_in_minutes / 525600)) mainnet_block_explorers = { + '3xpl.com': ('https://3xpl.com/bitcoin/', + {'tx': 'transaction/', 'addr': 'address/'}), 'Bitupper Explorer': ('https://bitupper.com/en/explorer/bitcoin/', {'tx': 'transactions/', 'addr': 'addresses/'}), 'Bitflyer.jp': ('https://chainflyer.bitflyer.jp/', From 603088a79f0c8ae7f1d786cfcabb29049182f359 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 19 May 2023 23:03:27 +0000 Subject: [PATCH 0926/1143] util: simplify profiler --- electrum/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index c091fa196..bf9fa88d0 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -461,9 +461,9 @@ def profiler(func=None, *, min_threshold: Union[int, float, None] = None): min_threshold: if set, only log if time taken is higher than threshold NOTE: does not work with async methods. """ - if func is None: + if func is None: # to make "@profiler(...)" work. (in addition to bare "@profiler") return partial(profiler, min_threshold=min_threshold) - def do_profile(args, kw_args): + def do_profile(*args, **kw_args): name = func.__qualname__ t0 = time.time() o = func(*args, **kw_args) @@ -471,7 +471,7 @@ def do_profile(args, kw_args): if min_threshold is None or t > min_threshold: _profiler_logger.debug(f"{name} {t:,.4f} sec") return o - return lambda *args, **kw_args: do_profile(args, kw_args) + return do_profile def android_ext_dir(): From 3861b7c152333a2cc7472060aaebb9be1f66f437 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 22 May 2023 13:12:26 +0000 Subject: [PATCH 0927/1143] contrib/make_download: adapt to downloads being merged into index.html see https://github.com/spesmilo/electrum-web/pull/20 --- contrib/make_download | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/contrib/make_download b/contrib/make_download index 70eb7f003..b2fd315c3 100755 --- a/contrib/make_download +++ b/contrib/make_download @@ -6,6 +6,10 @@ import importlib from collections import defaultdict +if len(sys.argv) < 2: + print(f"ERROR. usage: {os.path.basename(__file__)} ", file=sys.stderr) + sys.exit(1) + # cd to project root os.chdir(os.path.dirname(os.path.dirname(__file__))) @@ -16,24 +20,24 @@ version_spec.loader.exec_module(version_module) ELECTRUM_VERSION = version_module.ELECTRUM_VERSION APK_VERSION = version_module.APK_VERSION -print("version", ELECTRUM_VERSION) +print(f"version: {ELECTRUM_VERSION}", file=sys.stderr) dirname = sys.argv[1] -print("directory", dirname) +print(f"directory: {dirname}", file=sys.stderr) download_page = os.path.join(dirname, "panel-download.html") download_template = download_page + ".template" with open(download_template) as f: - string = f.read() + download_page_str = f.read() version = version_win = version_mac = version_android = ELECTRUM_VERSION -string = string.replace("##VERSION##", version) -string = string.replace("##VERSION_WIN##", version_win) -string = string.replace("##VERSION_MAC##", version_mac) -string = string.replace("##VERSION_ANDROID##", version_android) -string = string.replace("##VERSION_APK##", APK_VERSION) +download_page_str = download_page_str.replace("##VERSION##", version) +download_page_str = download_page_str.replace("##VERSION_WIN##", version_win) +download_page_str = download_page_str.replace("##VERSION_MAC##", version_mac) +download_page_str = download_page_str.replace("##VERSION_ANDROID##", version_android) +download_page_str = download_page_str.replace("##VERSION_APK##", APK_VERSION) # note: all dist files need to be listed here that we expect sigs for, # even if they are not linked to from the website @@ -67,18 +71,26 @@ for k, v in detected_sigs.items(): if k not in signers: signers.append(k) -print("signers:", signers) +print(f"signers: {signers}", file=sys.stderr) gpg_name = lambda x: 'sombernight_releasekey' if x=='SomberNight' else x signers_list = ', '.join("%s"%(gpg_name(x), x) for x in signers) -string = string.replace("##signers_list##", signers_list) +download_page_str = download_page_str.replace("##signers_list##", signers_list) for k, filename in files.items(): path = "dist/%s"%filename assert filename in list_dir link = "https://download.electrum.org/%s/%s"%(version, filename) - string = string.replace("##link_%s##"%k, link) - string = string.replace("##sigs_%s##"%k, link+'.asc') + download_page_str = download_page_str.replace("##link_%s##" % k, link) + download_page_str = download_page_str.replace("##sigs_%s##" % k, link + '.asc') + + +# download page has been constructed from template; now insert it into index.html +index_html_path = os.path.join(dirname, "index.html") +with open(f"{index_html_path}.template") as f: + index_html_str = f.read() + +index_html_str = index_html_str.replace("##DOWNLOAD_PAGE##", download_page_str) -with open(download_page,'w') as f: - f.write(string) +with open(index_html_path, 'w') as f: + f.write(index_html_str) From 03ab33f4b2f0415cdbf9f8c442de94c91c8828af Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 23 May 2023 13:45:47 +0000 Subject: [PATCH 0928/1143] SimpleConfig: change API of set_key(): "save" is now kwarg-only --- electrum/exchange_rate.py | 4 ++-- electrum/gui/kivy/main_window.py | 8 ++++---- electrum/gui/kivy/uix/dialogs/fee_dialog.py | 10 +++++----- electrum/gui/kivy/uix/dialogs/settings.py | 2 +- electrum/gui/qml/qeconfig.py | 10 +++++----- electrum/gui/qml/qetxfinalizer.py | 10 +++++----- electrum/gui/qt/__init__.py | 2 +- electrum/gui/qt/confirm_tx_dialog.py | 6 +++--- electrum/gui/qt/installwizard.py | 4 ++-- electrum/gui/qt/settings_dialog.py | 14 +++++++------- electrum/gui/qt/swap_dialog.py | 6 +++--- electrum/gui/qt/util.py | 4 ++-- electrum/gui/text.py | 2 +- electrum/network.py | 8 ++++---- electrum/plugin.py | 4 ++-- electrum/plugins/trustedcoin/qt.py | 2 +- electrum/simple_config.py | 8 ++++---- 17 files changed, 52 insertions(+), 52 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 9266cf01a..8a0ac21e6 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -599,7 +599,7 @@ def config_exchange(self): def set_currency(self, ccy: str): self.ccy = ccy - self.config.set_key('currency', ccy, True) + self.config.set_key('currency', ccy, save=True) self.trigger_update() self.on_quotes() @@ -611,7 +611,7 @@ def set_exchange(self, name): class_ = globals().get(name) or globals().get(DEFAULT_EXCHANGE) self.logger.info(f"using exchange {name}") if self.config_exchange() != name: - self.config.set_key('use_exchange', name, True) + self.config.set_key('use_exchange', name, save=True) assert issubclass(class_, ExchangeBase), f"unexpected type {class_} for {name}" self.exchange = class_(self.on_quotes, self.on_history) # type: ExchangeBase # A new exchange means new fx quotes, initially empty. Force diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 3a053373d..eb1a5fba5 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -196,7 +196,7 @@ def cb(name): use_gossip = BooleanProperty(False) def on_use_gossip(self, instance, x): - self.electrum_config.set_key('use_gossip', self.use_gossip, True) + self.electrum_config.set_key('use_gossip', self.use_gossip, save=True) if self.network: if self.use_gossip: self.network.start_gossip() @@ -206,7 +206,7 @@ def on_use_gossip(self, instance, x): enable_debug_logs = BooleanProperty(False) def on_enable_debug_logs(self, instance, x): - self.electrum_config.set_key('gui_enable_debug_logs', self.enable_debug_logs, True) + self.electrum_config.set_key('gui_enable_debug_logs', self.enable_debug_logs, save=True) use_change = BooleanProperty(False) def on_use_change(self, instance, x): @@ -217,11 +217,11 @@ def on_use_change(self, instance, x): use_unconfirmed = BooleanProperty(False) def on_use_unconfirmed(self, instance, x): - self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, True) + self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, save=True) use_recoverable_channels = BooleanProperty(True) def on_use_recoverable_channels(self, instance, x): - self.electrum_config.set_key('use_recoverable_channels', self.use_recoverable_channels, True) + self.electrum_config.set_key('use_recoverable_channels', self.use_recoverable_channels, save=True) def switch_to_send_screen(func): # try until send_screen is available diff --git a/electrum/gui/kivy/uix/dialogs/fee_dialog.py b/electrum/gui/kivy/uix/dialogs/fee_dialog.py index 59b1a5784..af0ab98b8 100644 --- a/electrum/gui/kivy/uix/dialogs/fee_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/fee_dialog.py @@ -99,15 +99,15 @@ def read_config(self): def save_config(self): value = int(self.slider.value) dynfees, mempool = self.get_method() - self.config.set_key('dynamic_fees', dynfees, False) - self.config.set_key('mempool_fees', mempool, False) + self.config.set_key('dynamic_fees', dynfees, save=False) + self.config.set_key('mempool_fees', mempool, save=False) if dynfees: if mempool: - self.config.set_key('depth_level', value, True) + self.config.set_key('depth_level', value, save=True) else: - self.config.set_key('fee_level', value, True) + self.config.set_key('fee_level', value, save=True) else: - self.config.set_key('fee_per_kb', self.config.static_fee(value), True) + self.config.set_key('fee_per_kb', self.config.static_fee(value), save=True) def update_text(self): pass diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py index c5fc9da4b..7e5007464 100644 --- a/electrum/gui/kivy/uix/dialogs/settings.py +++ b/electrum/gui/kivy/uix/dialogs/settings.py @@ -159,7 +159,7 @@ def language_dialog(self, item, dt): if self._language_dialog is None: l = self.config.get('language') or '' def cb(key): - self.config.set_key("language", key, True) + self.config.set_key("language", key, save=True) item.lang = self.get_language_name() self.app.language = key self._language_dialog = ChoiceDialog(_('Language'), languages, l, cb) diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 93e45460d..7e032028b 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -55,7 +55,7 @@ def autoConnect(self): @autoConnect.setter def autoConnect(self, auto_connect): - self.config.set_key('auto_connect', auto_connect, True) + self.config.set_key('auto_connect', auto_connect, save=True) self.autoConnectChanged.emit() # auto_connect is actually a tri-state, expose the undefined case @@ -70,7 +70,7 @@ def manualServer(self): @manualServer.setter def manualServer(self, oneserver): - self.config.set_key('oneserver', oneserver, True) + self.config.set_key('oneserver', oneserver, save=True) self.manualServerChanged.emit() baseUnitChanged = pyqtSignal() @@ -113,7 +113,7 @@ def spendUnconfirmed(self): @spendUnconfirmed.setter def spendUnconfirmed(self, checked): - self.config.set_key('confirmed_only', not checked, True) + self.config.set_key('confirmed_only', not checked, save=True) self.spendUnconfirmedChanged.emit() requestExpiryChanged = pyqtSignal() @@ -136,12 +136,12 @@ def pinCode(self, pin_code): if pin_code == '': self.pinCodeRemoveAuth() else: - self.config.set_key('pin_code', pin_code, True) + self.config.set_key('pin_code', pin_code, save=True) self.pinCodeChanged.emit() @auth_protect(method='wallet') def pinCodeRemoveAuth(self): - self.config.set_key('pin_code', '', True) + self.config.set_key('pin_code', '', save=True) self.pinCodeChanged.emit() useGossipChanged = pyqtSignal() diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 73bd2e305..59e621007 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -110,15 +110,15 @@ def read_config(self): def save_config(self): value = int(self._sliderPos) dynfees, mempool = self.get_method() - self._config.set_key('dynamic_fees', dynfees, False) - self._config.set_key('mempool_fees', mempool, False) + self._config.set_key('dynamic_fees', dynfees, save=False) + self._config.set_key('mempool_fees', mempool, save=False) if dynfees: if mempool: - self._config.set_key('depth_level', value, True) + self._config.set_key('depth_level', value, save=True) else: - self._config.set_key('fee_level', value, True) + self._config.set_key('fee_level', value, save=True) else: - self._config.set_key('fee_per_kb', self._config.static_fee(value), True) + self._config.set_key('fee_per_kb', self._config.static_fee(value), save=True) self.update_target() self.update() diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index e8d82d0ab..f06785a59 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -219,7 +219,7 @@ def toggle_tray_icon(self): if not self.tray: return self.dark_icon = not self.dark_icon - self.config.set_key("dark_icon", self.dark_icon, True) + self.config.set_key("dark_icon", self.dark_icon, save=True) self.tray.setIcon(self.tray_icon()) def tray_activated(self, reason): diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index 0845d5d66..ac0c118aa 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -127,11 +127,11 @@ def stop_editor_updates(self): def set_fee_config(self, dyn, pos, fee_rate): if dyn: if self.config.use_mempool_fees(): - self.config.set_key('depth_level', pos, False) + self.config.set_key('depth_level', pos, save=False) else: - self.config.set_key('fee_level', pos, False) + self.config.set_key('fee_level', pos, save=False) else: - self.config.set_key('fee_per_kb', fee_rate, False) + self.config.set_key('fee_per_kb', fee_rate, save=False) def update_tx(self, *, fallback_to_zero_fee: bool = False): # expected to set self.tx, self.message and self.error diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py index 5168f464e..2ad1e73a1 100644 --- a/electrum/gui/qt/installwizard.py +++ b/electrum/gui/qt/installwizard.py @@ -746,10 +746,10 @@ def init_network(self, network: 'Network'): nlayout = NetworkChoiceLayout(network, self.config, wizard=True) if self.exec_layout(nlayout.layout()): nlayout.accept() - self.config.set_key('auto_connect', network.auto_connect, True) + self.config.set_key('auto_connect', network.auto_connect, save=True) else: network.auto_connect = True - self.config.set_key('auto_connect', True, True) + self.config.set_key('auto_connect', True, save=True) @wizard_dialog def multisig_dialog(self, run_next): diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py index 4a4f59759..2a25e876d 100644 --- a/electrum/gui/qt/settings_dialog.py +++ b/electrum/gui/qt/settings_dialog.py @@ -83,7 +83,7 @@ def __init__(self, window: 'ElectrumWindow', config: 'SimpleConfig'): def on_lang(x): lang_request = list(languages.keys())[lang_combo.currentIndex()] if lang_request != self.config.get('language'): - self.config.set_key("language", lang_request, True) + self.config.set_key("language", lang_request, save=True) self.need_restart = True lang_combo.currentIndexChanged.connect(on_lang) @@ -99,7 +99,7 @@ def on_nz(): value = nz.value() if self.config.num_zeros != value: self.config.num_zeros = value - self.config.set_key('num_zeros', value, True) + self.config.set_key('num_zeros', value, save=True) self.app.refresh_tabs_signal.emit() self.app.update_status_signal.emit() nz.valueChanged.connect(on_nz) @@ -211,7 +211,7 @@ def on_set_thousandsep(v): qr_combo.addItem(cam_desc, cam_path) index = qr_combo.findData(self.config.get("video_device")) qr_combo.setCurrentIndex(index) - on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), True) + on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), save=True) qr_combo.currentIndexChanged.connect(on_video_device) colortheme_combo = QComboBox() @@ -221,7 +221,7 @@ def on_set_thousandsep(v): colortheme_combo.setCurrentIndex(index) colortheme_label = QLabel(_('Color theme') + ':') def on_colortheme(x): - self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), True) + self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), save=True) self.need_restart = True colortheme_combo.currentIndexChanged.connect(on_colortheme) @@ -279,8 +279,8 @@ def on_be_combo(x): on_be_edit() else: be_result = block_explorers[block_ex_combo.currentIndex()] - self.config.set_key('block_explorer_custom', None, False) - self.config.set_key('block_explorer', be_result, True) + self.config.set_key('block_explorer_custom', None, save=False) + self.config.set_key('block_explorer', be_result, save=True) showhide_block_ex_custom_e() block_ex_combo.currentIndexChanged.connect(on_be_combo) def on_be_edit(): @@ -429,7 +429,7 @@ def set_alias_color(self): def on_alias_edit(self): self.alias_e.setStyleSheet("") alias = str(self.alias_e.text()) - self.config.set_key('alias', alias, True) + self.config.set_key('alias', alias, save=True) if alias: self.wallet.contacts.fetch_openalias(self.config) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index a4686032c..1f47dc972 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -138,11 +138,11 @@ def init_recv_amount(self, recv_amount_sat): def fee_slider_callback(self, dyn, pos, fee_rate): if dyn: if self.config.use_mempool_fees(): - self.config.set_key('depth_level', pos, False) + self.config.set_key('depth_level', pos, save=False) else: - self.config.set_key('fee_level', pos, False) + self.config.set_key('fee_level', pos, save=False) else: - self.config.set_key('fee_per_kb', fee_rate, False) + self.config.set_key('fee_per_kb', fee_rate, save=False) if self.send_follows: self.on_recv_edited() else: diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 653d35288..bfae00a88 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -1051,7 +1051,7 @@ def getOpenFileName(*, parent, title, filter="", config: 'SimpleConfig') -> Opti directory = config.get('io_dir', os.path.expanduser('~')) fileName, __ = QFileDialog.getOpenFileName(parent, title, directory, filter) if fileName and directory != os.path.dirname(fileName): - config.set_key('io_dir', os.path.dirname(fileName), True) + config.set_key('io_dir', os.path.dirname(fileName), save=True) return fileName @@ -1082,7 +1082,7 @@ def getSaveFileName( selected_path = file_dialog.selectedFiles()[0] if selected_path and directory != os.path.dirname(selected_path): - config.set_key('io_dir', os.path.dirname(selected_path), True) + config.set_key('io_dir', os.path.dirname(selected_path), save=True) return selected_path diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 7fbad039f..b1d3eae75 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -747,7 +747,7 @@ def settings_dialog(self): if out: if out.get('Default fee'): fee = int(Decimal(out['Default fee']) * COIN) - self.config.set_key('fee_per_kb', fee, True) + self.config.set_key('fee_per_kb', fee, save=True) def password_dialog(self): out = self.run_dialog('Password', [ diff --git a/electrum/network.py b/electrum/network.py index 0def020ab..e1269899c 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -628,10 +628,10 @@ async def set_parameters(self, net_params: NetworkParameters): int(proxy['port']) except Exception: return - self.config.set_key('auto_connect', net_params.auto_connect, False) - self.config.set_key('oneserver', net_params.oneserver, False) - self.config.set_key('proxy', proxy_str, False) - self.config.set_key('server', str(server), True) + self.config.set_key('auto_connect', net_params.auto_connect, save=False) + self.config.set_key('oneserver', net_params.oneserver, save=False) + self.config.set_key('proxy', proxy_str, save=False) + self.config.set_key('server', str(server), save=True) # abort if changes were not allowed by config if self.config.get('server') != str(server) \ or self.config.get('proxy') != proxy_str \ diff --git a/electrum/plugin.py b/electrum/plugin.py index d080e7544..12a8cbae6 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -132,14 +132,14 @@ def close_plugin(self, plugin): self.remove_jobs(plugin.thread_jobs()) def enable(self, name: str) -> 'BasePlugin': - self.config.set_key('use_' + name, True, True) + self.config.set_key('use_' + name, True, save=True) p = self.get(name) if p: return p return self.load_plugin(name) def disable(self, name: str) -> None: - self.config.set_key('use_' + name, False, True) + self.config.set_key('use_' + name, False, save=True) p = self.get(name) if not p: return diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index 038fcbfe4..0aa918e4b 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -208,7 +208,7 @@ def show_settings_dialog(self, window, success): grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) b = QRadioButton() b.setChecked(k == n_prepay) - b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, True)) + b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, save=True)) grid.addWidget(b, i, 2) i += 1 diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 181e7ba80..1a73bdbfa 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -158,7 +158,7 @@ def rename_config_keys(self, config, keypairs, deprecation_warning=False): updated = True return updated - def set_key(self, key, value, save=True): + def set_key(self, key, value, *, save=True): if not self.is_modifiable(key): self.logger.warning(f"not changing config key '{key}' set on the command line") return @@ -168,9 +168,9 @@ def set_key(self, key, value, save=True): except Exception: self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})") return - self._set_key_in_user_config(key, value, save) + self._set_key_in_user_config(key, value, save=save) - def _set_key_in_user_config(self, key, value, save=True): + def _set_key_in_user_config(self, key, value, *, save=True): with self.lock: if value is not None: self.user_config[key] = value @@ -709,7 +709,7 @@ def get_base_unit(self): def set_base_unit(self, unit): assert unit in base_units.keys() self.decimal_point = base_unit_name_to_decimal_point(unit) - self.set_key('decimal_point', self.decimal_point, True) + self.set_key('decimal_point', self.decimal_point, save=True) def get_decimal_point(self): return self.decimal_point From 24980feab71957df05b1e55f72fbb7dc8cf34036 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 24 May 2023 17:41:44 +0000 Subject: [PATCH 0929/1143] config: introduce ConfigVars A new config API is introduced, and ~all of the codebase is adapted to it. The old API is kept but mainly only for dynamic usage where its extra flexibility is needed. Using examples, the old config API looked this: ``` >>> config.get("request_expiry", 86400) 604800 >>> config.set_key("request_expiry", 86400) >>> ``` The new config API instead: ``` >>> config.WALLET_PAYREQ_EXPIRY_SECONDS 604800 >>> config.WALLET_PAYREQ_EXPIRY_SECONDS = 86400 >>> ``` The old API operated on arbitrary string keys, the new one uses a static ~enum-like list of variables. With the new API: - there is a single centralised list of config variables, as opposed to these being scattered all over - no more duplication of default values (in the getters) - there is now some (minimal for now) type-validation/conversion for the config values closes https://github.com/spesmilo/electrum/pull/5640 closes https://github.com/spesmilo/electrum/pull/5649 Note: there is yet a third API added here, for certain niche/abstract use-cases, where we need a reference to the config variable itself. It should only be used when needed: ``` >>> var = config.cv.WALLET_PAYREQ_EXPIRY_SECONDS >>> var >>> var.get() 604800 >>> var.set(3600) >>> var.get_default_value() 86400 >>> var.is_set() True >>> var.is_modifiable() True ``` --- electrum/address_synchronizer.py | 2 +- electrum/base_crash_reporter.py | 1 - electrum/base_wizard.py | 4 +- electrum/coinchooser.py | 15 +- electrum/commands.py | 47 +-- electrum/contacts.py | 2 +- electrum/daemon.py | 42 +-- electrum/exchange_rate.py | 27 +- electrum/gui/kivy/main_window.py | 34 +- .../gui/kivy/uix/dialogs/crash_reporter.py | 12 +- electrum/gui/kivy/uix/dialogs/fee_dialog.py | 10 +- electrum/gui/kivy/uix/dialogs/settings.py | 8 +- electrum/gui/kivy/uix/screens.py | 4 +- electrum/gui/qml/qeapp.py | 4 +- electrum/gui/qml/qechannelopener.py | 5 +- electrum/gui/qml/qeconfig.py | 82 ++--- electrum/gui/qml/qedaemon.py | 4 +- electrum/gui/qml/qefx.py | 6 +- electrum/gui/qml/qeinvoice.py | 2 +- electrum/gui/qml/qetxfinalizer.py | 10 +- electrum/gui/qt/__init__.py | 10 +- electrum/gui/qt/address_list.py | 8 +- electrum/gui/qt/confirm_tx_dialog.py | 52 +-- electrum/gui/qt/exception_window.py | 4 +- electrum/gui/qt/fee_slider.py | 4 +- electrum/gui/qt/history_list.py | 12 +- electrum/gui/qt/installwizard.py | 4 +- electrum/gui/qt/main_window.py | 26 +- electrum/gui/qt/my_treeview.py | 28 +- electrum/gui/qt/network_dialog.py | 10 +- electrum/gui/qt/new_channel_dialog.py | 2 +- .../qt/qrreader/qtmultimedia/camera_dialog.py | 4 +- electrum/gui/qt/receive_tab.py | 26 +- electrum/gui/qt/settings_dialog.py | 75 ++-- electrum/gui/qt/swap_dialog.py | 8 +- electrum/gui/qt/transaction_dialog.py | 4 +- electrum/gui/qt/util.py | 10 +- electrum/gui/text.py | 6 +- electrum/interface.py | 8 +- electrum/lnpeer.py | 26 +- electrum/lnworker.py | 12 +- electrum/logging.py | 4 +- electrum/network.py | 38 +- electrum/paymentrequest.py | 10 +- electrum/plugins/ledger/ledger.py | 1 - electrum/plugins/payserver/payserver.py | 12 +- electrum/plugins/payserver/qt.py | 14 +- electrum/plugins/trustedcoin/qt.py | 4 +- electrum/plugins/trustedcoin/trustedcoin.py | 12 +- electrum/simple_config.py | 326 +++++++++++++++--- electrum/submarine_swaps.py | 2 +- electrum/tests/test_daemon.py | 4 +- electrum/tests/test_lnpeer.py | 42 +-- electrum/tests/test_simple_config.py | 73 ++++ electrum/tests/test_sswaps.py | 4 +- electrum/tests/test_wallet_vertical.py | 10 +- electrum/util.py | 11 +- electrum/verifier.py | 2 +- electrum/wallet.py | 8 +- run_electrum | 15 +- 60 files changed, 781 insertions(+), 471 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 0b8578213..b42be0474 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -959,7 +959,7 @@ def address_is_old(self, address: str, *, req_conf: int = 3) -> bool: """ max_conf = -1 h = self.db.get_addr_history(address) - needs_spv_check = not self.config.get("skipmerklecheck", False) + needs_spv_check = not self.config.NETWORK_SKIPMERKLECHECK for tx_hash, tx_height in h: if needs_spv_check: tx_age = self.get_tx_height(tx_hash).conf diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index 8471ee679..6d113dd3b 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -42,7 +42,6 @@ class CrashReportResponse(NamedTuple): class BaseCrashReporter(Logger): report_server = "https://crashhub.electrum.org" - config_key = "show_crash_reporter" issue_template = """

Traceback

 {traceback}
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
index acb77ec04..e7b88d1a2 100644
--- a/electrum/base_wizard.py
+++ b/electrum/base_wizard.py
@@ -92,7 +92,7 @@ def __init__(self, config: SimpleConfig, plugins: Plugins):
         self._stack = []  # type: List[WizardStackItem]
         self.plugin = None  # type: Optional[BasePlugin]
         self.keystores = []  # type: List[KeyStore]
-        self.is_kivy = config.get('gui') == 'kivy'
+        self.is_kivy = config.GUI_NAME == 'kivy'
         self.seed_type = None
 
     def set_icon(self, icon):
@@ -697,7 +697,7 @@ def show_xpub_and_add_cosigners(self, xpub):
         self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
 
     def choose_seed_type(self):
-        seed_type = 'standard' if self.config.get('nosegwit') else 'segwit'
+        seed_type = 'standard' if self.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit'
         self.create_seed(seed_type)
 
     def create_seed(self, seed_type):
diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py
index e59e3e6d7..aefd39603 100644
--- a/electrum/coinchooser.py
+++ b/electrum/coinchooser.py
@@ -24,7 +24,7 @@
 # SOFTWARE.
 from collections import defaultdict
 from math import floor, log10
-from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple, Mapping, Type
+from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple, Mapping, Type, TYPE_CHECKING
 from decimal import Decimal
 
 from .bitcoin import sha256, COIN, is_address
@@ -32,6 +32,9 @@
 from .util import NotEnoughFunds
 from .logging import Logger
 
+if TYPE_CHECKING:
+    from .simple_config import SimpleConfig
+
 
 # A simple deterministic PRNG.  Used to deterministically shuffle a
 # set of coins - the same set of coins should produce the same output.
@@ -484,13 +487,13 @@ def penalty(buckets: List[Bucket]) -> ScoredCandidate:
     'Privacy': CoinChooserPrivacy,
 }  # type: Mapping[str, Type[CoinChooserBase]]
 
-def get_name(config):
-    kind = config.get('coin_chooser')
+def get_name(config: 'SimpleConfig') -> str:
+    kind = config.WALLET_COIN_CHOOSER_POLICY
     if kind not in COIN_CHOOSERS:
-        kind = 'Privacy'
+        kind = config.cv.WALLET_COIN_CHOOSER_POLICY.get_default_value()
     return kind
 
-def get_coin_chooser(config) -> CoinChooserBase:
+def get_coin_chooser(config: 'SimpleConfig') -> CoinChooserBase:
     klass = COIN_CHOOSERS[get_name(config)]
     # note: we enable enable_output_value_rounding by default as
     #       - for sacrificing a few satoshis
@@ -498,6 +501,6 @@ def get_coin_chooser(config) -> CoinChooserBase:
     #       + it also helps the network as a whole as fees will become noisier
     #         (trying to counter the heuristic that "whole integer sat/byte feerates" are common)
     coinchooser = klass(
-        enable_output_value_rounding=config.get('coin_chooser_output_rounding', True),
+        enable_output_value_rounding=config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING,
     )
     return coinchooser
diff --git a/electrum/commands.py b/electrum/commands.py
index 2038a6f85..336e69a47 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -308,7 +308,7 @@ async def getconfig(self, key):
 
     @classmethod
     def _setconfig_normalize_value(cls, key, value):
-        if key not in ('rpcuser', 'rpcpassword'):
+        if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()):
             value = json_decode(value)
             # call literal_eval for backward compatibility (see #4225)
             try:
@@ -321,9 +321,9 @@ def _setconfig_normalize_value(cls, key, value):
     async def setconfig(self, key, value):
         """Set a configuration variable. 'value' may be a string or a Python expression."""
         value = self._setconfig_normalize_value(key, value)
-        if self.daemon and key == 'rpcuser':
+        if self.daemon and key == SimpleConfig.RPC_USERNAME.key():
             self.daemon.commands_server.rpc_user = value
-        if self.daemon and key == 'rpcpassword':
+        if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():
             self.daemon.commands_server.rpc_password = value
         self.config.set_key(key, value)
         return True
@@ -1149,7 +1149,7 @@ async def lnpay(self, invoice, timeout=120, wallet: Abstract_Wallet = None):
 
     @command('wl')
     async def nodeid(self, wallet: Abstract_Wallet = None):
-        listen_addr = self.config.get('lightning_listen')
+        listen_addr = self.config.LIGHTNING_LISTEN
         return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')
 
     @command('wl')
@@ -1545,13 +1545,14 @@ def subparser_call(self, parser, namespace, values, option_string=None):
 
 
 def add_network_options(parser):
-    parser.add_argument("-f", "--serverfingerprint", dest="serverfingerprint", default=None, help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint." + " " +
-                                                                                                  "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
-    parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
-    parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
-    parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port] (or 'none' to disable proxy), where type is socks4,socks5 or http")
-    parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
-    parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=None, help="Tolerate invalid merkle proofs from server")
+    parser.add_argument("-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,
+                        help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
+                             "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
+    parser.add_argument("-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None, help="connect to one server only")
+    parser.add_argument("-s", "--server", dest=SimpleConfig.NETWORK_SERVER.key(), default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
+    parser.add_argument("-p", "--proxy", dest=SimpleConfig.NETWORK_PROXY.key(), default=None, help="set proxy [type:]host[:port] (or 'none' to disable proxy), where type is socks4,socks5 or http")
+    parser.add_argument("--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None, help="do not try to connect to onion servers")
+    parser.add_argument("--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None, help="Tolerate invalid merkle proofs from server")
 
 def add_global_options(parser):
     group = parser.add_argument_group('global options')
@@ -1563,13 +1564,13 @@ def add_global_options(parser):
     group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
     group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
     group.add_argument("--signet", action="store_true", dest="signet", default=False, help="Use Signet")
-    group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
-    group.add_argument("--rpcuser", dest="rpcuser", default=argparse.SUPPRESS, help="RPC user")
-    group.add_argument("--rpcpassword", dest="rpcpassword", default=argparse.SUPPRESS, help="RPC password")
+    group.add_argument("-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None, help="Run offline")
+    group.add_argument("--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS, help="RPC user")
+    group.add_argument("--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS, help="RPC password")
 
 def add_wallet_option(parser):
     parser.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
-    parser.add_argument("--forgetconfig", action="store_true", dest="forget_config", default=False, help="Forget config on exit")
+    parser.add_argument("--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=False, help="Forget config on exit")
 
 def get_parser():
     # create main parser
@@ -1582,11 +1583,11 @@ def get_parser():
     # gui
     parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
     parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
-    parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
-    parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
-    parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
+    parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
+    parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup")
+    parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI")
     parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
-    parser_gui.add_argument("--nosegwit", action="store_true", dest="nosegwit", default=False, help="Do not create segwit wallets")
+    parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets")
     add_wallet_option(parser_gui)
     add_network_options(parser_gui)
     add_global_options(parser_gui)
@@ -1595,10 +1596,10 @@ def get_parser():
     parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
     # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
     #        instead it reads it from the daemon lockfile.
-    parser_daemon.add_argument("--rpchost", dest="rpchost", default=argparse.SUPPRESS, help="RPC host")
-    parser_daemon.add_argument("--rpcport", dest="rpcport", type=int, default=argparse.SUPPRESS, help="RPC port")
-    parser_daemon.add_argument("--rpcsock", dest="rpcsock", default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto'])
-    parser_daemon.add_argument("--rpcsockpath", dest="rpcsockpath", help="where to place RPC file socket")
+    parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host")
+    parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port")
+    parser_daemon.add_argument("--rpcsock", dest=SimpleConfig.RPC_SOCKET_TYPE.key(), default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto'])
+    parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket")
     add_network_options(parser_daemon)
     add_global_options(parser_daemon)
     # commands
diff --git a/electrum/contacts.py b/electrum/contacts.py
index 69e8dc060..cc7906554 100644
--- a/electrum/contacts.py
+++ b/electrum/contacts.py
@@ -98,7 +98,7 @@ def resolve(self, k):
 
     def fetch_openalias(self, config):
         self.alias_info = None
-        alias = config.get('alias')
+        alias = config.OPENALIAS_ID
         if alias:
             alias = str(alias)
             def f():
diff --git a/electrum/daemon.py b/electrum/daemon.py
index 674e143bc..8b6485801 100644
--- a/electrum/daemon.py
+++ b/electrum/daemon.py
@@ -69,7 +69,7 @@ def get_rpcsock_defaultpath(config: SimpleConfig):
     return os.path.join(config.path, 'daemon_rpc_socket')
 
 def get_rpcsock_default_type(config: SimpleConfig):
-    if config.get('rpcport'):
+    if config.RPC_PORT:
         return 'tcp'
     # Use unix domain sockets when available,
     # with the extra paranoia that in case windows "implements" them,
@@ -106,7 +106,7 @@ def get_file_descriptor(config: SimpleConfig):
 
 
 
-def request(config: SimpleConfig, endpoint, args=(), timeout=60):
+def request(config: SimpleConfig, endpoint, args=(), timeout: Union[float, int] = 60):
     lockfile = get_lockfile(config)
     while True:
         create_time = None
@@ -152,12 +152,8 @@ async def request_coroutine(
 
 
 def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
-    rpc_user = config.get('rpcuser', None)
-    rpc_password = config.get('rpcpassword', None)
-    if rpc_user == '':
-        rpc_user = None
-    if rpc_password == '':
-        rpc_password = None
+    rpc_user = config.RPC_USERNAME or None
+    rpc_password = config.RPC_PASSWORD or None
     if rpc_user is None or rpc_password is None:
         rpc_user = 'user'
         bits = 128
@@ -166,8 +162,8 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
         pw_b64 = b64encode(
             pw_int.to_bytes(nbytes, 'big'), b'-_')
         rpc_password = to_string(pw_b64, 'ascii')
-        config.set_key('rpcuser', rpc_user)
-        config.set_key('rpcpassword', rpc_password, save=True)
+        config.RPC_USERNAME = rpc_user
+        config.RPC_PASSWORD = rpc_password
     return rpc_user, rpc_password
 
 
@@ -252,17 +248,17 @@ async def handle(self, request):
 
 class CommandsServer(AuthenticatedServer):
 
-    def __init__(self, daemon, fd):
+    def __init__(self, daemon: 'Daemon', fd):
         rpc_user, rpc_password = get_rpc_credentials(daemon.config)
         AuthenticatedServer.__init__(self, rpc_user, rpc_password)
         self.daemon = daemon
         self.fd = fd
         self.config = daemon.config
-        sockettype = self.config.get('rpcsock', 'auto')
+        sockettype = self.config.RPC_SOCKET_TYPE
         self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config)
-        self.sockpath = self.config.get('rpcsockpath', get_rpcsock_defaultpath(self.config))
-        self.host = self.config.get('rpchost', '127.0.0.1')
-        self.port = self.config.get('rpcport', 0)
+        self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config)
+        self.host = self.config.RPC_HOST
+        self.port = self.config.RPC_PORT
         self.app = web.Application()
         self.app.router.add_post("/", self.handle)
         self.register_method(self.ping)
@@ -348,12 +344,12 @@ async def run_cmdline(self, config_options):
 
 class WatchTowerServer(AuthenticatedServer):
 
-    def __init__(self, network, netaddress):
+    def __init__(self, network: 'Network', netaddress):
         self.addr = netaddress
         self.config = network.config
         self.network = network
-        watchtower_user = self.config.get('watchtower_user', '')
-        watchtower_password = self.config.get('watchtower_password', '')
+        watchtower_user = self.config.WATCHTOWER_SERVER_USER or ""
+        watchtower_password = self.config.WATCHTOWER_SERVER_PASSWORD or ""
         AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)
         self.lnwatcher = network.local_watchtower
         self.app = web.Application()
@@ -403,7 +399,7 @@ def __init__(
             self.logger.warning("Ignoring parameter 'wallet_path' for daemon. "
                                 "Use the load_wallet command instead.")
         self.asyncio_loop = util.get_asyncio_loop()
-        if not config.get('offline'):
+        if not self.config.NETWORK_OFFLINE:
             self.network = Network(config, daemon=self)
         self.fx = FxThread(config=config)
         # path -> wallet;   make sure path is standardized.
@@ -444,16 +440,16 @@ async def _run(self, jobs: Iterable = None):
 
     def start_network(self):
         self.logger.info(f"starting network.")
-        assert not self.config.get('offline')
+        assert not self.config.NETWORK_OFFLINE
         assert self.network
         # server-side watchtower
-        if watchtower_address := self.config.get_netaddress('watchtower_address'):
+        if watchtower_address := self.config.get_netaddress(self.config.cv.WATCHTOWER_SERVER_ADDRESS):
             self.watchtower = WatchTowerServer(self.network, watchtower_address)
             asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.watchtower.run), self.asyncio_loop)
 
         self.network.start(jobs=[self.fx.run])
         # prepare lightning functionality, also load channel db early
-        if self.config.get('use_gossip', False):
+        if self.config.LIGHTNING_USE_GOSSIP:
             self.network.start_gossip()
 
     def with_wallet_lock(func):
@@ -582,7 +578,7 @@ async def stop(self):
 
     def run_gui(self, config: 'SimpleConfig', plugins: 'Plugins'):
         threading.current_thread().name = 'GUI'
-        gui_name = config.get('gui', 'qt')
+        gui_name = config.GUI_NAME
         if gui_name in ['lite', 'classic']:
             gui_name = 'qt'
         self.logger.info(f'launching GUI: {gui_name}')
diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py
index 8a0ac21e6..e3bdcc79e 100644
--- a/electrum/exchange_rate.py
+++ b/electrum/exchange_rate.py
@@ -23,11 +23,6 @@
 from .logging import Logger
 
 
-DEFAULT_ENABLED = False
-DEFAULT_CURRENCY = "EUR"
-DEFAULT_EXCHANGE = "CoinGecko"  # default exchange should ideally provide historical rates
-
-
 # See https://en.wikipedia.org/wiki/ISO_4217
 CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
                   'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
@@ -577,29 +572,29 @@ async def run(self):
             if self.is_enabled():
                 await self.exchange.update_safe(self.ccy)
 
-    def is_enabled(self):
-        return bool(self.config.get('use_exchange_rate', DEFAULT_ENABLED))
+    def is_enabled(self) -> bool:
+        return self.config.FX_USE_EXCHANGE_RATE
 
-    def set_enabled(self, b):
-        self.config.set_key('use_exchange_rate', bool(b))
+    def set_enabled(self, b: bool) -> None:
+        self.config.FX_USE_EXCHANGE_RATE = b
         self.trigger_update()
 
     def can_have_history(self):
         return self.is_enabled() and self.ccy in self.exchange.history_ccys()
 
     def has_history(self) -> bool:
-        return self.can_have_history() and bool(self.config.get('history_rates', False))
+        return self.can_have_history() and self.config.FX_HISTORY_RATES
 
     def get_currency(self) -> str:
         '''Use when dynamic fetching is needed'''
-        return self.config.get("currency", DEFAULT_CURRENCY)
+        return self.config.FX_CURRENCY
 
     def config_exchange(self):
-        return self.config.get('use_exchange', DEFAULT_EXCHANGE)
+        return self.config.FX_EXCHANGE
 
     def set_currency(self, ccy: str):
         self.ccy = ccy
-        self.config.set_key('currency', ccy, save=True)
+        self.config.FX_CURRENCY = ccy
         self.trigger_update()
         self.on_quotes()
 
@@ -608,10 +603,10 @@ def trigger_update(self):
         loop.call_soon_threadsafe(self._trigger.set)
 
     def set_exchange(self, name):
-        class_ = globals().get(name) or globals().get(DEFAULT_EXCHANGE)
+        class_ = globals().get(name) or globals().get(self.config.cv.FX_EXCHANGE.get_default_value())
         self.logger.info(f"using exchange {name}")
         if self.config_exchange() != name:
-            self.config.set_key('use_exchange', name, save=True)
+            self.config.FX_EXCHANGE = name
         assert issubclass(class_, ExchangeBase), f"unexpected type {class_} for {name}"
         self.exchange = class_(self.on_quotes, self.on_history)  # type: ExchangeBase
         # A new exchange means new fx quotes, initially empty.  Force
@@ -689,4 +684,4 @@ def timestamp_rate(self, timestamp: Optional[int]) -> Decimal:
         return self.history_rate(date)
 
 
-assert globals().get(DEFAULT_EXCHANGE), f"default exchange {DEFAULT_EXCHANGE} does not exist"
+assert globals().get(SimpleConfig.FX_EXCHANGE.get_default_value()), f"default exchange {SimpleConfig.FX_EXCHANGE.get_default_value()} does not exist"
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
index eb1a5fba5..6270436a2 100644
--- a/electrum/gui/kivy/main_window.py
+++ b/electrum/gui/kivy/main_window.py
@@ -25,6 +25,7 @@
 from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
 from electrum.logging import Logger
 from electrum.bitcoin import COIN
+from electrum.simple_config import SimpleConfig
 
 from electrum.gui import messages
 from .i18n import _
@@ -94,7 +95,6 @@
 
 if TYPE_CHECKING:
     from . import ElectrumGui
-    from electrum.simple_config import SimpleConfig
     from electrum.plugin import Plugins
     from electrum.paymentrequest import PaymentRequest
 
@@ -133,7 +133,7 @@ def on_auto_connect(self, instance, x):
     def set_auto_connect(self, b: bool):
         # This method makes sure we persist x into the config even if self.auto_connect == b.
         # Note: on_auto_connect() only gets called if the value of the self.auto_connect property *changes*.
-        self.electrum_config.set_key('auto_connect', b)
+        self.electrum_config.NETWORK_AUTO_CONNECT = b
         self.auto_connect = b
 
     def toggle_auto_connect(self, x):
@@ -196,7 +196,7 @@ def cb(name):
 
     use_gossip = BooleanProperty(False)
     def on_use_gossip(self, instance, x):
-        self.electrum_config.set_key('use_gossip', self.use_gossip, save=True)
+        self.electrum_config.LIGHTNING_USE_GOSSIP = self.use_gossip
         if self.network:
             if self.use_gossip:
                 self.network.start_gossip()
@@ -206,7 +206,7 @@ def on_use_gossip(self, instance, x):
 
     enable_debug_logs = BooleanProperty(False)
     def on_enable_debug_logs(self, instance, x):
-        self.electrum_config.set_key('gui_enable_debug_logs', self.enable_debug_logs, save=True)
+        self.electrum_config.GUI_ENABLE_DEBUG_LOGS = self.enable_debug_logs
 
     use_change = BooleanProperty(False)
     def on_use_change(self, instance, x):
@@ -217,11 +217,11 @@ def on_use_change(self, instance, x):
 
     use_unconfirmed = BooleanProperty(False)
     def on_use_unconfirmed(self, instance, x):
-        self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, save=True)
+        self.electrum_config.WALLET_SPEND_CONFIRMED_ONLY = not self.use_unconfirmed
 
     use_recoverable_channels = BooleanProperty(True)
     def on_use_recoverable_channels(self, instance, x):
-        self.electrum_config.set_key('use_recoverable_channels', self.use_recoverable_channels, save=True)
+        self.electrum_config.LIGHTNING_USE_RECOVERABLE_CHANNELS = self.use_recoverable_channels
 
     def switch_to_send_screen(func):
         # try until send_screen is available
@@ -414,7 +414,7 @@ def __init__(self, **kwargs):
         Logger.__init__(self)
 
         self.electrum_config = config = kwargs.get('config', None)  # type: SimpleConfig
-        self.language = config.get('language', get_default_language())
+        self.language = config.LOCALIZATION_LANGUAGE or get_default_language()
         self.network = network = kwargs.get('network', None)  # type: Network
         if self.network:
             self.num_blocks = self.network.get_local_height()
@@ -431,9 +431,9 @@ def __init__(self, **kwargs):
         self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
         self.daemon = self.gui_object.daemon
         self.fx = self.daemon.fx
-        self.use_gossip = config.get('use_gossip', False)
-        self.use_unconfirmed = not config.get('confirmed_only', False)
-        self.enable_debug_logs = config.get('gui_enable_debug_logs', False)
+        self.use_gossip = config.LIGHTNING_USE_GOSSIP
+        self.use_unconfirmed = not config.WALLET_SPEND_CONFIRMED_ONLY
+        self.enable_debug_logs = config.GUI_ENABLE_DEBUG_LOGS
 
         # create triggers so as to minimize updating a max of 2 times a sec
         self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
@@ -644,7 +644,7 @@ def on_start(self):
             self.on_new_intent(mactivity.getIntent())
             activity.bind(on_new_intent=self.on_new_intent)
         self.register_callbacks()
-        if self.network and self.electrum_config.get('auto_connect') is None:
+        if self.network and not self.electrum_config.cv.NETWORK_AUTO_CONNECT.is_set():
             self.popup_dialog("first_screen")
             # load_wallet_on_start will be called later, after initial network setup is completed
         else:
@@ -676,7 +676,7 @@ def get_wallet_path(self):
 
     def on_wizard_success(self, storage, db, password):
         self.password = password
-        if self.electrum_config.get('single_password'):
+        if self.electrum_config.WALLET_USE_SINGLE_PASSWORD:
             self._use_single_password = self.daemon.update_password_for_directory(
                 old_password=password, new_password=password)
         self.logger.info(f'use single password: {self._use_single_password}')
@@ -811,7 +811,7 @@ def on_event_channels(self, wallet):
             Clock.schedule_once(lambda dt: self._channels_dialog.update())
 
     def is_wallet_creation_disabled(self):
-        return bool(self.electrum_config.get('single_password')) and self.password is None
+        return self.electrum_config.WALLET_USE_SINGLE_PASSWORD and self.password is None
 
     def wallets_dialog(self):
         from .uix.dialogs.wallets import WalletDialog
@@ -1278,7 +1278,7 @@ def on_event_fee(self, *arg):
         self.set_fee_status()
 
     def protected(self, msg, f, args):
-        if self.electrum_config.get('pin_code'):
+        if self.electrum_config.CONFIG_PIN_CODE:
             msg += "\n" + _("Enter your PIN code to proceed")
             on_success = lambda pw: f(*args, self.password)
             d = PincodeDialog(
@@ -1337,10 +1337,10 @@ def _show_seed(self, label, password):
             label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
 
     def has_pin_code(self):
-        return bool(self.electrum_config.get('pin_code'))
+        return bool(self.electrum_config.CONFIG_PIN_CODE)
 
     def check_pin_code(self, pin):
-        if pin != self.electrum_config.get('pin_code'):
+        if pin != self.electrum_config.CONFIG_PIN_CODE:
             raise InvalidPassword
 
     def change_password(self, cb):
@@ -1386,7 +1386,7 @@ def reset_pin_code(self, cb):
         d.open()
 
     def _set_new_pin_code(self, new_pin, cb):
-        self.electrum_config.set_key('pin_code', new_pin)
+        self.electrum_config.CONFIG_PIN_CODE = new_pin
         cb()
         self.show_info(_("PIN updated") if new_pin else _('PIN disabled'))
 
diff --git a/electrum/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
index f5aa0a9da..d28437f44 100644
--- a/electrum/gui/kivy/uix/dialogs/crash_reporter.py
+++ b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
@@ -1,5 +1,6 @@
 import sys
 import json
+from typing import TYPE_CHECKING
 
 from aiohttp.client_exceptions import ClientError
 from kivy import base, utils
@@ -15,6 +16,9 @@
 from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue
 from electrum.logging import Logger
 
+if TYPE_CHECKING:
+    from electrum.gui.kivy.main_window import ElectrumWindow
+
 
 Builder.load_string('''
 
@@ -95,7 +99,7 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
  * Locale: {locale}
         """
 
-    def __init__(self, main_window, exctype, value, tb):
+    def __init__(self, main_window: 'ElectrumWindow', exctype, value, tb):
         BaseCrashReporter.__init__(self, exctype, value, tb)
         Factory.Popup.__init__(self)
         self.main_window = main_window
@@ -156,7 +160,7 @@ def open_url(self, url):
         currentActivity.startActivity(browserIntent)
 
     def show_never(self):
-        self.main_window.electrum_config.set_key(BaseCrashReporter.config_key, False)
+        self.main_window.electrum_config.SHOW_CRASH_REPORTER = False
         self.dismiss()
 
     def get_user_description(self):
@@ -175,11 +179,11 @@ def __init__(self, text):
 
 
 class ExceptionHook(base.ExceptionHandler, Logger):
-    def __init__(self, main_window):
+    def __init__(self, main_window: 'ElectrumWindow'):
         base.ExceptionHandler.__init__(self)
         Logger.__init__(self)
         self.main_window = main_window
-        if not main_window.electrum_config.get(BaseCrashReporter.config_key, default=True):
+        if not main_window.electrum_config.SHOW_CRASH_REPORTER:
             EarlyExceptionsQueue.set_hook_as_ready()  # flush already queued exceptions
             return
         # For exceptions in Kivy:
diff --git a/electrum/gui/kivy/uix/dialogs/fee_dialog.py b/electrum/gui/kivy/uix/dialogs/fee_dialog.py
index af0ab98b8..2fa2436e9 100644
--- a/electrum/gui/kivy/uix/dialogs/fee_dialog.py
+++ b/electrum/gui/kivy/uix/dialogs/fee_dialog.py
@@ -99,15 +99,15 @@ def read_config(self):
     def save_config(self):
         value = int(self.slider.value)
         dynfees, mempool = self.get_method()
-        self.config.set_key('dynamic_fees', dynfees, save=False)
-        self.config.set_key('mempool_fees', mempool, save=False)
+        self.config.FEE_EST_DYNAMIC = dynfees
+        self.config.FEE_EST_USE_MEMPOOL = mempool
         if dynfees:
             if mempool:
-                self.config.set_key('depth_level', value, save=True)
+                self.config.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = value
             else:
-                self.config.set_key('fee_level', value, save=True)
+                self.config.FEE_EST_DYNAMIC_ETA_SLIDERPOS = value
         else:
-            self.config.set_key('fee_per_kb', self.config.static_fee(value), save=True)
+            self.config.FEE_EST_STATIC_FEERATE_FALLBACK = self.config.static_fee(value)
 
     def update_text(self):
         pass
diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py
index 7e5007464..3193fc130 100644
--- a/electrum/gui/kivy/uix/dialogs/settings.py
+++ b/electrum/gui/kivy/uix/dialogs/settings.py
@@ -146,7 +146,7 @@ def update(self):
         self.enable_toggle_use_recoverable_channels = bool(self.wallet.lnworker and self.wallet.lnworker.can_have_recoverable_channels())
 
     def get_language_name(self) -> str:
-        lang = self.config.get('language') or ''
+        lang = self.config.LOCALIZATION_LANGUAGE
         return languages.get(lang) or languages.get('') or ''
 
     def change_password(self, dt):
@@ -157,9 +157,9 @@ def change_pin_code(self, label, dt):
 
     def language_dialog(self, item, dt):
         if self._language_dialog is None:
-            l = self.config.get('language') or ''
+            l = self.config.LOCALIZATION_LANGUAGE
             def cb(key):
-                self.config.set_key("language", key, save=True)
+                self.config.LOCALIZATION_LANGUAGE = key
                 item.lang = self.get_language_name()
                 self.app.language = key
             self._language_dialog = ChoiceDialog(_('Language'), languages, l, cb)
@@ -194,7 +194,7 @@ def coinselect_dialog(self, item, dt):
             choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
             chooser_name = coinchooser.get_name(self.config)
             def cb(text):
-                self.config.set_key('coin_chooser', text)
+                self.config.WALLET_COIN_CHOOSER_POLICY = text
                 item.status = text
             self._coinselect_dialog = ChoiceDialog(_('Coin selection'), choosers, chooser_name, cb)
         self._coinselect_dialog.open()
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
index 9359fc207..23556ac92 100644
--- a/electrum/gui/kivy/uix/screens.py
+++ b/electrum/gui/kivy/uix/screens.py
@@ -480,7 +480,7 @@ def on_open(self):
         self.expiration_text = pr_expiration_values[c]
 
     def expiry(self):
-        return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        return self.app.electrum_config.WALLET_PAYREQ_EXPIRY_SECONDS
 
     def clear(self):
         self.address = ''
@@ -587,7 +587,7 @@ def show_item(self, obj):
     def expiration_dialog(self, obj):
         from .dialogs.choice_dialog import ChoiceDialog
         def callback(c):
-            self.app.electrum_config.set_key('request_expiry', c)
+            self.app.electrum_config.WALLET_PAYREQ_EXPIRY_SECONDS = c
             self.expiration_text = pr_expiration_values[c]
         d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback)
         d.open()
diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py
index c4f3c29c4..bf41bd932 100644
--- a/electrum/gui/qml/qeapp.py
+++ b/electrum/gui/qml/qeapp.py
@@ -272,7 +272,7 @@ def report_task():
 
     @pyqtSlot()
     def showNever(self):
-        self.config.set_key(BaseCrashReporter.config_key, False)
+        self.config.SHOW_CRASH_REPORTER = False
 
     @pyqtSlot(str)
     def setCrashUserText(self, text):
@@ -425,7 +425,7 @@ def __init__(self, *, config: 'SimpleConfig', slot):
 
     @classmethod
     def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None, slot = None) -> None:
-        if not config.get(BaseCrashReporter.config_key, default=True):
+        if not config.SHOW_CRASH_REPORTER:
             EarlyExceptionsQueue.set_hook_as_ready()  # flush already queued exceptions
             return
         if not cls._INSTANCE:
diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py
index fff270c00..d05e7d94f 100644
--- a/electrum/gui/qml/qechannelopener.py
+++ b/electrum/gui/qml/qechannelopener.py
@@ -1,6 +1,7 @@
 import threading
 from concurrent.futures import CancelledError
 from asyncio.exceptions import TimeoutError
+from typing import TYPE_CHECKING, Optional
 
 from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
 
@@ -32,7 +33,7 @@ class QEChannelOpener(QObject, AuthMixin):
     def __init__(self, parent=None):
         super().__init__(parent)
 
-        self._wallet = None
+        self._wallet = None  # type: Optional[QEWallet]
         self._connect_str = None
         self._amount = QEAmount()
         self._valid = False
@@ -101,7 +102,7 @@ def validate(self):
         connect_str_valid = False
         if self._connect_str:
             self._logger.debug(f'checking if {self._connect_str=!r} is valid')
-            if not self._wallet.wallet.config.get('use_gossip', False):
+            if not self._wallet.wallet.config.LIGHTNING_USE_GOSSIP:
                 # using trampoline: connect_str is the name of a trampoline node
                 peer_addr = hardcoded_trampoline_nodes()[self._connect_str]
                 self._node_pubkey = peer_addr.pubkey
diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py
index 7e032028b..159e3e0d1 100644
--- a/electrum/gui/qml/qeconfig.py
+++ b/electrum/gui/qml/qeconfig.py
@@ -9,13 +9,11 @@
 from electrum.logging import get_logger
 from electrum.util import DECIMAL_POINT_DEFAULT, base_unit_name_to_decimal_point
 from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING
+from electrum.simple_config import SimpleConfig
 
 from .qetypes import QEAmount
 from .auth import AuthMixin, auth_protect
 
-if TYPE_CHECKING:
-    from electrum.simple_config import SimpleConfig
-
 
 class QEConfig(AuthMixin, QObject):
     _logger = get_logger(__name__)
@@ -27,14 +25,14 @@ def __init__(self, config: 'SimpleConfig', parent=None):
     languageChanged = pyqtSignal()
     @pyqtProperty(str, notify=languageChanged)
     def language(self):
-        return self.config.get('language')
+        return self.config.LOCALIZATION_LANGUAGE
 
     @language.setter
     def language(self, language):
         if language not in languages:
             return
-        if self.config.get('language') != language:
-            self.config.set_key('language', language)
+        if self.config.LOCALIZATION_LANGUAGE != language:
+            self.config.LOCALIZATION_LANGUAGE = language
             set_language(language)
             self.languageChanged.emit()
 
@@ -51,27 +49,17 @@ def languagesAvailable(self):
     autoConnectChanged = pyqtSignal()
     @pyqtProperty(bool, notify=autoConnectChanged)
     def autoConnect(self):
-        return self.config.get('auto_connect')
+        return self.config.NETWORK_AUTO_CONNECT
 
     @autoConnect.setter
     def autoConnect(self, auto_connect):
-        self.config.set_key('auto_connect', auto_connect, save=True)
+        self.config.NETWORK_AUTO_CONNECT = auto_connect
         self.autoConnectChanged.emit()
 
     # auto_connect is actually a tri-state, expose the undefined case
     @pyqtProperty(bool, notify=autoConnectChanged)
     def autoConnectDefined(self):
-        return self.config.get('auto_connect') is not None
-
-    manualServerChanged = pyqtSignal()
-    @pyqtProperty(bool, notify=manualServerChanged)
-    def manualServer(self):
-        return self.config.get('oneserver')
-
-    @manualServer.setter
-    def manualServer(self, oneserver):
-        self.config.set_key('oneserver', oneserver, save=True)
-        self.manualServerChanged.emit()
+        return self.config.cv.NETWORK_AUTO_CONNECT.is_set()
 
     baseUnitChanged = pyqtSignal()
     @pyqtProperty(str, notify=baseUnitChanged)
@@ -98,129 +86,129 @@ def btcAmountRegex(self):
     thousandsSeparatorChanged = pyqtSignal()
     @pyqtProperty(bool, notify=thousandsSeparatorChanged)
     def thousandsSeparator(self):
-        return self.config.get('amt_add_thousands_sep', False)
+        return self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP
 
     @thousandsSeparator.setter
     def thousandsSeparator(self, checked):
-        self.config.set_key('amt_add_thousands_sep', checked)
+        self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked
         self.config.amt_add_thousands_sep = checked
         self.thousandsSeparatorChanged.emit()
 
     spendUnconfirmedChanged = pyqtSignal()
     @pyqtProperty(bool, notify=spendUnconfirmedChanged)
     def spendUnconfirmed(self):
-        return not self.config.get('confirmed_only', False)
+        return not self.config.WALLET_SPEND_CONFIRMED_ONLY
 
     @spendUnconfirmed.setter
     def spendUnconfirmed(self, checked):
-        self.config.set_key('confirmed_only', not checked, save=True)
+        self.config.WALLET_SPEND_CONFIRMED_ONLY = not checked
         self.spendUnconfirmedChanged.emit()
 
     requestExpiryChanged = pyqtSignal()
     @pyqtProperty(int, notify=requestExpiryChanged)
     def requestExpiry(self):
-        return self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        return self.config.WALLET_PAYREQ_EXPIRY_SECONDS
 
     @requestExpiry.setter
     def requestExpiry(self, expiry):
-        self.config.set_key('request_expiry', expiry)
+        self.config.WALLET_PAYREQ_EXPIRY_SECONDS = expiry
         self.requestExpiryChanged.emit()
 
     pinCodeChanged = pyqtSignal()
     @pyqtProperty(str, notify=pinCodeChanged)
     def pinCode(self):
-        return self.config.get('pin_code', '')
+        return self.config.CONFIG_PIN_CODE or ""
 
     @pinCode.setter
     def pinCode(self, pin_code):
         if pin_code == '':
             self.pinCodeRemoveAuth()
         else:
-            self.config.set_key('pin_code', pin_code, save=True)
+            self.config.CONFIG_PIN_CODE = pin_code
             self.pinCodeChanged.emit()
 
     @auth_protect(method='wallet')
     def pinCodeRemoveAuth(self):
-        self.config.set_key('pin_code', '', save=True)
+        self.config.CONFIG_PIN_CODE = ""
         self.pinCodeChanged.emit()
 
     useGossipChanged = pyqtSignal()
     @pyqtProperty(bool, notify=useGossipChanged)
     def useGossip(self):
-        return self.config.get('use_gossip', False)
+        return self.config.LIGHTNING_USE_GOSSIP
 
     @useGossip.setter
     def useGossip(self, gossip):
-        self.config.set_key('use_gossip', gossip)
+        self.config.LIGHTNING_USE_GOSSIP = gossip
         self.useGossipChanged.emit()
 
     useFallbackAddressChanged = pyqtSignal()
     @pyqtProperty(bool, notify=useFallbackAddressChanged)
     def useFallbackAddress(self):
-        return self.config.get('bolt11_fallback', True)
+        return self.config.WALLET_BOLT11_FALLBACK
 
     @useFallbackAddress.setter
     def useFallbackAddress(self, use_fallback):
-        self.config.set_key('bolt11_fallback', use_fallback)
+        self.config.WALLET_BOLT11_FALLBACK = use_fallback
         self.useFallbackAddressChanged.emit()
 
     enableDebugLogsChanged = pyqtSignal()
     @pyqtProperty(bool, notify=enableDebugLogsChanged)
     def enableDebugLogs(self):
-        gui_setting = self.config.get('gui_enable_debug_logs', False)
+        gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
         return gui_setting or bool(self.config.get('verbosity'))
 
     @pyqtProperty(bool, notify=enableDebugLogsChanged)
     def canToggleDebugLogs(self):
-        gui_setting = self.config.get('gui_enable_debug_logs', False)
+        gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
         return not self.config.get('verbosity') or gui_setting
 
     @enableDebugLogs.setter
     def enableDebugLogs(self, enable):
-        self.config.set_key('gui_enable_debug_logs', enable)
+        self.config.GUI_ENABLE_DEBUG_LOGS = enable
         self.enableDebugLogsChanged.emit()
 
     useRecoverableChannelsChanged = pyqtSignal()
     @pyqtProperty(bool, notify=useRecoverableChannelsChanged)
     def useRecoverableChannels(self):
-        return self.config.get('use_recoverable_channels', True)
+        return self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS
 
     @useRecoverableChannels.setter
     def useRecoverableChannels(self, useRecoverableChannels):
-        self.config.set_key('use_recoverable_channels', useRecoverableChannels)
+        self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS = useRecoverableChannels
         self.useRecoverableChannelsChanged.emit()
 
     trustedcoinPrepayChanged = pyqtSignal()
     @pyqtProperty(int, notify=trustedcoinPrepayChanged)
     def trustedcoinPrepay(self):
-        return self.config.get('trustedcoin_prepay', 20)
+        return self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY
 
     @trustedcoinPrepay.setter
     def trustedcoinPrepay(self, num_prepay):
-        if num_prepay != self.config.get('trustedcoin_prepay', 20):
-            self.config.set_key('trustedcoin_prepay', num_prepay)
+        if num_prepay != self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY:
+            self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = num_prepay
             self.trustedcoinPrepayChanged.emit()
 
     preferredRequestTypeChanged = pyqtSignal()
     @pyqtProperty(str, notify=preferredRequestTypeChanged)
     def preferredRequestType(self):
-        return self.config.get('preferred_request_type', 'bolt11')
+        return self.config.GUI_QML_PREFERRED_REQUEST_TYPE
 
     @preferredRequestType.setter
     def preferredRequestType(self, preferred_request_type):
-        if preferred_request_type != self.config.get('preferred_request_type', 'bolt11'):
-            self.config.set_key('preferred_request_type', preferred_request_type)
+        if preferred_request_type != self.config.GUI_QML_PREFERRED_REQUEST_TYPE:
+            self.config.GUI_QML_PREFERRED_REQUEST_TYPE = preferred_request_type
             self.preferredRequestTypeChanged.emit()
 
     userKnowsPressAndHoldChanged = pyqtSignal()
     @pyqtProperty(bool, notify=userKnowsPressAndHoldChanged)
     def userKnowsPressAndHold(self):
-        return self.config.get('user_knows_press_and_hold', False)
+        return self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD
 
     @userKnowsPressAndHold.setter
     def userKnowsPressAndHold(self, userKnowsPressAndHold):
-        if userKnowsPressAndHold != self.config.get('user_knows_press_and_hold', False):
-            self.config.set_key('user_knows_press_and_hold', userKnowsPressAndHold)
+        if userKnowsPressAndHold != self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD:
+            self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD = userKnowsPressAndHold
             self.userKnowsPressAndHoldChanged.emit()
 
 
@@ -251,7 +239,7 @@ def formatMilliSats(self, amount, with_unit=False):
 
     # TODO delegate all this to config.py/util.py
     def decimal_point(self):
-        return self.config.get('decimal_point', DECIMAL_POINT_DEFAULT)
+        return self.config.BTC_AMOUNTS_DECIMAL_POINT
 
     def max_precision(self):
         return self.decimal_point() + 0 #self.extra_precision
diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py
index 093d6a124..4d7c52d9d 100644
--- a/electrum/gui/qml/qedaemon.py
+++ b/electrum/gui/qml/qedaemon.py
@@ -162,7 +162,7 @@ def loadWallet(self, path=None, password=None):
         if path is None:
             self._path = self.daemon.config.get('wallet_path') # command line -w option
             if self._path is None:
-                self._path = self.daemon.config.get('gui_last_wallet')
+                self._path = self.daemon.config.GUI_LAST_WALLET
         else:
             self._path = path
         if self._path is None:
@@ -208,7 +208,7 @@ def load_wallet_task():
                     # we need the correct current wallet password below
                     local_password = QEWallet.getInstanceFor(wallet).password
 
-                if self.daemon.config.get('single_password'):
+                if self.daemon.config.WALLET_USE_SINGLE_PASSWORD:
                     self._use_single_password = self.daemon.update_password_for_directory(old_password=local_password, new_password=local_password)
                     self._password = local_password
                     self.singlePasswordChanged.emit()
diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py
index c742d990d..57df12c72 100644
--- a/electrum/gui/qml/qefx.py
+++ b/electrum/gui/qml/qefx.py
@@ -72,12 +72,14 @@ def fiatAmountRegex(self):
     historicRatesChanged = pyqtSignal()
     @pyqtProperty(bool, notify=historicRatesChanged)
     def historicRates(self):
-        return bool(self.fx.config.get('history_rates', True))
+        if not self.fx.config.cv.FX_HISTORY_RATES.is_set():
+            self.fx.config.FX_HISTORY_RATES = True  # override default
+        return self.fx.config.FX_HISTORY_RATES
 
     @historicRates.setter
     def historicRates(self, checked):
         if checked != self.historicRates:
-            self.fx.config.set_key('history_rates', bool(checked))
+            self.fx.config.FX_HISTORY_RATES = bool(checked)
             self.historicRatesChanged.emit()
             self.rateSourcesChanged.emit()
 
diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py
index 6ef09e7c7..a6f2a395c 100644
--- a/electrum/gui/qml/qeinvoice.py
+++ b/electrum/gui/qml/qeinvoice.py
@@ -387,7 +387,7 @@ def payLightningInvoice(self):
 
     def get_max_spendable_onchain(self):
         spendable = self._wallet.confirmedBalance.satsInt
-        if not self._wallet.wallet.config.get('confirmed_only', False):
+        if not self._wallet.wallet.config.WALLET_SPEND_CONFIRMED_ONLY:
             spendable += self._wallet.unconfirmedBalance.satsInt
         return spendable
 
diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py
index 59e621007..9748c1c60 100644
--- a/electrum/gui/qml/qetxfinalizer.py
+++ b/electrum/gui/qml/qetxfinalizer.py
@@ -110,15 +110,15 @@ def read_config(self):
     def save_config(self):
         value = int(self._sliderPos)
         dynfees, mempool = self.get_method()
-        self._config.set_key('dynamic_fees', dynfees, save=False)
-        self._config.set_key('mempool_fees', mempool, save=False)
+        self._config.FEE_EST_DYNAMIC = dynfees
+        self._config.FEE_EST_USE_MEMPOOL = mempool
         if dynfees:
             if mempool:
-                self._config.set_key('depth_level', value, save=True)
+                self._config.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = value
             else:
-                self._config.set_key('fee_level', value, save=True)
+                self._config.FEE_EST_DYNAMIC_ETA_SLIDERPOS = value
         else:
-            self._config.set_key('fee_per_kb', self._config.static_fee(value), save=True)
+            self._config.FEE_EST_STATIC_FEERATE_FALLBACK = self._config.static_fee(value)
         self.update_target()
         self.update()
 
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
index f06785a59..fdb54b98f 100644
--- a/electrum/gui/qt/__init__.py
+++ b/electrum/gui/qt/__init__.py
@@ -63,6 +63,7 @@
 from electrum.wallet_db import WalletDB
 from electrum.logging import Logger
 from electrum.gui import BaseElectrumGui
+from electrum.simple_config import SimpleConfig
 
 from .installwizard import InstallWizard, WalletAlreadyOpenInMemory
 from .util import read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin
@@ -75,7 +76,6 @@
 
 if TYPE_CHECKING:
     from electrum.daemon import Daemon
-    from electrum.simple_config import SimpleConfig
     from electrum.plugin import Plugins
 
 
@@ -139,7 +139,7 @@ def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugin
         self.watchtower_dialog = None
         self._num_wizards_in_progress = 0
         self._num_wizards_lock = threading.Lock()
-        self.dark_icon = self.config.get("dark_icon", False)
+        self.dark_icon = self.config.GUI_QT_DARK_TRAY_ICON
         self.tray = None
         self._init_tray()
         self.app.new_window_signal.connect(self.start_new_window)
@@ -167,7 +167,7 @@ def reload_app_stylesheet(self):
              - in Coins tab, the color for "frozen" UTXOs, or
              - in TxDialog, the receiving/change address colors
         """
-        use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark'
+        use_dark_theme = self.config.GUI_QT_COLOR_THEME == 'dark'
         if use_dark_theme:
             try:
                 import qdarkstyle
@@ -219,7 +219,7 @@ def toggle_tray_icon(self):
         if not self.tray:
             return
         self.dark_icon = not self.dark_icon
-        self.config.set_key("dark_icon", self.dark_icon, save=True)
+        self.config.GUI_QT_DARK_TRAY_ICON = self.dark_icon
         self.tray.setIcon(self.tray_icon())
 
     def tray_activated(self, reason):
@@ -436,7 +436,7 @@ def init_network(self):
         """Start the network, including showing a first-start network dialog if config does not exist."""
         if self.daemon.network:
             # first-start network-setup
-            if self.config.get('auto_connect') is None:
+            if not self.config.cv.NETWORK_AUTO_CONNECT.is_set():
                 wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
                 wizard.init_network(self.daemon.network)
                 wizard.terminate()
diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py
index 7989bbc2c..51effed92 100644
--- a/electrum/gui/qt/address_list.py
+++ b/electrum/gui/qt/address_list.py
@@ -36,6 +36,7 @@
 from electrum.plugin import run_hook
 from electrum.bitcoin import is_address
 from electrum.wallet import InternalAddressCorruption
+from electrum.simple_config import SimpleConfig
 
 from .util import MONOSPACE_FONT, ColorScheme, webopen
 from .my_treeview import MyTreeView, MySortModel
@@ -115,23 +116,24 @@ def __init__(self, main_window: 'ElectrumWindow'):
         self.setModel(self.proxy)
         self.update()
         self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder)
+        if self.config:
+            self.configvar_show_toolbar = self.config.cv.GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR
 
     def on_double_click(self, idx):
         addr = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR)
         self.main_window.show_address(addr)
 
-    CONFIG_KEY_SHOW_TOOLBAR = "show_toolbar_addresses"
     def create_toolbar(self, config):
         toolbar, menu = self.create_toolbar_with_menu('')
         self.num_addr_label = toolbar.itemAt(0).widget()
         self._toolbar_checkbox = menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar())
-        menu.addConfig(_('Show Fiat balances'), 'fiat_address', False, callback=self.main_window.app.update_fiat_signal.emit)
+        menu.addConfig(_('Show Fiat balances'), config.cv.FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES, callback=self.main_window.app.update_fiat_signal.emit)
         hbox = self.create_toolbar_buttons()
         toolbar.insertLayout(1, hbox)
         return toolbar
 
     def should_show_fiat(self):
-        return self.main_window.fx and self.main_window.fx.is_enabled() and self.config.get('fiat_address', False)
+        return self.main_window.fx and self.main_window.fx.is_enabled() and self.config.FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES
 
     def get_toolbar_buttons(self):
         return self.change_button, self.used_button
diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py
index ac0c118aa..de5dc296d 100644
--- a/electrum/gui/qt/confirm_tx_dialog.py
+++ b/electrum/gui/qt/confirm_tx_dialog.py
@@ -102,9 +102,9 @@ def __init__(self, *, title='',
         vbox.addStretch(1)
         vbox.addLayout(buttons)
 
-        self.set_io_visible(self.config.get('show_tx_io', False))
-        self.set_fee_edit_visible(self.config.get('show_tx_fee_details', False))
-        self.set_locktime_visible(self.config.get('show_tx_locktime', False))
+        self.set_io_visible(self.config.GUI_QT_TX_EDITOR_SHOW_IO)
+        self.set_fee_edit_visible(self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS)
+        self.set_locktime_visible(self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME)
         self.update_fee_target()
         self.resize(self.layout().sizeHint())
 
@@ -127,11 +127,11 @@ def stop_editor_updates(self):
     def set_fee_config(self, dyn, pos, fee_rate):
         if dyn:
             if self.config.use_mempool_fees():
-                self.config.set_key('depth_level', pos, save=False)
+                self.config.cv.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS.set(pos, save=False)
             else:
-                self.config.set_key('fee_level', pos, save=False)
+                self.config.cv.FEE_EST_DYNAMIC_ETA_SLIDERPOS.set(pos, save=False)
         else:
-            self.config.set_key('fee_per_kb', fee_rate, save=False)
+            self.config.cv.FEE_EST_STATIC_FEERATE_FALLBACK.set(fee_rate, save=False)
 
     def update_tx(self, *, fallback_to_zero_fee: bool = False):
         # expected to set self.tx, self.message and self.error
@@ -383,15 +383,15 @@ def add_pref_action(b, action, text, tooltip):
             m.setToolTip(tooltip)
             return m
         add_pref_action(
-            self.config.get('show_tx_io', False),
+            self.config.GUI_QT_TX_EDITOR_SHOW_IO,
             self.toggle_io_visibility,
             _('Show inputs and outputs'), '')
         add_pref_action(
-            self.config.get('show_tx_fee_details', False),
+            self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS,
             self.toggle_fee_details,
             _('Edit fees manually'), '')
         add_pref_action(
-            self.config.get('show_tx_locktime', False),
+            self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME,
             self.toggle_locktime,
             _('Edit Locktime'), '')
         self.pref_menu.addSeparator()
@@ -410,18 +410,18 @@ def add_pref_action(b, action, text, tooltip):
             ]))
         self.use_multi_change_menu.setEnabled(self.wallet.use_change)
         add_pref_action(
-            self.config.get('batch_rbf', False),
+            self.config.WALLET_BATCH_RBF,
             self.toggle_batch_rbf,
             _('Batch unconfirmed transactions'),
             _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \
             _('This will save fees, but might have unwanted effects in terms of privacy'))
         add_pref_action(
-            self.config.get('confirmed_only', False),
+            self.config.WALLET_SPEND_CONFIRMED_ONLY,
             self.toggle_confirmed_only,
             _('Spend only confirmed coins'),
             _('Spend only confirmed inputs.'))
         add_pref_action(
-            self.config.get('coin_chooser_output_rounding', True),
+            self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING,
             self.toggle_output_rounding,
             _('Enable output value rounding'),
             _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' + \
@@ -445,8 +445,8 @@ def resize_to_fit_content(self):
         self.resize(size)
 
     def toggle_output_rounding(self):
-        b = not self.config.get('coin_chooser_output_rounding', True)
-        self.config.set_key('coin_chooser_output_rounding', b)
+        b = not self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING
+        self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = b
         self.trigger_update()
 
     def toggle_use_change(self):
@@ -461,30 +461,30 @@ def toggle_multiple_change(self):
         self.trigger_update()
 
     def toggle_batch_rbf(self):
-        b = not self.config.get('batch_rbf', False)
-        self.config.set_key('batch_rbf', b)
+        b = not self.config.WALLET_BATCH_RBF
+        self.config.WALLET_BATCH_RBF = b
         self.trigger_update()
 
     def toggle_confirmed_only(self):
-        b = not self.config.get('confirmed_only', False)
-        self.config.set_key('confirmed_only', b)
+        b = not self.config.WALLET_SPEND_CONFIRMED_ONLY
+        self.config.WALLET_SPEND_CONFIRMED_ONLY = b
         self.trigger_update()
 
     def toggle_io_visibility(self):
-        b = not self.config.get('show_tx_io', False)
-        self.config.set_key('show_tx_io', b)
+        b = not self.config.GUI_QT_TX_EDITOR_SHOW_IO
+        self.config.GUI_QT_TX_EDITOR_SHOW_IO = b
         self.set_io_visible(b)
         self.resize_to_fit_content()
 
     def toggle_fee_details(self):
-        b = not self.config.get('show_tx_fee_details', False)
-        self.config.set_key('show_tx_fee_details', b)
+        b = not self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS
+        self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = b
         self.set_fee_edit_visible(b)
         self.resize_to_fit_content()
 
     def toggle_locktime(self):
-        b = not self.config.get('show_tx_locktime', False)
-        self.config.set_key('show_tx_locktime', b)
+        b = not self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME
+        self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME = b
         self.set_locktime_visible(b)
         self.resize_to_fit_content()
 
@@ -524,7 +524,7 @@ def _update_widgets(self):
         self._update_amount_label()
         if self.not_enough_funds:
             self.error = _('Not enough funds.')
-            confirmed_only = self.config.get('confirmed_only', False)
+            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
             if confirmed_only and self.can_pay_assuming_zero_fees(confirmed_only=False):
                 self.error += ' ' + _('Change your settings to allow spending unconfirmed coins.')
             elif self.can_pay_assuming_zero_fees(confirmed_only=confirmed_only):
@@ -631,7 +631,7 @@ def _update_amount_label(self):
 
     def update_tx(self, *, fallback_to_zero_fee: bool = False):
         fee_estimator = self.get_fee_estimator()
-        confirmed_only = self.config.get('confirmed_only', False)
+        confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
         try:
             self.tx = self.make_tx(fee_estimator, confirmed_only=confirmed_only)
             self.not_enough_funds = False
diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py
index d549d75d1..820d7db1a 100644
--- a/electrum/gui/qt/exception_window.py
+++ b/electrum/gui/qt/exception_window.py
@@ -132,7 +132,7 @@ def on_close(self):
         self.close()
 
     def show_never(self):
-        self.config.set_key(BaseCrashReporter.config_key, False)
+        self.config.SHOW_CRASH_REPORTER = False
         self.close()
 
     def closeEvent(self, event):
@@ -177,7 +177,7 @@ def __init__(self, *, config: 'SimpleConfig'):
 
     @classmethod
     def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None) -> None:
-        if not config.get(BaseCrashReporter.config_key, default=True):
+        if not config.SHOW_CRASH_REPORTER:
             EarlyExceptionsQueue.set_hook_as_ready()  # flush already queued exceptions
             return
         if not cls._INSTANCE:
diff --git a/electrum/gui/qt/fee_slider.py b/electrum/gui/qt/fee_slider.py
index 05f681426..34bb0cca7 100644
--- a/electrum/gui/qt/fee_slider.py
+++ b/electrum/gui/qt/fee_slider.py
@@ -23,8 +23,8 @@ def __init__(self, fee_slider):
         )
 
     def on_fee_type(self, x):
-        self.config.set_key('mempool_fees', x==2)
-        self.config.set_key('dynamic_fees', x>0)
+        self.config.FEE_EST_USE_MEMPOOL = (x == 2)
+        self.config.FEE_EST_DYNAMIC = (x > 0)
         self.fee_slider.update()
 
 
diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
index 83c64be51..ce4951fef 100644
--- a/electrum/gui/qt/history_list.py
+++ b/electrum/gui/qt/history_list.py
@@ -47,6 +47,7 @@
                            OrderedDictWithIndex, timestamp_to_datetime,
                            Satoshis, Fiat, format_time)
 from electrum.logging import get_logger, Logger
+from electrum.simple_config import SimpleConfig
 
 from .custom_model import CustomNode, CustomModel
 from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
@@ -252,7 +253,7 @@ def should_include_lightning_payments(self) -> bool:
         return True
 
     def should_show_fiat(self):
-        if not bool(self.window.config.get('history_rates', False)):
+        if not self.window.config.FX_HISTORY_RATES:
             return False
         fx = self.window.fx
         if not fx or not fx.is_enabled():
@@ -260,7 +261,7 @@ def should_show_fiat(self):
         return fx.has_history()
 
     def should_show_capital_gains(self):
-        return self.should_show_fiat() and self.window.config.get('history_rates_capital_gains', False)
+        return self.should_show_fiat() and self.window.config.FX_HISTORY_RATES_CAPITAL_GAINS
 
     @profiler
     def refresh(self, reason: str):
@@ -518,6 +519,8 @@ def __init__(self, main_window: 'ElectrumWindow', model: HistoryModel):
         for col in HistoryColumns:
             sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
             self.header().setSectionResizeMode(col, sm)
+        if self.config:
+            self.configvar_show_toolbar = self.config.cv.GUI_QT_HISTORY_TAB_SHOW_TOOLBAR
 
     def update(self):
         self.hm.refresh('HistoryList.update()')
@@ -546,13 +549,12 @@ def on_combo(self, x):
             self.end_button.setText(_('To') + ' ' + self.format_date(self.end_date))
         self.hide_rows()
 
-    CONFIG_KEY_SHOW_TOOLBAR = "show_toolbar_history"
     def create_toolbar(self, config):
         toolbar, menu = self.create_toolbar_with_menu('')
         self.num_tx_label = toolbar.itemAt(0).widget()
         self._toolbar_checkbox = menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar())
-        self.menu_fiat = menu.addConfig(_('Show Fiat Values'), 'history_rates', False, callback=self.main_window.app.update_fiat_signal.emit)
-        self.menu_capgains = menu.addConfig(_('Show Capital Gains'), 'history_rates_capital_gains', False, callback=self.main_window.app.update_fiat_signal.emit)
+        self.menu_fiat = menu.addConfig(_('Show Fiat Values'), config.cv.FX_HISTORY_RATES, callback=self.main_window.app.update_fiat_signal.emit)
+        self.menu_capgains = menu.addConfig(_('Show Capital Gains'), config.cv.FX_HISTORY_RATES_CAPITAL_GAINS, callback=self.main_window.app.update_fiat_signal.emit)
         self.menu_summary = menu.addAction(_("&Summary"), self.show_summary)
         menu.addAction(_("&Plot"), self.plot_history_dialog)
         menu.addAction(_("&Export"), self.export_history_dialog)
diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
index 2ad1e73a1..0fb20d705 100644
--- a/electrum/gui/qt/installwizard.py
+++ b/electrum/gui/qt/installwizard.py
@@ -746,10 +746,10 @@ def init_network(self, network: 'Network'):
             nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
             if self.exec_layout(nlayout.layout()):
                 nlayout.accept()
-                self.config.set_key('auto_connect', network.auto_connect, save=True)
+                self.config.NETWORK_AUTO_CONNECT = network.auto_connect
         else:
             network.auto_connect = True
-            self.config.set_key('auto_connect', True, save=True)
+            self.config.NETWORK_AUTO_CONNECT = True
 
     @wizard_dialog
     def multisig_dialog(self, run_next):
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index 6c568fe09..08eb6490f 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -242,7 +242,7 @@ def add_optional_tab(tabs, tab, icon, description, name):
 
         self.setMinimumWidth(640)
         self.setMinimumHeight(400)
-        if self.config.get("is_maximized"):
+        if self.config.GUI_QT_WINDOW_IS_MAXIMIZED:
             self.showMaximized()
 
         self.setWindowIcon(read_QIcon("electrum.png"))
@@ -280,14 +280,14 @@ def add_optional_tab(tabs, tab, icon, description, name):
         self.contacts.fetch_openalias(self.config)
 
         # If the option hasn't been set yet
-        if config.get('check_updates') is None:
+        if not config.cv.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS.is_set():
             choice = self.question(title="Electrum - " + _("Enable update check"),
                                    msg=_("For security reasons we advise that you always use the latest version of Electrum.") + " " +
                                        _("Would you like to be notified when there is a newer version of Electrum available?"))
-            config.set_key('check_updates', bool(choice), save=True)
+            config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = bool(choice)
 
         self._update_check_thread = None
-        if config.get('check_updates', False):
+        if config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS:
             # The references to both the thread and the window need to be stored somewhere
             # to prevent GC from getting in our way.
             def on_version_received(v):
@@ -497,7 +497,7 @@ def load_wallet(self, wallet: Abstract_Wallet):
         self.channels_list.update()
         self.tabs.show()
         self.init_geometry()
-        if self.config.get('hide_gui') and self.gui_object.tray.isVisible():
+        if self.config.GUI_QT_HIDE_ON_STARTUP and self.gui_object.tray.isVisible():
             self.hide()
         else:
             self.show()
@@ -552,7 +552,7 @@ def warn_if_testnet(self):
         if not constants.net.TESTNET:
             return
         # user might have opted out already
-        if self.config.get('dont_show_testnet_warning', False):
+        if self.config.DONT_SHOW_TESTNET_WARNING:
             return
         # only show once per process lifecycle
         if getattr(self.gui_object, '_warned_testnet', False):
@@ -571,7 +571,7 @@ def on_cb(x):
         cb.stateChanged.connect(on_cb)
         self.show_warning(msg, title=_('Testnet'), checkbox=cb)
         if cb_checked:
-            self.config.set_key('dont_show_testnet_warning', True)
+            self.config.DONT_SHOW_TESTNET_WARNING = True
 
     def open_wallet(self):
         try:
@@ -585,10 +585,10 @@ def open_wallet(self):
         self.gui_object.new_window(filename)
 
     def select_backup_dir(self, b):
-        name = self.config.get('backup_dir', '')
+        name = self.config.WALLET_BACKUP_DIRECTORY or ""
         dirname = QFileDialog.getExistingDirectory(self, "Select your wallet backup directory", name)
         if dirname:
-            self.config.set_key('backup_dir', dirname)
+            self.config.WALLET_BACKUP_DIRECTORY = dirname
             self.backup_dir_e.setText(dirname)
 
     def backup_wallet(self):
@@ -596,7 +596,7 @@ def backup_wallet(self):
         vbox = QVBoxLayout(d)
         grid = QGridLayout()
         backup_help = ""
-        backup_dir = self.config.get('backup_dir')
+        backup_dir = self.config.WALLET_BACKUP_DIRECTORY
         backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)
         msg = _('Please select a backup directory')
         if self.wallet.has_lightning() and self.wallet.lnworker.channels:
@@ -628,7 +628,7 @@ def backup_wallet(self):
         return True
 
     def update_recently_visited(self, filename):
-        recent = self.config.get('recently_open', [])
+        recent = self.config.RECENTLY_OPEN_WALLET_FILES or []
         try:
             sorted(recent)
         except Exception:
@@ -638,7 +638,7 @@ def update_recently_visited(self, filename):
         recent.insert(0, filename)
         recent = [path for path in recent if os.path.exists(path)]
         recent = recent[:5]
-        self.config.set_key('recently_open', recent)
+        self.config.RECENTLY_OPEN_WALLET_FILES = recent
         self.recently_visited_menu.clear()
         for i, k in enumerate(sorted(recent)):
             b = os.path.basename(k)
@@ -2557,7 +2557,7 @@ def clean_up(self):
         for fut in coro_keys:
             fut.cancel()
         self.unregister_callbacks()
-        self.config.set_key("is_maximized", self.isMaximized())
+        self.config.GUI_QT_WINDOW_IS_MAXIMIZED = self.isMaximized()
         if not self.isMaximized():
             g = self.geometry()
             self.wallet.db.put("winpos-qt", [g.left(),g.top(),
diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py
index c31c2ee15..da79f9554 100644
--- a/electrum/gui/qt/my_treeview.py
+++ b/electrum/gui/qt/my_treeview.py
@@ -59,6 +59,7 @@
 from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED
 from electrum.logging import Logger
 from electrum.qrreader import MissingQrDetectionLib
+from electrum.simple_config import ConfigVarWithConfig
 
 from .util import read_QIcon
 
@@ -80,17 +81,18 @@ def addToggle(self, text: str, callback, *, tooltip='') -> QAction:
         m.setToolTip(tooltip)
         return m
 
-    def addConfig(self, text: str, name: str, default: bool, *, tooltip='', callback=None) -> QAction:
-        b = self.config.get(name, default)
-        m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback))
+    def addConfig(self, text: str, configvar: 'ConfigVarWithConfig', *, tooltip='', callback=None) -> QAction:
+        assert isinstance(configvar, ConfigVarWithConfig), configvar
+        b = configvar.get()
+        m = self.addAction(text, lambda: self._do_toggle_config(configvar, callback=callback))
         m.setCheckable(True)
         m.setChecked(bool(b))
         m.setToolTip(tooltip)
         return m
 
-    def _do_toggle_config(self, name, default, callback):
-        b = self.config.get(name, default)
-        self.config.set_key(name, not b)
+    def _do_toggle_config(self, configvar: 'ConfigVarWithConfig', *, callback):
+        b = configvar.get()
+        configvar.set(not b)
         if callback:
             callback()
 
@@ -387,7 +389,7 @@ def hide_rows(self):
         for row in range(self.model().rowCount()):
             self.hide_row(row)
 
-    def create_toolbar(self, config):
+    def create_toolbar(self, config: 'SimpleConfig'):
         return
 
     def create_toolbar_buttons(self):
@@ -402,13 +404,13 @@ def create_toolbar_buttons(self):
     def create_toolbar_with_menu(self, title):
         return create_toolbar_with_menu(self.config, title)
 
-    CONFIG_KEY_SHOW_TOOLBAR = None  # type: Optional[str]
+    configvar_show_toolbar = None  # type: Optional[ConfigVarWithConfig]
     _toolbar_checkbox = None  # type: Optional[QAction]
     def show_toolbar(self, state: bool = None):
         if state is None:  # get value from config
-            if self.config and self.CONFIG_KEY_SHOW_TOOLBAR:
-                state = self.config.get(self.CONFIG_KEY_SHOW_TOOLBAR, None)
-            if state is None:
+            if self.configvar_show_toolbar:
+                state = self.configvar_show_toolbar.get()
+            else:
                 return
         assert isinstance(state, bool), state
         if state == self.toolbar_shown:
@@ -428,8 +430,8 @@ def on_hide_toolbar(self):
     def toggle_toolbar(self):
         new_state = not self.toolbar_shown
         self.show_toolbar(new_state)
-        if self.config and self.CONFIG_KEY_SHOW_TOOLBAR:
-            self.config.set_key(self.CONFIG_KEY_SHOW_TOOLBAR, new_state)
+        if self.configvar_show_toolbar:
+            self.configvar_show_toolbar.set(new_state)
 
     def add_copy_menu(self, menu: QMenu, idx) -> QMenu:
         cc = menu.addMenu(_("Copy"))
diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py
index 7a08bd3fa..fb66903c0 100644
--- a/electrum/gui/qt/network_dialog.py
+++ b/electrum/gui/qt/network_dialog.py
@@ -41,14 +41,12 @@
 from electrum.network import Network
 from electrum.logging import get_logger
 from electrum.util import detect_tor_socks_proxy
+from electrum.simple_config import SimpleConfig
 
 from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
                    PasswordLineEdit)
 from .util import QtEventListener, qt_event_listener
 
-if TYPE_CHECKING:
-    from electrum.simple_config import SimpleConfig
-
 
 _logger = get_logger(__name__)
 
@@ -279,7 +277,7 @@ def __init__(self, network: Network, config: 'SimpleConfig', wizard=False):
         grid.addWidget(HelpButton(msg), 0, 4)
 
         self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
+        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
         self.autoconnect_cb.clicked.connect(self.set_server)
         self.autoconnect_cb.clicked.connect(self.update)
         msg = ' '.join([
@@ -327,13 +325,13 @@ def clean_up(self):
             self.td = None
 
     def check_disable_proxy(self, b):
-        if not self.config.is_modifiable('proxy'):
+        if not self.config.cv.NETWORK_PROXY.is_modifiable():
             b = False
         for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
             w.setEnabled(b)
 
     def enable_set_server(self):
-        if self.config.is_modifiable('server'):
+        if self.config.cv.NETWORK_SERVER.is_modifiable():
             enabled = not self.autoconnect_cb.isChecked()
             self.server_e.setEnabled(enabled)
         else:
diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py
index 36b589026..e71f21fa9 100644
--- a/electrum/gui/qt/new_channel_dialog.py
+++ b/electrum/gui/qt/new_channel_dialog.py
@@ -37,7 +37,7 @@ def __init__(self, window: 'ElectrumWindow', amount_sat: Optional[int] = None, m
         toolbar, menu = create_toolbar_with_menu(self.config, '')
         recov_tooltip = messages.to_rtf(messages.MSG_RECOVERABLE_CHANNELS)
         menu.addConfig(
-            _("Create recoverable channels"), 'use_recoverable_channels', True,
+            _("Create recoverable channels"), self.config.cv.LIGHTNING_USE_RECOVERABLE_CHANNELS,
             tooltip=recov_tooltip,
         ).setEnabled(self.lnworker.can_have_recoverable_channels())
         vbox.addLayout(toolbar)
diff --git a/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
index 9b25e1e76..856cb8d44 100644
--- a/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
+++ b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
@@ -130,7 +130,7 @@ def __init__(self, parent: Optional[QWidget], *, config: SimpleConfig):
         # Flip horizontally checkbox with default coming from global config
         self.flip_x = QCheckBox()
         self.flip_x.setText(_("&Flip horizontally"))
-        self.flip_x.setChecked(bool(self.config.get('qrreader_flip_x', True)))
+        self.flip_x.setChecked(self.config.QR_READER_FLIP_X)
         self.flip_x.stateChanged.connect(self._on_flip_x_changed)
         controls_layout.addWidget(self.flip_x)
 
@@ -155,7 +155,7 @@ def __init__(self, parent: Optional[QWidget], *, config: SimpleConfig):
         self.finished.connect(self._on_finished, Qt.QueuedConnection)
 
     def _on_flip_x_changed(self, _state: int):
-        self.config.set_key('qrreader_flip_x', self.flip_x.isChecked())
+        self.config.QR_READER_FLIP_X = self.flip_x.isChecked()
 
     def _get_resolution(self, resolutions: List[QSize], min_size: int) -> QSize:
         """
diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py
index e5a4ed4c0..3b279ee31 100644
--- a/electrum/gui/qt/receive_tab.py
+++ b/electrum/gui/qt/receive_tab.py
@@ -147,10 +147,10 @@ def on_receive_swap():
         self.toolbar.insertWidget(2, self.toggle_view_button)
         # menu
         menu.addConfig(
-            _('Add on-chain fallback to lightning requests'), 'bolt11_fallback', True,
+            _('Add on-chain fallback to lightning requests'), self.config.cv.WALLET_BOLT11_FALLBACK,
             callback=self.on_toggle_bolt11_fallback)
         menu.addConfig(
-            _('Add lightning requests to bitcoin URIs'), 'bip21_lightning', False,
+            _('Add lightning requests to bitcoin URIs'), self.config.cv.WALLET_BIP21_LIGHTNING,
             tooltip=_('This may result in large QR codes'),
             callback=self.update_current_request)
         self.qr_menu_action = menu.addToggle(_("Show detached QR code window"), self.window.toggle_qr_window)
@@ -181,7 +181,7 @@ def on_receive_swap():
         self.update_expiry_text()
 
     def update_expiry_text(self):
-        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS
         text = pr_expiration_values[expiry]
         self.expiry_button.setText(text)
 
@@ -196,11 +196,11 @@ def expiry_dialog(self):
             '\n\n',
             _('For Lightning requests, payments will not be accepted after the expiration.'),
         ])
-        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS
         v = self.window.query_choice(msg, pr_expiration_values, title=_('Expiry'), default_choice=expiry)
         if v is None:
             return
-        self.config.set_key('request_expiry', v)
+        self.config.WALLET_PAYREQ_EXPIRY_SECONDS = v
         self.update_expiry_text()
 
     def on_toggle_bolt11_fallback(self):
@@ -210,7 +210,7 @@ def on_toggle_bolt11_fallback(self):
         self.update_current_request()
 
     def update_view_button(self):
-        i = self.config.get('receive_tabs_index', 0)
+        i = self.config.GUI_QT_RECEIVE_TABS_INDEX
         if i == 0:
             icon, text = read_QIcon("link.png"), _('Bitcoin URI')
         elif i == 1:
@@ -221,9 +221,9 @@ def update_view_button(self):
         self.toggle_view_button.setIcon(icon)
 
     def toggle_view(self):
-        i = self.config.get('receive_tabs_index', 0)
+        i = self.config.GUI_QT_RECEIVE_TABS_INDEX
         i = (i + 1) % (3 if self.wallet.has_lightning() else 2)
-        self.config.set_key('receive_tabs_index', i)
+        self.config.GUI_QT_RECEIVE_TABS_INDEX = i
         self.update_current_request()
         self.update_view_button()
 
@@ -239,12 +239,12 @@ def do_copy(self, e):
         self.window.do_copy(data, title=title)
 
     def toggle_receive_qr(self):
-        b = not self.config.get('receive_qr_visible', False)
-        self.config.set_key('receive_qr_visible', b)
+        b = not self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
+        self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE = b
         self.update_receive_widgets()
 
     def update_receive_widgets(self):
-        b = self.config.get('receive_qr_visible', False)
+        b = self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
         self.receive_widget.update_visibility(b)
 
     def update_current_request(self):
@@ -286,7 +286,7 @@ def update_current_request(self):
         self.update_receive_qr_window()
 
     def get_tab_data(self):
-        i = self.config.get('receive_tabs_index', 0)
+        i = self.config.GUI_QT_RECEIVE_TABS_INDEX
         if i == 0:
             out = self.URI, self.URI, self.URI_help, _('Bitcoin URI')
         elif i == 1:
@@ -305,7 +305,7 @@ def update_receive_qr_window(self):
     def create_invoice(self):
         amount_sat = self.receive_amount_e.get_amount()
         message = self.receive_message_e.text()
-        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS
 
         if amount_sat and amount_sat < self.wallet.dust_threshold():
             address = None
diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py
index 2a25e876d..99b491ba4 100644
--- a/electrum/gui/qt/settings_dialog.py
+++ b/electrum/gui/qt/settings_dialog.py
@@ -72,18 +72,18 @@ def __init__(self, window: 'ElectrumWindow', config: 'SimpleConfig'):
         lang_combo = QComboBox()
         lang_combo.addItems(list(languages.values()))
         lang_keys = list(languages.keys())
-        lang_cur_setting = self.config.get("language", '')
+        lang_cur_setting = self.config.LOCALIZATION_LANGUAGE
         try:
             index = lang_keys.index(lang_cur_setting)
         except ValueError:  # not in list
             index = 0
         lang_combo.setCurrentIndex(index)
-        if not self.config.is_modifiable('language'):
+        if not self.config.cv.LOCALIZATION_LANGUAGE.is_modifiable():
             for w in [lang_combo, lang_label]: w.setEnabled(False)
         def on_lang(x):
             lang_request = list(languages.keys())[lang_combo.currentIndex()]
-            if lang_request != self.config.get('language'):
-                self.config.set_key("language", lang_request, save=True)
+            if lang_request != self.config.LOCALIZATION_LANGUAGE:
+                self.config.LOCALIZATION_LANGUAGE = lang_request
                 self.need_restart = True
         lang_combo.currentIndexChanged.connect(on_lang)
 
@@ -93,13 +93,13 @@ def on_lang(x):
         nz.setMinimum(0)
         nz.setMaximum(self.config.decimal_point)
         nz.setValue(self.config.num_zeros)
-        if not self.config.is_modifiable('num_zeros'):
+        if not self.config.cv.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT.is_modifiable():
             for w in [nz, nz_label]: w.setEnabled(False)
         def on_nz():
             value = nz.value()
             if self.config.num_zeros != value:
                 self.config.num_zeros = value
-                self.config.set_key('num_zeros', value, save=True)
+                self.config.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = value
                 self.app.refresh_tabs_signal.emit()
                 self.app.update_status_signal.emit()
         nz.valueChanged.connect(on_nz)
@@ -108,7 +108,7 @@ def on_nz():
         help_trampoline = messages.MSG_HELP_TRAMPOLINE
         trampoline_cb = QCheckBox(_("Use trampoline routing"))
         trampoline_cb.setToolTip(messages.to_rtf(help_trampoline))
-        trampoline_cb.setChecked(not bool(self.config.get('use_gossip', False)))
+        trampoline_cb.setChecked(not self.config.LIGHTNING_USE_GOSSIP)
         def on_trampoline_checked(use_trampoline):
             use_trampoline = bool(use_trampoline)
             if not use_trampoline:
@@ -119,7 +119,7 @@ def on_trampoline_checked(use_trampoline):
                 ])):
                     trampoline_cb.setCheckState(Qt.Checked)
                     return
-            self.config.set_key('use_gossip', not use_trampoline)
+            self.config.LIGHTNING_USE_GOSSIP = not use_trampoline
             if not use_trampoline:
                 self.network.start_gossip()
             else:
@@ -137,17 +137,17 @@ def on_trampoline_checked(use_trampoline):
         ])
         remote_wt_cb = QCheckBox(_("Use a remote watchtower"))
         remote_wt_cb.setToolTip('

'+help_remote_wt+'

') - remote_wt_cb.setChecked(bool(self.config.get('use_watchtower', False))) + remote_wt_cb.setChecked(self.config.WATCHTOWER_CLIENT_ENABLED) def on_remote_wt_checked(x): - self.config.set_key('use_watchtower', bool(x)) + self.config.WATCHTOWER_CLIENT_ENABLED = bool(x) self.watchtower_url_e.setEnabled(bool(x)) remote_wt_cb.stateChanged.connect(on_remote_wt_checked) - watchtower_url = self.config.get('watchtower_url') + watchtower_url = self.config.WATCHTOWER_CLIENT_URL self.watchtower_url_e = QLineEdit(watchtower_url) - self.watchtower_url_e.setEnabled(self.config.get('use_watchtower', False)) + self.watchtower_url_e.setEnabled(self.config.WATCHTOWER_CLIENT_ENABLED) def on_wt_url(): url = self.watchtower_url_e.text() or None - watchtower_url = self.config.set_key('watchtower_url', url) + self.config.WATCHTOWER_CLIENT_URL = url self.watchtower_url_e.editingFinished.connect(on_wt_url) msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\ @@ -155,18 +155,18 @@ def on_wt_url(): + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ + 'For more information, see https://openalias.org' alias_label = HelpLabel(_('OpenAlias') + ':', msg) - alias = self.config.get('alias','') + alias = self.config.OPENALIAS_ID self.alias_e = QLineEdit(alias) self.set_alias_color() self.alias_e.editingFinished.connect(self.on_alias_edit) msat_cb = QCheckBox(_("Show Lightning amounts with msat precision")) - msat_cb.setChecked(bool(self.config.get('amt_precision_post_satoshi', False))) + msat_cb.setChecked(self.config.BTC_AMOUNTS_PREC_POST_SAT > 0) def on_msat_checked(v): prec = 3 if v == Qt.Checked else 0 if self.config.amt_precision_post_satoshi != prec: self.config.amt_precision_post_satoshi = prec - self.config.set_key('amt_precision_post_satoshi', prec) + self.config.BTC_AMOUNTS_PREC_POST_SAT = prec self.app.refresh_tabs_signal.emit() msat_cb.stateChanged.connect(on_msat_checked) @@ -191,12 +191,12 @@ def on_unit(x, nz): unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz)) thousandsep_cb = QCheckBox(_("Add thousand separators to bitcoin amounts")) - thousandsep_cb.setChecked(bool(self.config.get('amt_add_thousands_sep', False))) + thousandsep_cb.setChecked(self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP) def on_set_thousandsep(v): checked = v == Qt.Checked if self.config.amt_add_thousands_sep != checked: self.config.amt_add_thousands_sep = checked - self.config.set_key('amt_add_thousands_sep', checked) + self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked self.app.refresh_tabs_signal.emit() thousandsep_cb.stateChanged.connect(on_set_thousandsep) @@ -209,32 +209,33 @@ def on_set_thousandsep(v): system_cameras = find_system_cameras() for cam_desc, cam_path in system_cameras.items(): qr_combo.addItem(cam_desc, cam_path) - index = qr_combo.findData(self.config.get("video_device")) + index = qr_combo.findData(self.config.VIDEO_DEVICE_PATH) qr_combo.setCurrentIndex(index) - on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), save=True) + def on_video_device(x): + self.config.VIDEO_DEVICE_PATH = qr_combo.itemData(x) qr_combo.currentIndexChanged.connect(on_video_device) colortheme_combo = QComboBox() colortheme_combo.addItem(_('Light'), 'default') colortheme_combo.addItem(_('Dark'), 'dark') - index = colortheme_combo.findData(self.config.get('qt_gui_color_theme', 'default')) + index = colortheme_combo.findData(self.config.GUI_QT_COLOR_THEME) colortheme_combo.setCurrentIndex(index) colortheme_label = QLabel(_('Color theme') + ':') def on_colortheme(x): - self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), save=True) + self.config.GUI_QT_COLOR_THEME = colortheme_combo.itemData(x) self.need_restart = True colortheme_combo.currentIndexChanged.connect(on_colortheme) updatecheck_cb = QCheckBox(_("Automatically check for software updates")) - updatecheck_cb.setChecked(bool(self.config.get('check_updates', False))) + updatecheck_cb.setChecked(self.config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS) def on_set_updatecheck(v): - self.config.set_key('check_updates', v == Qt.Checked, save=True) + self.config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = (v == Qt.Checked) updatecheck_cb.stateChanged.connect(on_set_updatecheck) filelogging_cb = QCheckBox(_("Write logs to file")) - filelogging_cb.setChecked(bool(self.config.get('log_to_file', False))) + filelogging_cb.setChecked(self.config.WRITE_LOGS_TO_DISK) def on_set_filelogging(v): - self.config.set_key('log_to_file', v == Qt.Checked, save=True) + self.config.WRITE_LOGS_TO_DISK = (v == Qt.Checked) self.need_restart = True filelogging_cb.stateChanged.connect(on_set_filelogging) filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.')) @@ -256,7 +257,7 @@ def fmt_docs(key, klass): chooser_combo.setCurrentIndex(i) def on_chooser(x): chooser_name = choosers[chooser_combo.currentIndex()] - self.config.set_key('coin_chooser', chooser_name) + self.config.WALLET_COIN_CHOOSER_POLICY = chooser_name chooser_combo.currentIndexChanged.connect(on_chooser) block_explorers = sorted(util.block_explorer_info().keys()) @@ -267,7 +268,7 @@ def on_chooser(x): msg = _('Choose which online block explorer to use for functions that open a web browser') block_ex_label = HelpLabel(_('Online Block Explorer') + ':', msg) block_ex_combo = QComboBox() - block_ex_custom_e = QLineEdit(str(self.config.get('block_explorer_custom') or '')) + block_ex_custom_e = QLineEdit(str(self.config.BLOCK_EXPLORER_CUSTOM or '')) block_ex_combo.addItems(block_explorers) block_ex_combo.setCurrentIndex( block_ex_combo.findText(util.block_explorer(self.config) or BLOCK_EX_CUSTOM_ITEM)) @@ -279,8 +280,8 @@ def on_be_combo(x): on_be_edit() else: be_result = block_explorers[block_ex_combo.currentIndex()] - self.config.set_key('block_explorer_custom', None, save=False) - self.config.set_key('block_explorer', be_result, save=True) + self.config.BLOCK_EXPLORER_CUSTOM = None + self.config.BLOCK_EXPLORER = be_result showhide_block_ex_custom_e() block_ex_combo.currentIndexChanged.connect(on_be_combo) def on_be_edit(): @@ -289,7 +290,7 @@ def on_be_edit(): val = ast.literal_eval(val) # to also accept tuples except Exception: pass - self.config.set_key('block_explorer_custom', val) + self.config.BLOCK_EXPLORER_CUSTOM = val block_ex_custom_e.editingFinished.connect(on_be_edit) block_ex_hbox = QHBoxLayout() block_ex_hbox.setContentsMargins(0, 0, 0, 0) @@ -307,7 +308,7 @@ def on_be_edit(): def update_currencies(): if not self.fx: return - h = bool(self.config.get('history_rates', False)) + h = self.config.FX_HISTORY_RATES currencies = sorted(self.fx.get_currencies(h)) ccy_combo.clear() ccy_combo.addItems([_('None')] + currencies) @@ -319,7 +320,7 @@ def update_exchanges(): b = self.fx.is_enabled() ex_combo.setEnabled(b) if b: - h = bool(self.config.get('history_rates', False)) + h = self.config.FX_HISTORY_RATES c = self.fx.get_currency() exchanges = self.fx.get_exchanges_by_ccy(c, h) else: @@ -347,7 +348,7 @@ def on_exchange(idx): self.app.update_fiat_signal.emit() def on_history_rates(checked): - self.config.set_key('history_rates', bool(checked)) + self.config.FX_HISTORY_RATES = bool(checked) if not self.fx: return update_exchanges() @@ -356,7 +357,7 @@ def on_history_rates(checked): update_currencies() update_exchanges() ccy_combo.currentIndexChanged.connect(on_currency) - self.history_rates_cb.setChecked(bool(self.config.get('history_rates', False))) + self.history_rates_cb.setChecked(self.config.FX_HISTORY_RATES) self.history_rates_cb.stateChanged.connect(on_history_rates) ex_combo.currentIndexChanged.connect(on_exchange) @@ -417,7 +418,7 @@ def on_event_alias_received(self): self.app.alias_received_signal.emit() def set_alias_color(self): - if not self.config.get('alias'): + if not self.config.OPENALIAS_ID: self.alias_e.setStyleSheet("") return if self.wallet.contacts.alias_info: @@ -429,7 +430,7 @@ def set_alias_color(self): def on_alias_edit(self): self.alias_e.setStyleSheet("") alias = str(self.alias_e.text()) - self.config.set_key('alias', alias, save=True) + self.config.OPENALIAS_ID = alias if alias: self.wallet.contacts.fetch_openalias(self.config) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 1f47dc972..0c52442ff 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -45,7 +45,7 @@ def __init__(self, window: 'ElectrumWindow', is_reverse=None, recv_amount_sat=No vbox = QVBoxLayout(self) toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( - _("Allow instant swaps"), 'allow_instant_swaps', False, + _("Allow instant swaps"), self.config.cv.LIGHTNING_ALLOW_INSTANT_SWAPS, tooltip=messages.to_rtf(messages.MSG_CONFIG_INSTANT_SWAPS), ).setEnabled(self.lnworker.can_have_recoverable_channels()) vbox.addLayout(toolbar) @@ -138,11 +138,11 @@ def init_recv_amount(self, recv_amount_sat): def fee_slider_callback(self, dyn, pos, fee_rate): if dyn: if self.config.use_mempool_fees(): - self.config.set_key('depth_level', pos, save=False) + self.config.cv.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS.set(pos, save=False) else: - self.config.set_key('fee_level', pos, save=False) + self.config.cv.FEE_EST_DYNAMIC_ETA_SLIDERPOS.set(pos, save=False) else: - self.config.set_key('fee_per_kb', fee_rate, save=False) + self.config.cv.FEE_EST_STATIC_FEERATE_FALLBACK.set(fee_rate, save=False) if self.send_follows: self.on_recv_edited() else: diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index c29de2cbb..b9e5bda8e 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -416,7 +416,7 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav self.setLayout(vbox) toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( - _('Download missing data'), 'tx_dialog_fetch_txin_data', False, + _('Download missing data'), self.config.cv.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA, tooltip=_( 'Download parent transactions from the network.\n' 'Allows filling in missing fee and input details.'), @@ -945,7 +945,7 @@ def maybe_fetch_txin_data(self): We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp), but this is not done currently. """ - if not self.config.get('tx_dialog_fetch_txin_data', False): + if not self.config.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA: return tx = self.tx if not tx: diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index bfae00a88..a393c2bdb 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -483,7 +483,7 @@ def filename_field(parent, config, defaultname, select_msg): hbox = QHBoxLayout() - directory = config.get('io_dir', os.path.expanduser('~')) + directory = config.IO_DIRECTORY path = os.path.join(directory, defaultname) filename_e = QLineEdit() filename_e.setText(path) @@ -1048,10 +1048,10 @@ def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter): def getOpenFileName(*, parent, title, filter="", config: 'SimpleConfig') -> Optional[str]: """Custom wrapper for getOpenFileName that remembers the path selected by the user.""" - directory = config.get('io_dir', os.path.expanduser('~')) + directory = config.IO_DIRECTORY fileName, __ = QFileDialog.getOpenFileName(parent, title, directory, filter) if fileName and directory != os.path.dirname(fileName): - config.set_key('io_dir', os.path.dirname(fileName), save=True) + config.IO_DIRECTORY = os.path.dirname(fileName) return fileName @@ -1066,7 +1066,7 @@ def getSaveFileName( config: 'SimpleConfig', ) -> Optional[str]: """Custom wrapper for getSaveFileName that remembers the path selected by the user.""" - directory = config.get('io_dir', os.path.expanduser('~')) + directory = config.IO_DIRECTORY path = os.path.join(directory, filename) file_dialog = QFileDialog(parent, title, path, filter) @@ -1082,7 +1082,7 @@ def getSaveFileName( selected_path = file_dialog.selectedFiles()[0] if selected_path and directory != os.path.dirname(selected_path): - config.set_key('io_dir', os.path.dirname(selected_path), save=True) + config.IO_DIRECTORY = os.path.dirname(selected_path) return selected_path diff --git a/electrum/gui/text.py b/electrum/gui/text.py index b1d3eae75..5fc954c2e 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -558,7 +558,7 @@ def do_create_request(self): if not address: return message = self.str_recv_description - expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS key = self.wallet.create_request(amount_sat, message, expiry, address) self.do_clear_request() self.pos = self.max_pos @@ -719,7 +719,7 @@ def network_dialog(self): srv = 'auto-connect' if auto_connect else str(self.network.default_server) out = self.run_dialog('Network', [ {'label':'server', 'type':'str', 'value':srv}, - {'label':'proxy', 'type':'str', 'value':self.config.get('proxy', '')}, + {'label':'proxy', 'type':'str', 'value':self.config.NETWORK_PROXY}, ], buttons = 1) if out: if out.get('server'): @@ -747,7 +747,7 @@ def settings_dialog(self): if out: if out.get('Default fee'): fee = int(Decimal(out['Default fee']) * COIN) - self.config.set_key('fee_per_kb', fee, save=True) + self.config.FEE_EST_STATIC_FEERATE_FALLBACK = fee def password_dialog(self): out = self.run_dialog('Password', [ diff --git a/electrum/interface.py b/electrum/interface.py index 782c5af41..f1d6f4d9f 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -68,8 +68,6 @@ BUCKET_NAME_OF_ONION_SERVERS = 'onion' -MAX_INCOMING_MSG_SIZE = 1_000_000 # in bytes - _KNOWN_NETWORK_PROTOCOLS = {'t', 's'} PREFERRED_NETWORK_PROTOCOL = 's' assert PREFERRED_NETWORK_PROTOCOL in _KNOWN_NETWORK_PROTOCOLS @@ -216,8 +214,8 @@ def maybe_log(self, msg: str) -> None: def default_framer(self): # overridden so that max_size can be customized - max_size = int(self.interface.network.config.get('network_max_incoming_msg_size', - MAX_INCOMING_MSG_SIZE)) + max_size = self.interface.network.config.NETWORK_MAX_INCOMING_MSG_SIZE + assert max_size > 500_000, f"{max_size=} (< 500_000) is too small" return NewlineFramer(max_size=max_size) async def close(self, *, force_after: int = None): @@ -604,7 +602,7 @@ async def _fetch_certificate(self) -> bytes: def _get_expected_fingerprint(self) -> Optional[str]: if self.is_main_server(): - return self.network.config.get("serverfingerprint") + return self.network.config.NETWORK_SERVERFINGERPRINT def _verify_certificate_fingerprint(self, certificate): expected_fingerprint = self._get_expected_fingerprint() diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 7d034a06d..be53811f7 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -647,7 +647,7 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn channel_seed=channel_seed, static_remotekey=static_remotekey, upfront_shutdown_script=upfront_shutdown_script, - to_self_delay=self.network.config.get('lightning_to_self_delay', 7 * 144), + to_self_delay=self.network.config.LIGHTNING_TO_SELF_DELAY_CSV, dust_limit_sat=dust_limit_sat, max_htlc_value_in_flight_msat=funding_sat * 1000, max_accepted_htlcs=30, @@ -1389,7 +1389,7 @@ def mark_open(self, chan: Channel): if pending_channel_update: chan.set_remote_update(pending_channel_update) self.logger.info(f"CHANNEL OPENING COMPLETED ({chan.get_id_for_log()})") - forwarding_enabled = self.network.config.get('lightning_forward_payments', False) + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS if forwarding_enabled: # send channel_update of outgoing edge to peer, # so that channel can be used to to receive payments @@ -1578,7 +1578,7 @@ def maybe_forward_htlc( # (same for trampoline forwarding) # - we could check for the exposure to dust HTLCs, see: # https://github.com/ACINQ/eclair/pull/1985 - forwarding_enabled = self.network.config.get('lightning_forward_payments', False) + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS if not forwarding_enabled: self.logger.info(f"forwarding is disabled. failing htlc.") raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') @@ -1660,8 +1660,8 @@ def maybe_forward_trampoline( htlc: UpdateAddHtlc, trampoline_onion: ProcessedOnionPacket): - forwarding_enabled = self.network.config.get('lightning_forward_payments', False) - forwarding_trampoline_enabled = self.network.config.get('lightning_forward_trampoline_payments', False) + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS + forwarding_trampoline_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS if not (forwarding_enabled and forwarding_trampoline_enabled): self.logger.info(f"trampoline forwarding is disabled. failing htlc.") raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') @@ -1996,8 +1996,8 @@ def get_shutdown_fee_range(self, chan, closing_tx, is_local): """ return the closing fee and fee range we initially try to enforce """ config = self.network.config our_fee = None - if config.get('test_shutdown_fee'): - our_fee = config.get('test_shutdown_fee') + if config.TEST_SHUTDOWN_FEE: + our_fee = config.TEST_SHUTDOWN_FEE else: fee_rate_per_kb = config.eta_target_to_fee(FEE_LN_ETA_TARGET) if fee_rate_per_kb is None: # fallback @@ -2012,10 +2012,10 @@ def get_shutdown_fee_range(self, chan, closing_tx, is_local): our_fee = max_fee our_fee = min(our_fee, max_fee) # config modern_fee_negotiation can be set in tests - if config.get('test_shutdown_legacy'): + if config.TEST_SHUTDOWN_LEGACY: our_fee_range = None - elif config.get('test_shutdown_fee_range'): - our_fee_range = config.get('test_shutdown_fee_range') + elif config.TEST_SHUTDOWN_FEE_RANGE: + our_fee_range = config.TEST_SHUTDOWN_FEE_RANGE else: # we aim at a fee between next block inclusion and some lower value our_fee_range = {'min_fee_satoshis': our_fee // 2, 'max_fee_satoshis': our_fee * 2} @@ -2101,7 +2101,7 @@ def choose_new_fee(our_fee, our_fee_range, their_fee, their_fee_range, their_pre fee_range_sent = our_fee_range and (is_initiator or (their_previous_fee is not None)) # The sending node, if it is not the funder: - if our_fee_range and their_fee_range and not is_initiator and not self.network.config.get('test_shutdown_fee_range'): + if our_fee_range and their_fee_range and not is_initiator and not self.network.config.TEST_SHUTDOWN_FEE_RANGE: # SHOULD set max_fee_satoshis to at least the max_fee_satoshis received our_fee_range['max_fee_satoshis'] = max(their_fee_range['max_fee_satoshis'], our_fee_range['max_fee_satoshis']) # SHOULD set min_fee_satoshis to a fairly low value @@ -2400,8 +2400,8 @@ def process_onion_packet( except Exception as e: self.logger.info(f"error processing onion packet: {e!r}") raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data) - if self.network.config.get('test_fail_malformed_htlc'): + if self.network.config.TEST_FAIL_HTLCS_AS_MALFORMED: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data) - if self.network.config.get('test_fail_htlcs_with_temp_node_failure'): + if self.network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE: raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') return processed_onion diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 2abe2662c..563b66a0b 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -253,13 +253,13 @@ def get_node_alias(self, node_id: bytes) -> Optional[str]: async def maybe_listen(self): # FIXME: only one LNWorker can listen at a time (single port) - listen_addr = self.config.get('lightning_listen') + listen_addr = self.config.LIGHTNING_LISTEN if listen_addr: self.logger.info(f'lightning_listen enabled. will try to bind: {listen_addr!r}') try: netaddr = NetAddress.from_string(listen_addr) except Exception as e: - self.logger.error(f"failed to parse config key 'lightning_listen'. got: {e!r}") + self.logger.error(f"failed to parse config key '{self.config.cv.LIGHTNING_LISTEN.key()}'. got: {e!r}") return addr = str(netaddr.host) async def cb(reader, writer): @@ -351,7 +351,7 @@ async def stop(self): await self.taskgroup.cancel_remaining() def _add_peers_from_config(self): - peer_list = self.config.get('lightning_peers', []) + peer_list = self.config.LIGHTNING_PEERS or [] for host, port, pubkey in peer_list: asyncio.run_coroutine_threadsafe( self._add_peer(host, int(port), bfh(pubkey)), @@ -675,14 +675,14 @@ def has_deterministic_node_id(self) -> bool: def can_have_recoverable_channels(self) -> bool: return (self.has_deterministic_node_id() - and not (self.config.get('lightning_listen'))) + and not self.config.LIGHTNING_LISTEN) def has_recoverable_channels(self) -> bool: """Whether *future* channels opened by this wallet would be recoverable from seed (via putting OP_RETURN outputs into funding txs). """ return (self.can_have_recoverable_channels() - and self.config.get('use_recoverable_channels', True)) + and self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS) @property def channels(self) -> Mapping[bytes, Channel]: @@ -728,7 +728,7 @@ async def sync_with_remote_watchtower(self): while True: # periodically poll if the user updated 'watchtower_url' await asyncio.sleep(5) - watchtower_url = self.config.get('watchtower_url') + watchtower_url = self.config.WATCHTOWER_CLIENT_URL if not watchtower_url: continue parsed_url = urllib.parse.urlparse(watchtower_url) diff --git a/electrum/logging.py b/electrum/logging.py index 54cf35948..4891d306f 100644 --- a/electrum/logging.py +++ b/electrum/logging.py @@ -318,12 +318,12 @@ def configure_logging(config: 'SimpleConfig', *, log_to_file: Optional[bool] = N verbosity = config.get('verbosity') verbosity_shortcuts = config.get('verbosity_shortcuts') - if not verbosity and config.get('gui_enable_debug_logs'): + if not verbosity and config.GUI_ENABLE_DEBUG_LOGS: verbosity = '*' _configure_stderr_logging(verbosity=verbosity, verbosity_shortcuts=verbosity_shortcuts) if log_to_file is None: - log_to_file = config.get('log_to_file', False) + log_to_file = config.WRITE_LOGS_TO_DISK log_to_file |= is_android_debug_apk() if log_to_file: log_directory = pathlib.Path(config.path) / "logs" diff --git a/electrum/network.py b/electrum/network.py index e1269899c..ab08adb49 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -172,7 +172,7 @@ def serialize_proxy(p): p.get('user', ''), p.get('password', '')]) -def deserialize_proxy(s: str) -> Optional[dict]: +def deserialize_proxy(s: Optional[str]) -> Optional[dict]: if not isinstance(s, str): return None if s.lower() == 'none': @@ -295,7 +295,7 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): blockchain.read_blockchains(self.config) blockchain.init_headers_file_for_best_chain() self.logger.info(f"blockchains {list(map(lambda b: b.forkpoint, blockchain.blockchains.values()))}") - self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Dict[str, Any] + self._blockchain_preferred_block = self.config.BLOCKCHAIN_PREFERRED_BLOCK # type: Dict[str, Any] if self._blockchain_preferred_block is None: self._set_preferred_chain(None) self._blockchain = blockchain.get_best_chain() @@ -342,7 +342,7 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): self._was_started = False # lightning network - if self.config.get('run_watchtower', False): + if self.config.WATCHTOWER_SERVER_ENABLED: from . import lnwatcher self.local_watchtower = lnwatcher.WatchTower(self) asyncio.ensure_future(self.local_watchtower.start_watching()) @@ -358,7 +358,7 @@ def start_gossip(self): from . import lnrouter from . import channel_db from . import lnworker - if not self.config.get('use_gossip'): + if not self.config.LIGHTNING_USE_GOSSIP: return if self.lngossip is None: self.channel_db = channel_db.ChannelDB(self) @@ -489,9 +489,9 @@ def get_parameters(self) -> NetworkParameters: oneserver=self.oneserver) def _init_parameters_from_config(self) -> None: - self.auto_connect = self.config.get('auto_connect', True) + self.auto_connect = self.config.NETWORK_AUTO_CONNECT self._set_default_server() - self._set_proxy(deserialize_proxy(self.config.get('proxy'))) + self._set_proxy(deserialize_proxy(self.config.NETWORK_PROXY)) self._maybe_set_oneserver() def get_donation_address(self): @@ -554,7 +554,7 @@ def get_servers(self): else: out[server.host] = {server.protocol: port} # potentially filter out some - if self.config.get('noonion'): + if self.config.NETWORK_NOONION: out = filter_noonion(out) return out @@ -590,7 +590,7 @@ def _get_next_server_to_try(self) -> Optional[ServerAddr]: def _set_default_server(self) -> None: # Server for addresses and transactions - server = self.config.get('server', None) + server = self.config.NETWORK_SERVER # Sanitize default server if server: try: @@ -628,14 +628,14 @@ async def set_parameters(self, net_params: NetworkParameters): int(proxy['port']) except Exception: return - self.config.set_key('auto_connect', net_params.auto_connect, save=False) - self.config.set_key('oneserver', net_params.oneserver, save=False) - self.config.set_key('proxy', proxy_str, save=False) - self.config.set_key('server', str(server), save=True) + self.config.NETWORK_AUTO_CONNECT = net_params.auto_connect + self.config.NETWORK_ONESERVER = net_params.oneserver + self.config.NETWORK_PROXY = proxy_str + self.config.NETWORK_SERVER = str(server) # abort if changes were not allowed by config - if self.config.get('server') != str(server) \ - or self.config.get('proxy') != proxy_str \ - or self.config.get('oneserver') != net_params.oneserver: + if self.config.NETWORK_SERVER != str(server) \ + or self.config.NETWORK_PROXY != proxy_str \ + or self.config.NETWORK_ONESERVER != net_params.oneserver: return proxy_changed = self.proxy != proxy @@ -657,7 +657,7 @@ async def set_parameters(self, net_params: NetworkParameters): util.trigger_callback('network_updated') def _maybe_set_oneserver(self) -> None: - oneserver = bool(self.config.get('oneserver', False)) + oneserver = self.config.NETWORK_ONESERVER self.oneserver = oneserver self.num_server = NUM_TARGET_CONNECTED_SERVERS if not oneserver else 0 @@ -788,8 +788,8 @@ async def connection_down(self, interface: Interface): util.trigger_callback('network_updated') def get_network_timeout_seconds(self, request_type=NetworkTimeout.Generic) -> int: - if self.config.get('network_timeout', None): - return int(self.config.get('network_timeout')) + if self.config.NETWORK_TIMEOUT: + return self.config.NETWORK_TIMEOUT if self.oneserver and not self.auto_connect: return request_type.MOST_RELAXED if self.proxy: @@ -1191,7 +1191,7 @@ def _set_preferred_chain(self, chain: Optional[Blockchain]): 'height': height, 'hash': header_hash, } - self.config.set_key('blockchain_preferred_block', self._blockchain_preferred_block) + self.config.BLOCKCHAIN_PREFERRED_BLOCK = self._blockchain_preferred_block async def follow_chain_given_id(self, chain_id: str) -> None: bc = blockchain.blockchains.get(chain_id) diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 00e6d4f62..73273dd7e 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -400,10 +400,10 @@ def verify_cert_chain(chain): return x509_chain[0], ca -def check_ssl_config(config): +def check_ssl_config(config: 'SimpleConfig'): from . import pem - key_path = config.get('ssl_keyfile') - cert_path = config.get('ssl_certfile') + key_path = config.SSL_KEYFILE_PATH + cert_path = config.SSL_CERTFILE_PATH with open(key_path, 'r', encoding='utf-8') as f: params = pem.parse_private_key(f.read()) with open(cert_path, 'r', encoding='utf-8') as f: @@ -453,8 +453,8 @@ def serialize_request(req): # FIXME this is broken def make_request(config: 'SimpleConfig', req: 'Invoice'): pr = make_unsigned_request(req) - key_path = config.get('ssl_keyfile') - cert_path = config.get('ssl_certfile') + key_path = config.SSL_KEYFILE_PATH + cert_path = config.SSL_CERTFILE_PATH if key_path and cert_path: sign_request_with_x509(pr, key_path, cert_path) return pr diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 950297229..c6f9942f4 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1344,7 +1344,6 @@ class LedgerPlugin(HW_PluginBase): SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') def __init__(self, parent, config, name): - self.segwit = config.get("segwit") HW_PluginBase.__init__(self, parent, config, name) self.libraries_available = self.check_libraries_available() if not self.libraries_available: diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index 8f46c99bf..9300741c1 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -60,7 +60,7 @@ def daemon_wallet_loaded(self, daemon: 'Daemon', wallet: 'Abstract_Wallet'): # we use the first wallet loaded if self.server is not None: return - if self.config.get('offline'): + if self.config.NETWORK_OFFLINE: return self.server = PayServer(self.config, wallet) asyncio.run_coroutine_threadsafe(daemon.taskgroup.spawn(self.server.run()), daemon.asyncio_loop) @@ -79,7 +79,7 @@ def __init__(self, config: 'SimpleConfig', wallet: 'Abstract_Wallet'): assert self.has_www_dir(), self.WWW_DIR self.config = config self.wallet = wallet - url = self.config.get('payserver_address', 'localhost:8080') + url = self.config.PAYSERVER_ADDRESS self.addr = NetAddress.from_string(url) self.pending = defaultdict(asyncio.Event) self.register_callbacks() @@ -91,15 +91,15 @@ def has_www_dir(cls) -> bool: @property def base_url(self): - payserver = self.config.get('payserver_address', 'localhost:8080') + payserver = self.config.PAYSERVER_ADDRESS payserver = NetAddress.from_string(payserver) - use_ssl = bool(self.config.get('ssl_keyfile')) + use_ssl = bool(self.config.SSL_KEYFILE_PATH) protocol = 'https' if use_ssl else 'http' return '%s://%s:%d'%(protocol, payserver.host, payserver.port) @property def root(self): - return self.config.get('payserver_root', '/r') + return self.config.PAYSERVER_ROOT @event_listener async def on_event_request_status(self, wallet, key, status): @@ -118,7 +118,7 @@ async def run(self): # to minimise attack surface. note: "add_routes" call order matters (inner path goes first) app.add_routes([web.static(f"{self.root}/vendor", os.path.join(self.WWW_DIR, 'vendor'), follow_symlinks=True)]) app.add_routes([web.static(self.root, self.WWW_DIR)]) - if self.config.get('payserver_allow_create_invoice'): + if self.config.PAYSERVER_ALLOW_CREATE_INVOICE: app.add_routes([web.post('/api/create_invoice', self.create_request)]) runner = web.AppRunner(app) await runner.setup() diff --git a/electrum/plugins/payserver/qt.py b/electrum/plugins/payserver/qt.py index 48c65e03c..e2e36b470 100644 --- a/electrum/plugins/payserver/qt.py +++ b/electrum/plugins/payserver/qt.py @@ -59,19 +59,19 @@ def settings_widget(self, window: WindowModalDialog): partial(self.settings_dialog, window)) def settings_dialog(self, window: WindowModalDialog): - if self.config.get('offline'): + if self.config.NETWORK_OFFLINE: window.show_error(_("You are offline.")) return d = WindowModalDialog(window, _("PayServer Settings")) form = QtWidgets.QFormLayout(None) - addr = self.config.get('payserver_address', 'localhost:8080') + addr = self.config.PAYSERVER_ADDRESS assert self.server url = self.server.base_url + self.server.root + '/create_invoice.html' self.help_button = QtWidgets.QPushButton('View sample invoice creation form') self.help_button.clicked.connect(lambda: webopen(url)) address_e = QtWidgets.QLineEdit(addr) - keyfile_e = QtWidgets.QLineEdit(self.config.get('ssl_keyfile', '')) - certfile_e = QtWidgets.QLineEdit(self.config.get('ssl_certfile', '')) + keyfile_e = QtWidgets.QLineEdit(self.config.SSL_KEYFILE_PATH) + certfile_e = QtWidgets.QLineEdit(self.config.SSL_CERTFILE_PATH) form.addRow(QtWidgets.QLabel("Network address:"), address_e) form.addRow(QtWidgets.QLabel("SSL key file:"), keyfile_e) form.addRow(QtWidgets.QLabel("SSL cert file:"), certfile_e) @@ -82,9 +82,9 @@ def settings_dialog(self, window: WindowModalDialog): vbox.addSpacing(20) vbox.addLayout(Buttons(OkButton(d))) if d.exec_(): - self.config.set_key('payserver_address', str(address_e.text())) - self.config.set_key('ssl_keyfile', str(keyfile_e.text())) - self.config.set_key('ssl_certfile', str(certfile_e.text())) + self.config.PAYSERVER_ADDRESS = str(address_e.text()) + self.config.SSL_KEYFILE_PATH = str(keyfile_e.text()) + self.config.SSL_CERTFILE_PATH = str(certfile_e.text()) # fixme: restart the server window.show_message('Please restart Electrum to enable those changes') diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index 0aa918e4b..037e30f1e 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -208,7 +208,9 @@ def show_settings_dialog(self, window, success): grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) b = QRadioButton() b.setChecked(k == n_prepay) - b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, save=True)) + def on_click(b, k): + self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = k + b.clicked.connect(partial(on_click, k=k)) grid.addWidget(b, i, 2) i += 1 diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 181425dc2..b79a77a29 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -296,11 +296,11 @@ def min_prepay(self): return min(self.price_per_tx.keys()) def num_prepay(self): - default = self.min_prepay() - n = self.config.get('trustedcoin_prepay', default) - if n not in self.price_per_tx: - n = default - return n + default_fallback = self.min_prepay() + num = self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY + if num not in self.price_per_tx: + num = default_fallback + return num def extra_fee(self): if self.can_sign_without_server(): @@ -559,7 +559,7 @@ def choose_seed(self, wizard): wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) def choose_seed_type(self, wizard): - seed_type = '2fa' if self.config.get('nosegwit') else '2fa_segwit' + seed_type = '2fa' if self.config.WIZARD_DONT_CREATE_SEGWIT else '2fa_segwit' self.create_seed(wizard, seed_type) def create_seed(self, wizard, seed_type): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 1a73bdbfa..d98b00b7d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -5,14 +5,16 @@ import stat import ssl from decimal import Decimal -from typing import Union, Optional, Dict, Sequence, Tuple +from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set from numbers import Real +from functools import cached_property from copy import deepcopy from aiorpcx import NetAddress from . import util from . import constants +from . import invoices from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT from .util import format_satoshis, format_fee_satoshis, os_chmod from .util import user_dir, make_dir, NoDynamicFeeEstimates, quantize_feerate @@ -50,6 +52,72 @@ FINAL_CONFIG_VERSION = 3 +class ConfigVar(property): + + def __init__(self, key: str, *, default, type_=None): + self._key = key + self._default = default + self._type = type_ + property.__init__(self, self._get_config_value, self._set_config_value) + + def _get_config_value(self, config: 'SimpleConfig'): + value = config.get(self._key, default=self._default) + if self._type is not None and value != self._default: + assert value is not None, f"got None for key={self._key!r}" + try: + value = self._type(value) + except Exception as e: + raise ValueError( + f"ConfigVar.get type-check and auto-conversion failed. " + f"key={self._key!r}. type={self._type}. value={value!r}") from e + return value + + def _set_config_value(self, config: 'SimpleConfig', value, *, save=True): + if self._type is not None and value is not None: + if not isinstance(value, self._type): + raise ValueError( + f"ConfigVar.set type-check failed. " + f"key={self._key!r}. type={self._type}. value={value!r}") + config.set_key(self._key, value, save=save) + + def key(self) -> str: + return self._key + + def get_default_value(self) -> Any: + return self._default + + def __repr__(self): + return f"" + + +class ConfigVarWithConfig: + + def __init__(self, *, config: 'SimpleConfig', config_var: 'ConfigVar'): + self._config = config + self._config_var = config_var + + def get(self) -> Any: + return self._config_var._get_config_value(self._config) + + def set(self, value: Any, *, save=True) -> None: + self._config_var._set_config_value(self._config, value, save=save) + + def key(self) -> str: + return self._config_var.key() + + def get_default_value(self) -> Any: + return self._config_var.get_default_value() + + def is_modifiable(self) -> bool: + return self._config.is_modifiable(self._config_var) + + def is_set(self) -> bool: + return self._config.is_set(self._config_var) + + def __repr__(self): + return f"" + + class SimpleConfig(Logger): """ The SimpleConfig class is responsible for handling operations involving @@ -98,7 +166,7 @@ def __init__(self, options=None, read_user_config_function=None, # avoid new config getting upgraded self.user_config = {'config_version': FINAL_CONFIG_VERSION} - self._not_modifiable_keys = set() + self._not_modifiable_keys = set() # type: Set[str] # config "upgrade" - CLI options self.rename_config_keys( @@ -111,14 +179,15 @@ def __init__(self, options=None, read_user_config_function=None, self._check_dependent_keys() # units and formatting - self.decimal_point = self.get('decimal_point', DECIMAL_POINT_DEFAULT) + # FIXME is this duplication (dp, nz, post_sat, thou_sep) due to performance reasons?? + self.decimal_point = self.BTC_AMOUNTS_DECIMAL_POINT try: decimal_point_to_base_unit_name(self.decimal_point) except UnknownBaseUnit: self.decimal_point = DECIMAL_POINT_DEFAULT - self.num_zeros = int(self.get('num_zeros', 0)) - self.amt_precision_post_satoshi = int(self.get('amt_precision_post_satoshi', 0)) - self.amt_add_thousands_sep = bool(self.get('amt_add_thousands_sep', False)) + self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT + self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT + self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP def electrum_path(self): # Read electrum_path from command line @@ -158,7 +227,15 @@ def rename_config_keys(self, config, keypairs, deprecation_warning=False): updated = True return updated - def set_key(self, key, value, *, save=True): + def set_key(self, key: Union[str, ConfigVar, ConfigVarWithConfig], value, *, save=True) -> None: + """Set the value for an arbitrary string config key. + note: try to use explicit predefined ConfigVars instead of this method, whenever possible. + This method side-steps ConfigVars completely, and is mainly kept for situations + where the config key is dynamically constructed. + """ + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key if not self.is_modifiable(key): self.logger.warning(f"not changing config key '{key}' set on the command line") return @@ -170,7 +247,8 @@ def set_key(self, key, value, *, save=True): return self._set_key_in_user_config(key, value, save=save) - def _set_key_in_user_config(self, key, value, *, save=True): + def _set_key_in_user_config(self, key: str, value, *, save=True) -> None: + assert isinstance(key, str), key with self.lock: if value is not None: self.user_config[key] = value @@ -179,18 +257,33 @@ def _set_key_in_user_config(self, key, value, *, save=True): if save: self.save_user_config() - def get(self, key, default=None): + def get(self, key: str, default=None) -> Any: + """Get the value for an arbitrary string config key. + note: try to use explicit predefined ConfigVars instead of this method, whenever possible. + This method side-steps ConfigVars completely, and is mainly kept for situations + where the config key is dynamically constructed. + """ + assert isinstance(key, str), key with self.lock: out = self.cmdline_options.get(key) if out is None: out = self.user_config.get(key, default) return out + def is_set(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool: + """Returns whether the config key has any explicit value set/defined.""" + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key + return self.get(key, default=...) is not ... + def _check_dependent_keys(self) -> None: - if self.get('serverfingerprint'): - if not self.get('server'): - raise Exception("config key 'serverfingerprint' requires 'server' to also be set") - self.make_key_not_modifiable('server') + if self.NETWORK_SERVERFINGERPRINT: + if not self.NETWORK_SERVER: + raise Exception( + f"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires " + f"{self.__class__.NETWORK_SERVER.key()!r} to also be set") + self.make_key_not_modifiable(self.__class__.NETWORK_SERVER) def requires_upgrade(self): return self.get_config_version() < FINAL_CONFIG_VERSION @@ -254,15 +347,20 @@ def get_config_version(self): .format(config_version, FINAL_CONFIG_VERSION)) return config_version - def is_modifiable(self, key) -> bool: + def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool: + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() return (key not in self.cmdline_options and key not in self._not_modifiable_keys) - def make_key_not_modifiable(self, key) -> None: + def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None: + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key self._not_modifiable_keys.add(key) def save_user_config(self): - if self.get('forget_config'): + if self.CONFIG_FORGET_CHANGES: return if not self.path: return @@ -277,13 +375,13 @@ def save_user_config(self): if os.path.exists(self.path): # or maybe not? raise - def get_backup_dir(self): + def get_backup_dir(self) -> Optional[str]: # this is used to save wallet file backups (without active lightning channels) # on Android, the export backup button uses android_backup_dir() if 'ANDROID_DATA' in os.environ: return None else: - return self.get('backup_dir') + return self.WALLET_BACKUP_DIRECTORY def get_wallet_path(self, *, use_gui_last_wallet=False): """Set the path of the wallet.""" @@ -293,7 +391,7 @@ def get_wallet_path(self, *, use_gui_last_wallet=False): return os.path.join(self.get('cwd', ''), self.get('wallet_path')) if use_gui_last_wallet: - path = self.get('gui_last_wallet') + path = self.GUI_LAST_WALLET if path and os.path.exists(path): return path @@ -314,22 +412,22 @@ def get_fallback_wallet_path(self): return path def remove_from_recently_open(self, filename): - recent = self.get('recently_open', []) + recent = self.RECENTLY_OPEN_WALLET_FILES or [] if filename in recent: recent.remove(filename) - self.set_key('recently_open', recent) + self.RECENTLY_OPEN_WALLET_FILES = recent def set_session_timeout(self, seconds): self.logger.info(f"session timeout -> {seconds} seconds") - self.set_key('session_timeout', seconds) + self.HWD_SESSION_TIMEOUT = seconds def get_session_timeout(self): - return self.get('session_timeout', 300) + return self.HWD_SESSION_TIMEOUT def save_last_wallet(self, wallet): if self.get('wallet_path') is None: path = wallet.storage.path - self.set_key('gui_last_wallet', path) + self.GUI_LAST_WALLET = path def impose_hard_limits_on_fee(func): def get_fee_within_limits(self, *args, **kwargs): @@ -511,13 +609,13 @@ def get_fee_text( tooltip = '' return text, tooltip - def get_depth_level(self): + def get_depth_level(self) -> int: maxp = len(FEE_DEPTH_TARGETS) - 1 - return min(maxp, self.get('depth_level', 2)) + return min(maxp, self.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS) - def get_fee_level(self): + def get_fee_level(self) -> int: maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block" - return min(maxp, self.get('fee_level', 2)) + return min(maxp, self.FEE_EST_DYNAMIC_ETA_SLIDERPOS) def get_fee_slider(self, dyn, mempool) -> Tuple[int, int, Optional[int]]: if dyn: @@ -556,11 +654,11 @@ def has_dynamic_fees_ready(self): else: return self.has_fee_etas() - def is_dynfee(self): - return bool(self.get('dynamic_fees', True)) + def is_dynfee(self) -> bool: + return self.FEE_EST_DYNAMIC - def use_mempool_fees(self): - return bool(self.get('mempool_fees', False)) + def use_mempool_fees(self) -> bool: + return self.FEE_EST_USE_MEMPOOL def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool, mempool: bool) -> Union[int, None]: @@ -599,7 +697,7 @@ def fee_per_kb(self, dyn: bool=None, mempool: bool=None, fee_level: float=None) else: fee_rate = self.eta_to_fee(self.get_fee_level()) else: - fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE) + fee_rate = self.FEE_EST_STATIC_FEERATE_FALLBACK if fee_rate is not None: fee_rate = int(fee_rate) return fee_rate @@ -648,14 +746,14 @@ def requested_fee_estimates(self): self.last_time_fee_estimates_requested = time.time() def get_video_device(self): - device = self.get("video_device", "default") + device = self.VIDEO_DEVICE_PATH if device == 'default': device = '' return device def get_ssl_context(self): - ssl_keyfile = self.get('ssl_keyfile') - ssl_certfile = self.get('ssl_certfile') + ssl_keyfile = self.SSL_KEYFILE_PATH + ssl_certfile = self.SSL_CERTFILE_PATH if ssl_keyfile and ssl_certfile: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile) @@ -663,13 +761,16 @@ def get_ssl_context(self): def get_ssl_domain(self): from .paymentrequest import check_ssl_config - if self.get('ssl_keyfile') and self.get('ssl_certfile'): + if self.SSL_KEYFILE_PATH and self.SSL_CERTFILE_PATH: SSL_identity = check_ssl_config(self) else: SSL_identity = None return SSL_identity - def get_netaddress(self, key: str) -> Optional[NetAddress]: + def get_netaddress(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> Optional[NetAddress]: + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key text = self.get(key) if text: try: @@ -709,13 +810,158 @@ def get_base_unit(self): def set_base_unit(self, unit): assert unit in base_units.keys() self.decimal_point = base_unit_name_to_decimal_point(unit) - self.set_key('decimal_point', self.decimal_point, save=True) + self.BTC_AMOUNTS_DECIMAL_POINT = self.decimal_point def get_decimal_point(self): return self.decimal_point + @cached_property + def cv(self): + """Allows getting a reference to a config variable without dereferencing it. -def read_user_config(path): + Compare: + >>> config.NETWORK_SERVER + 'testnet.hsmiths.com:53012:s' + >>> config.cv.NETWORK_SERVER + + """ + class CVLookupHelper: + def __getattribute__(self2, name: str) -> ConfigVarWithConfig: + config_var = self.__class__.__getattribute__(type(self), name) + if not isinstance(config_var, ConfigVar): + raise AttributeError() + return ConfigVarWithConfig(config=self, config_var=config_var) + def __setattr__(self, name, value): + raise Exception( + f"Cannot assign value to config.cv.{name} directly. " + f"Either use config.cv.{name}.set() or assign to config.{name} instead.") + return CVLookupHelper() + + # config variables -----> + + NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) + NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool) + NETWORK_PROXY = ConfigVar('proxy', default=None) + NETWORK_SERVER = ConfigVar('server', default=None, type_=str) + NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool) + NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool) + NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool) + NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str) + NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=1_000_000, type_=int) # in bytes + NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int) + + WALLET_BATCH_RBF = ConfigVar('batch_rbf', default=False, type_=bool) + WALLET_SPEND_CONFIRMED_ONLY = ConfigVar('confirmed_only', default=False, type_=bool) + WALLET_COIN_CHOOSER_POLICY = ConfigVar('coin_chooser', default='Privacy', type_=str) + WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = ConfigVar('coin_chooser_output_rounding', default=True, type_=bool) + WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int) + WALLET_BIP21_LIGHTNING = ConfigVar('bip21_lightning', default=False, type_=bool) + WALLET_BOLT11_FALLBACK = ConfigVar('bolt11_fallback', default=True, type_=bool) + WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int) + WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool) + # note: 'use_change' and 'multiple_change' are per-wallet settings + + FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool) + FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) + FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str) # default exchange should ideally provide historical rates + FX_HISTORY_RATES = ConfigVar('history_rates', default=False, type_=bool) + FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar('history_rates_capital_gains', default=False, type_=bool) + FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar('fiat_address', default=False, type_=bool) + + LIGHTNING_LISTEN = ConfigVar('lightning_listen', default=None, type_=str) + LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None) + LIGHTNING_USE_GOSSIP = ConfigVar('use_gossip', default=False, type_=bool) + LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) + LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) + LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) + + EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool) + EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool) + TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool) + TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool) + TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int) + TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None) + TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool) + + FEE_EST_DYNAMIC = ConfigVar('dynamic_fees', default=True, type_=bool) + FEE_EST_USE_MEMPOOL = ConfigVar('mempool_fees', default=False, type_=bool) + FEE_EST_STATIC_FEERATE_FALLBACK = ConfigVar('fee_per_kb', default=FEERATE_FALLBACK_STATIC_FEE, type_=int) + FEE_EST_DYNAMIC_ETA_SLIDERPOS = ConfigVar('fee_level', default=2, type_=int) + FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = ConfigVar('depth_level', default=2, type_=int) + + RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str) + RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str) + RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str) + RPC_PORT = ConfigVar('rpcport', default=0, type_=int) + RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str) + RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str) + + GUI_NAME = ConfigVar('gui', default='qt', type_=str) + GUI_LAST_WALLET = ConfigVar('gui_last_wallet', default=None, type_=str) + + GUI_QT_COLOR_THEME = ConfigVar('qt_gui_color_theme', default='default', type_=str) + GUI_QT_DARK_TRAY_ICON = ConfigVar('dark_icon', default=False, type_=bool) + GUI_QT_WINDOW_IS_MAXIMIZED = ConfigVar('is_maximized', default=False, type_=bool) + GUI_QT_HIDE_ON_STARTUP = ConfigVar('hide_gui', default=False, type_=bool) + GUI_QT_HISTORY_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_history', default=False, type_=bool) + GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_addresses', default=False, type_=bool) + GUI_QT_TX_DIALOG_FETCH_TXIN_DATA = ConfigVar('tx_dialog_fetch_txin_data', default=False, type_=bool) + GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int) + GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool) + GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar('show_tx_io', default=False, type_=bool) + GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar('show_tx_fee_details', default=False, type_=bool) + GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar('show_tx_locktime', default=False, type_=bool) + + GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str) + GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool) + + BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int) + BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar('num_zeros', default=0, type_=int) + BTC_AMOUNTS_PREC_POST_SAT = ConfigVar('amt_precision_post_satoshi', default=0, type_=int) + BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar('amt_add_thousands_sep', default=False, type_=bool) + + BLOCK_EXPLORER = ConfigVar('block_explorer', default='Blockstream.info', type_=str) + BLOCK_EXPLORER_CUSTOM = ConfigVar('block_explorer_custom', default=None) + VIDEO_DEVICE_PATH = ConfigVar('video_device', default='default', type_=str) + OPENALIAS_ID = ConfigVar('alias', default="", type_=str) + HWD_SESSION_TIMEOUT = ConfigVar('session_timeout', default=300, type_=int) + CLI_TIMEOUT = ConfigVar('timeout', default=60, type_=float) + AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = ConfigVar('check_updates', default=False, type_=bool) + WRITE_LOGS_TO_DISK = ConfigVar('log_to_file', default=False, type_=bool) + GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool) + LOCALIZATION_LANGUAGE = ConfigVar('language', default="", type_=str) + BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None) + SHOW_CRASH_REPORTER = ConfigVar('show_crash_reporter', default=True, type_=bool) + DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool) + RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None) + IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str) + WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str) + CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str) + QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool) + WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool) + CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool) + + SSL_CERTFILE_PATH = ConfigVar('ssl_certfile', default='', type_=str) + SSL_KEYFILE_PATH = ConfigVar('ssl_keyfile', default='', type_=str) + + # connect to remote WT + WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) + WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) + + # run WT locally + WATCHTOWER_SERVER_ENABLED = ConfigVar('run_watchtower', default=False, type_=bool) + WATCHTOWER_SERVER_ADDRESS = ConfigVar('watchtower_address', default=None, type_=str) + WATCHTOWER_SERVER_USER = ConfigVar('watchtower_user', default=None, type_=str) + WATCHTOWER_SERVER_PASSWORD = ConfigVar('watchtower_password', default=None, type_=str) + + PAYSERVER_ADDRESS = ConfigVar('payserver_address', default='localhost:8080', type_=str) + PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str) + PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool) + + PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int) + + +def read_user_config(path: Optional[str]) -> Dict[str, Any]: """Parse and store the user config settings in electrum.conf into user_config[].""" if not path: return {} diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 5708a0df9..43e11951e 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -203,7 +203,7 @@ async def _claim_swap(self, swap: SwapData) -> None: self.lnwatcher.remove_callback(swap.lockup_address) swap.is_redeemed = True elif spent_height == TX_HEIGHT_LOCAL: - if txin.block_height > 0 or self.wallet.config.get('allow_instant_swaps', False): + if txin.block_height > 0 or self.wallet.config.LIGHTNING_ALLOW_INSTANT_SWAPS: tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) self.logger.info(f'broadcasting tx {txin.spent_txid}') await self.network.broadcast_transaction(tx) diff --git a/electrum/tests/test_daemon.py b/electrum/tests/test_daemon.py index 688890880..d5e707335 100644 --- a/electrum/tests/test_daemon.py +++ b/electrum/tests/test_daemon.py @@ -15,8 +15,8 @@ class TestUnifiedPassword(ElectrumTestCase): def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - self.config.set_key("single_password", True) - self.config.set_key("offline", True) + self.config.WALLET_USE_SINGLE_PASSWORD = True + self.config.NETWORK_OFFLINE = True self.wallet_dir = os.path.dirname(self.config.get_wallet_path()) assert "wallets" == os.path.basename(self.wallet_dir) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 095165469..99051c516 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -366,8 +366,8 @@ class PeerInTests(Peer): 'dave': high_fee_channel.copy(), }, 'config': { - 'lightning_forward_payments': True, - 'lightning_forward_trampoline_payments': True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True, }, }, 'carol': { @@ -375,8 +375,8 @@ class PeerInTests(Peer): 'dave': low_fee_channel.copy(), }, 'config': { - 'lightning_forward_payments': True, - 'lightning_forward_trampoline_payments': True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True, }, }, 'dave': { @@ -932,8 +932,8 @@ async def f(): @needs_test_with_all_chacha20_implementations async def test_payment_multihop_temp_node_failure(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) - graph.workers['bob'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) - graph.workers['carol'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) + graph.workers['bob'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True + graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True peers = graph.peers.values() async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash)) @@ -959,7 +959,7 @@ async def test_payment_multihop_route_around_failure(self): # Alice will pay Dave. Alice first tries A->C->D route, due to lower fees, but Carol # will fail the htlc and get blacklisted. Alice will then try A->B->D and succeed. graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) - graph.workers['carol'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) + graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True peers = graph.peers.values() async def pay(lnaddr, pay_req): self.assertEqual(500000000000, graph.channels[('alice', 'bob')].balance(LOCAL)) @@ -1298,16 +1298,16 @@ async def test_modern_shutdown_with_overlap(self): async def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee_range=None): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) - w1.network.config.set_key('test_shutdown_fee', alice_fee) - w2.network.config.set_key('test_shutdown_fee', bob_fee) + w1.network.config.TEST_SHUTDOWN_FEE = alice_fee + w2.network.config.TEST_SHUTDOWN_FEE = bob_fee if alice_fee_range is not None: - w1.network.config.set_key('test_shutdown_fee_range', alice_fee_range) + w1.network.config.TEST_SHUTDOWN_FEE_RANGE = alice_fee_range else: - w1.network.config.set_key('test_shutdown_legacy', True) + w1.network.config.TEST_SHUTDOWN_LEGACY = True if bob_fee_range is not None: - w2.network.config.set_key('test_shutdown_fee_range', bob_fee_range) + w2.network.config.TEST_SHUTDOWN_FEE_RANGE = bob_fee_range else: - w2.network.config.set_key('test_shutdown_legacy', True) + w2.network.config.TEST_SHUTDOWN_LEGACY = True w2.enable_htlc_settle = False lnaddr, pay_req = self.prepare_invoice(w2) async def pay(): @@ -1377,10 +1377,10 @@ async def test_close_upfront_shutdown_script(self): bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = b'' p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel) - w1.network.config.set_key('dynamic_fees', False) - w2.network.config.set_key('dynamic_fees', False) - w1.network.config.set_key('fee_per_kb', 5000) - w2.network.config.set_key('fee_per_kb', 1000) + w1.network.config.FEE_EST_DYNAMIC = False + w2.network.config.FEE_EST_DYNAMIC = False + w1.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 5000 + w2.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 1000 async def test(): async def close(): @@ -1407,10 +1407,10 @@ async def main_loop(peer): bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = bob_uss p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel) - w1.network.config.set_key('dynamic_fees', False) - w2.network.config.set_key('dynamic_fees', False) - w1.network.config.set_key('fee_per_kb', 5000) - w2.network.config.set_key('fee_per_kb', 1000) + w1.network.config.FEE_EST_DYNAMIC = False + w2.network.config.FEE_EST_DYNAMIC = False + w1.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 5000 + w2.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 1000 async def test(): async def close(): diff --git a/electrum/tests/test_simple_config.py b/electrum/tests/test_simple_config.py index f15e87a14..3204328a9 100644 --- a/electrum/tests/test_simple_config.py +++ b/electrum/tests/test_simple_config.py @@ -10,6 +10,10 @@ from . import ElectrumTestCase +MAX_MSG_SIZE_DEFAULT = SimpleConfig.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value() +assert isinstance(MAX_MSG_SIZE_DEFAULT, int), MAX_MSG_SIZE_DEFAULT + + class Test_SimpleConfig(ElectrumTestCase): def setUp(self): @@ -109,6 +113,75 @@ def test_user_config_is_not_written_with_read_only_config(self): result.pop('config_version', None) self.assertEqual({"something": "a"}, result) + def test_configvars_set_and_get(self): + config = SimpleConfig(self.options) + self.assertEqual("server", config.cv.NETWORK_SERVER.key()) + + def _set_via_assignment(): + config.NETWORK_SERVER = "example.com:443:s" + + for f in ( + lambda: config.set_key("server", "example.com:443:s"), + _set_via_assignment, + lambda: config.cv.NETWORK_SERVER.set("example.com:443:s"), + ): + self.assertTrue(config.get("server") is None) + self.assertTrue(config.NETWORK_SERVER is None) + self.assertTrue(config.cv.NETWORK_SERVER.get() is None) + f() + self.assertEqual("example.com:443:s", config.get("server")) + self.assertEqual("example.com:443:s", config.NETWORK_SERVER) + self.assertEqual("example.com:443:s", config.cv.NETWORK_SERVER.get()) + # revert: + config.NETWORK_SERVER = None + + def test_configvars_get_default_value(self): + config = SimpleConfig(self.options) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value()) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555 + self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value()) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = None + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + def test_configvars_is_set(self): + config = SimpleConfig(self.options) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555 + self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = None + self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = MAX_MSG_SIZE_DEFAULT + self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + def test_configvars_is_modifiable(self): + config = SimpleConfig({**self.options, "server": "example.com:443:s"}) + + self.assertFalse(config.is_modifiable("server")) + self.assertFalse(config.cv.NETWORK_SERVER.is_modifiable()) + + config.NETWORK_SERVER = "other-example.com:80:t" + self.assertEqual("example.com:443:s", config.NETWORK_SERVER) + + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_modifiable()) + config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555 + self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + config.make_key_not_modifiable(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_modifiable()) + config.NETWORK_MAX_INCOMING_MSG_SIZE = 2_222_222 + self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE) + def test_depth_target_to_fee(self): config = SimpleConfig(self.options) config.mempool_fees = [[49, 100110], [10, 121301], [6, 153731], [5, 125872], [1, 36488810]] diff --git a/electrum/tests/test_sswaps.py b/electrum/tests/test_sswaps.py index 3ce2906e0..89aaf9a4b 100644 --- a/electrum/tests/test_sswaps.py +++ b/electrum/tests/test_sswaps.py @@ -12,8 +12,8 @@ class TestSwapTxs(ElectrumTestCase): def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - self.config.set_key('dynamic_fees', False) - self.config.set_key('fee_per_kb', 1000) + self.config.FEE_EST_DYNAMIC = False + self.config.FEE_EST_STATIC_FEERATE_FALLBACK = 1000 def test_claim_tx_for_successful_reverse_swap(self): swap_data = SwapData( diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index aeeeeb92a..ebd05b551 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1040,7 +1040,7 @@ class TmpConfig(tempfile.TemporaryDirectory): # to avoid sub-tests side-effecti def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.config = SimpleConfig({'electrum_path': self.name}) - self.config.set_key('coin_chooser_output_rounding', False) + self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = False def __enter__(self): return self.config @@ -1744,7 +1744,7 @@ async def _bump_fee_when_new_inputs_need_to_be_added(self, *, simulate_moving_tx async def _rbf_batching(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) - wallet.config.set_key('batch_rbf', True) + wallet.config.WALLET_BATCH_RBF = True # bootstrap wallet (incoming funding_tx1) funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') @@ -1879,7 +1879,7 @@ async def test_rbf_batching__cannot_batch_as_would_need_to_use_ismine_outputs_of coins = wallet.get_spendable_coins(domain=None) self.assertEqual(2, len(coins)) - wallet.config.set_key('batch_rbf', batch_rbf) + wallet.config.WALLET_BATCH_RBF = batch_rbf tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000) tx.set_rbf(True) tx.locktime = 2423302 @@ -2211,7 +2211,7 @@ async def test_standard_wallet_cannot_sign_multisig_input_even_if_cosigner(self, async def test_dscancel(self, mock_save_db): self.maxDiff = None config = SimpleConfig({'electrum_path': self.electrum_path}) - config.set_key('coin_chooser_output_rounding', False) + config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = False for simulate_moving_txs in (False, True): with self.subTest(msg="_dscancel_when_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): @@ -3691,8 +3691,8 @@ def setUp(self): super().setUp() self.config = SimpleConfig({ 'electrum_path': self.electrum_path, - 'skipmerklecheck': True, # needed for Synchronizer to generate new addresses without SPV }) + self.config.NETWORK_SKIPMERKLECHECK = True # needed for Synchronizer to generate new addresses without SPV def create_wallet(self): ks = keystore.from_xpub('vpub5Vhmk4dEJKanDTTw6immKXa3thw45u3gbd1rPYjREB6viP13sVTWcH6kvbR2YeLtGjradr6SFLVt9PxWDBSrvw1Dc1nmd3oko3m24CQbfaJ') diff --git a/electrum/util.py b/electrum/util.py index 11b551848..d09a31a18 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -974,25 +974,24 @@ def block_explorer(config: 'SimpleConfig') -> Optional[str]: """Returns name of selected block explorer, or None if a custom one (not among hardcoded ones) is configured. """ - if config.get('block_explorer_custom') is not None: + if config.BLOCK_EXPLORER_CUSTOM is not None: return None - default_ = 'Blockstream.info' - be_key = config.get('block_explorer', default_) + be_key = config.BLOCK_EXPLORER be_tuple = block_explorer_info().get(be_key) if be_tuple is None: - be_key = default_ + be_key = config.cv.BLOCK_EXPLORER.get_default_value() assert isinstance(be_key, str), f"{be_key!r} should be str" return be_key def block_explorer_tuple(config: 'SimpleConfig') -> Optional[Tuple[str, dict]]: - custom_be = config.get('block_explorer_custom') + custom_be = config.BLOCK_EXPLORER_CUSTOM if custom_be: if isinstance(custom_be, str): return custom_be, _block_explorer_default_api_loc if isinstance(custom_be, (tuple, list)) and len(custom_be) == 2: return tuple(custom_be) - _logger.warning(f"not using 'block_explorer_custom' from config. " + _logger.warning(f"not using {config.cv.BLOCK_EXPLORER_CUSTOM.key()!r} from config. " f"expected a str or a pair but got {custom_be!r}") return None else: diff --git a/electrum/verifier.py b/electrum/verifier.py index 13f556b0d..ab44220ba 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -121,7 +121,7 @@ async def _request_and_verify_single_proof(self, tx_hash, tx_height): try: verify_tx_is_in_block(tx_hash, merkle_branch, pos, header, tx_height) except MerkleVerificationFailure as e: - if self.network.config.get("skipmerklecheck"): + if self.network.config.NETWORK_SKIPMERKLECHECK: self.logger.info(f"skipping merkle proof check {tx_hash}") else: self.logger.info(repr(e)) diff --git a/electrum/wallet.py b/electrum/wallet.py index 1ac27628d..f2efbc441 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1731,7 +1731,7 @@ def make_unsigned_transaction( # Let the coin chooser select the coins to spend coin_chooser = coinchooser.get_coin_chooser(self.config) # If there is an unconfirmed RBF tx, merge with it - base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) if self.config.get('batch_rbf', False) else None + base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) if self.config.WALLET_BATCH_RBF else None if base_tx: # make sure we don't try to spend change from the tx-to-be-replaced: coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()] @@ -1847,7 +1847,7 @@ def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool: # exempt large value UTXOs value_sats = utxo.value_sats() assert value_sats is not None - threshold = self.config.get('unconf_utxo_freeze_threshold', 5_000) + threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT if value_sats >= threshold: return False # if funding tx has any is_mine input, then UTXO is fine @@ -2457,7 +2457,7 @@ def delete_address(self, address: str) -> None: def get_request_URI(self, req: Request) -> Optional[str]: lightning_invoice = None - if self.config.get('bip21_lightning', False): + if self.config.WALLET_BIP21_LIGHTNING: lightning_invoice = self.get_bolt11_invoice(req) return req.get_bip21_URI(lightning_invoice=lightning_invoice) @@ -2614,7 +2614,7 @@ def get_bolt11_invoice(self, req: Request) -> str: amount_msat=amount_msat, message=req.message, expiry=req.exp, - fallback_address=req.get_address() if self.config.get('bolt11_fallback', True) else None) + fallback_address=req.get_address() if self.config.WALLET_BOLT11_FALLBACK else None) return invoice def create_request(self, amount_sat: int, message: str, exp_delay: int, address: Optional[str]): diff --git a/run_electrum b/run_electrum index 0f32d954f..36d9817b9 100755 --- a/run_electrum +++ b/run_electrum @@ -341,8 +341,8 @@ def main(): config_options = { 'verbosity': '*' if util.is_android_debug_apk() else '', 'cmd': 'gui', - 'gui': android_gui, - 'single_password': True, + SimpleConfig.GUI_NAME.key(): android_gui, + SimpleConfig.WALLET_USE_SINGLE_PASSWORD.key(): True, } if util.get_android_package_name() == "org.electrum.testnet.electrum": # ~hack for easier testnet builds. pkgname subject to change. @@ -394,8 +394,8 @@ def main(): # to not-yet-evaluated strings. if cmdname == 'gui': from electrum.gui.default_lang import get_default_language - gui_name = config.get('gui', 'qt') - lang = config.get('language') + gui_name = config.GUI_NAME + lang = config.LOCALIZATION_LANGUAGE if not lang: lang = get_default_language(gui_name=gui_name) _logger.info(f"get_default_language: detected default as {lang=!r}") @@ -459,7 +459,7 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): configure_logging(config) fd = daemon.get_file_descriptor(config) if fd is not None: - plugins = init_plugins(config, config.get('gui', 'qt')) + plugins = init_plugins(config, config.GUI_NAME) d = daemon.Daemon(config, fd, start_network=False) try: d.run_gui(config, plugins) @@ -490,10 +490,9 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): configure_logging(config, log_to_file=False) # don't spam logfiles for each client-side RPC, but support "-v" cmd = known_commands[cmdname] wallet_path = config.get_wallet_path() - if not config.get('offline'): + if not config.NETWORK_OFFLINE: init_cmdline(config_options, wallet_path, True, config=config) - timeout = config.get('timeout', 60) - if timeout: timeout = int(timeout) + timeout = config.CLI_TIMEOUT try: result = daemon.request(config, 'run_cmdline', (config_options,), timeout) except daemon.DaemonNotRunning: From c049b461bbd26c04c77448a01f506ca66257cbe1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 13:22:35 +0000 Subject: [PATCH 0930/1143] bump libsecp256k1 version --- contrib/android/p4a_recipes/libsecp256k1/__init__.py | 4 ++-- contrib/make_libsecp256k1.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/android/p4a_recipes/libsecp256k1/__init__.py b/contrib/android/p4a_recipes/libsecp256k1/__init__.py index a639fcf89..bf4d4e04f 100644 --- a/contrib/android/p4a_recipes/libsecp256k1/__init__.py +++ b/contrib/android/p4a_recipes/libsecp256k1/__init__.py @@ -6,9 +6,9 @@ class LibSecp256k1RecipePinned(LibSecp256k1Recipe): - version = "346a053d4c442e08191f075c3932d03140579d47" + version = "acf5c55ae6a94e5ca847e07def40427547876101" url = "https://github.com/bitcoin-core/secp256k1/archive/{version}.zip" - sha512sum = "d6232bd8fb29395984b15633bee582e7588ade0ec1c7bea5b2cab766b1ff657672b804e078656e0ce4067071140b0552d12ce3c01866231b212f3c65908b85aa" + sha512sum = "6639f239de3c7abc1906088f2b0bf833b3c7b073bc25151fa908a64b5585dce59a073ed4eb0c0c3360c785a639ca4fce897e0288b94bbfa7f1d07f7ab610f1d6" recipe = LibSecp256k1RecipePinned() diff --git a/contrib/make_libsecp256k1.sh b/contrib/make_libsecp256k1.sh index 11cee4078..99a4bb441 100755 --- a/contrib/make_libsecp256k1.sh +++ b/contrib/make_libsecp256k1.sh @@ -14,8 +14,8 @@ # sudo apt-get install gcc-multilib g++-multilib # $ AUTOCONF_FLAGS="--host=i686-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32" ./contrib/make_libsecp256k1.sh -LIBSECP_VERSION="346a053d4c442e08191f075c3932d03140579d47" -# ^ tag "v0.3.1" +LIBSECP_VERSION="acf5c55ae6a94e5ca847e07def40427547876101" +# ^ tag "v0.3.2" set -e From dfa2b71bc326883da38865aa8609509eaf70eff2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 14:03:03 +0000 Subject: [PATCH 0931/1143] config: trivial rename for better readability --- electrum/simple_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index d98b00b7d..4ebf37f72 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -816,7 +816,7 @@ def get_decimal_point(self): return self.decimal_point @cached_property - def cv(self): + def cv(config): """Allows getting a reference to a config variable without dereferencing it. Compare: @@ -826,11 +826,11 @@ def cv(self): """ class CVLookupHelper: - def __getattribute__(self2, name: str) -> ConfigVarWithConfig: - config_var = self.__class__.__getattribute__(type(self), name) + def __getattribute__(self, name: str) -> ConfigVarWithConfig: + config_var = config.__class__.__getattribute__(type(config), name) if not isinstance(config_var, ConfigVar): raise AttributeError() - return ConfigVarWithConfig(config=self, config_var=config_var) + return ConfigVarWithConfig(config=config, config_var=config_var) def __setattr__(self, name, value): raise Exception( f"Cannot assign value to config.cv.{name} directly. " From 328a2bb3f23b7db7b46479dcc6572cfc55b7e3d1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 13:47:56 +0000 Subject: [PATCH 0932/1143] config: migrate qt gui optional tabs to config vars --- electrum/gui/qt/main_window.py | 40 ++++++++++++++++++++-------------- electrum/simple_config.py | 5 +++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 08eb6490f..2eefc1ec8 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -103,6 +103,7 @@ from .balance_dialog import BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED, COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING if TYPE_CHECKING: + from electrum.simple_config import ConfigVarWithConfig from . import ElectrumGui @@ -173,8 +174,8 @@ def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): self.gui_thread = gui_object.gui_thread assert wallet, "no wallet" self.wallet = wallet - if wallet.has_lightning(): - self.wallet.config.set_key('show_channels_tab', True) + if wallet.has_lightning() and not self.config.cv.GUI_QT_SHOW_TAB_CHANNELS.is_set(): + self.config.GUI_QT_SHOW_TAB_CHANNELS = True # override default, but still allow disabling tab manually Exception_Hook.maybe_setup(config=self.config, wallet=self.wallet) @@ -216,19 +217,18 @@ def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): tabs.addTab(self.send_tab, read_QIcon("tab_send.png"), _('Send')) tabs.addTab(self.receive_tab, read_QIcon("tab_receive.png"), _('Receive')) - def add_optional_tab(tabs, tab, icon, description, name): + def add_optional_tab(tabs, tab, icon, description): tab.tab_icon = icon tab.tab_description = description tab.tab_pos = len(tabs) - tab.tab_name = name - if self.config.get('show_{}_tab'.format(name), False): + if tab.is_shown_cv.get(): tabs.addTab(tab, icon, description.replace("&", "")) - add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses"), "addresses") - add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels") - add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins"), "utxo") - add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts"), "contacts") - add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole"), "console") + add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses")) + add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels")) + add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins")) + add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts")) + add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole")) tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -339,8 +339,8 @@ def on_fx_quotes(self): self.address_list.refresh_all() def toggle_tab(self, tab): - show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) - self.config.set_key('show_{}_tab'.format(tab.tab_name), show) + show = not tab.is_shown_cv.get() + tab.is_shown_cv.set(show) if show: # Find out where to place the tab index = len(self.tabs) @@ -698,7 +698,7 @@ def init_menubar(self): wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) def add_toggle_action(view_menu, tab): - is_shown = self.config.get('show_{}_tab'.format(tab.tab_name), False) + is_shown = tab.is_shown_cv.get() tab.menu_action = view_menu.addAction(tab.tab_description, lambda: self.toggle_tab(tab)) tab.menu_action.setCheckable(True) tab.menu_action.setChecked(is_shown) @@ -1026,7 +1026,9 @@ def refresh_tabs(self, wallet=None): def create_channels_tab(self): self.channels_list = ChannelsList(self) - return self.create_list_tab(self.channels_list) + tab = self.create_list_tab(self.channels_list) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CHANNELS + return tab def create_history_tab(self): self.history_model = HistoryModel(self) @@ -1351,17 +1353,22 @@ def create_addresses_tab(self): from .address_list import AddressList self.address_list = AddressList(self) tab = self.create_list_tab(self.address_list) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_ADDRESSES return tab def create_utxo_tab(self): from .utxo_list import UTXOList self.utxo_list = UTXOList(self) - return self.create_list_tab(self.utxo_list) + tab = self.create_list_tab(self.utxo_list) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_UTXO + return tab def create_contacts_tab(self): from .contact_list import ContactList self.contact_list = l = ContactList(self) - return self.create_list_tab(l) + tab = self.create_list_tab(l) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CONTACTS + return tab def remove_address(self, addr): if not self.question(_("Do you want to remove {} from your wallet?").format(addr)): @@ -1489,6 +1496,7 @@ def show_lightning_invoice(self, invoice: Invoice): def create_console_tab(self): from .console import Console self.console = console = Console() + console.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CONSOLE return console def update_console(self): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4ebf37f72..41ad1b47f 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -911,6 +911,11 @@ def __setattr__(self, name, value): GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar('show_tx_io', default=False, type_=bool) GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar('show_tx_fee_details', default=False, type_=bool) GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar('show_tx_locktime', default=False, type_=bool) + GUI_QT_SHOW_TAB_ADDRESSES = ConfigVar('show_addresses_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_CHANNELS = ConfigVar('show_channels_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_UTXO = ConfigVar('show_utxo_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool) GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str) GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool) From 7164f9fd6ea9f13a88ebbb4836162e9417f249a1 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 30 May 2023 23:55:21 +0200 Subject: [PATCH 0933/1143] bitbox02: update to 6.2.0 6.2.0 was released to put a minimum requirement on hidapi 0.14.0, which includes the fix for this issue: https://github.com/libusb/hidapi/issues/531 That bug caused hidapi on macOS 13.3 to report 0 as the interface number for all hid devices, which led to the bitbox02 multi edition being listed twice instead of once - once for the main HW wallet interface and once erroneously For the U2F interface (which should not be listed). --- contrib/requirements/requirements-hw.txt | 2 +- electrum/plugins/bitbox02/bitbox02.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 05cff1acc..36f310eed 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -22,7 +22,7 @@ hidapi ckcc-protocol>=0.7.7 # device plugin: bitbox02 -bitbox02>=6.0.0 +bitbox02>=6.2.0 # device plugin: jade cbor>=1.0.0,<2.0.0 diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index e07544213..1095c39da 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -631,7 +631,7 @@ def show_address( class BitBox02Plugin(HW_PluginBase): keystore_class = BitBox02_KeyStore - minimum_library = (6, 0, 0) + minimum_library = (6, 2, 0) DEVICE_IDS = [(0x03EB, 0x2403)] SUPPORTED_XTYPES = ("p2wpkh-p2sh", "p2wpkh", "p2wsh", "p2wsh-p2sh") From dfed0ef54acd566cb8c375157a23c4611043bd8d Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Tue, 30 May 2023 23:56:27 +0200 Subject: [PATCH 0934/1143] bitbox02: display amounts in sats if Electrum's base unit is sat The BitBox02 has the ability to display all amounts in sats instead of BTC. This was introduced in v9.13.0. If Electrum is configured to show sats, we propagate this config to the BitBox02. This is backwards compatible: users with older firmware will see the values in BTC regardless of the config. --- electrum/plugins/bitbox02/bitbox02.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/electrum/plugins/bitbox02/bitbox02.py b/electrum/plugins/bitbox02/bitbox02.py index 1095c39da..320dd0e16 100644 --- a/electrum/plugins/bitbox02/bitbox02.py +++ b/electrum/plugins/bitbox02/bitbox02.py @@ -510,6 +510,12 @@ def sign_transaction( ) keypath_account = full_path[:-2] + + format_unit = bitbox02.btc.BTCSignInitRequest.FormatUnit.DEFAULT + # Base unit is configured to be "sat": + if self.config.get_decimal_point() == 0: + format_unit = bitbox02.btc.BTCSignInitRequest.FormatUnit.SAT + sigs = self.bitbox02_device.btc_sign( coin, [bitbox02.btc.BTCScriptConfigWithKeypath( @@ -520,6 +526,7 @@ def sign_transaction( outputs=outputs, locktime=tx.locktime, version=tx.version, + format_unit=format_unit, ) # Fill signatures From 1b9cb47623fc4b7f7622a7fd02ca7dd35d6c6a87 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 22:51:06 +0000 Subject: [PATCH 0935/1143] dependencies: rm upper limit for hidapi Was added in 752b37a03b03cc4e68c3ec1c187cf7626891ecb2, due to upstream issue https://github.com/trezor/cython-hidapi/issues/142, which should now be fixed in 0.14.0. --- .../deterministic-build/requirements-hw.txt | 64 ++++++++++++------- contrib/requirements/requirements-hw.txt | 5 +- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 542267c22..8e16feb18 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -123,28 +123,48 @@ cryptography==38.0.3 \ ecdsa==0.18.0 \ --hash=sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49 \ --hash=sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd -hidapi==0.10.1 \ - --hash=sha256:095798ae1b3d6892fb0eb7ba1ab06054f6fafe6d09bc3714d80fdbf227c98f87 \ - --hash=sha256:0c92b398f6907654b07f7dbd7e06661abe9ad6119b403eb5fd3c2af4ce66a3b7 \ - --hash=sha256:0f558430c086e9e0028022f4fdfaed5044e6af50cb4f12b79c498da59fc84d51 \ - --hash=sha256:1e25317ac577e837154f90bc9b6da0451242211beea7a34b9bb117eec17c2e26 \ - --hash=sha256:310c53aa81697bf16b5f0c127afda36e5e9ea37794147afe1461422623263ef7 \ - --hash=sha256:39191d3e34e9a79e3dbb37898ab2ca4b84495e0815801cf84d185936e97bc6ee \ - --hash=sha256:3b93d3f9bae38a3459491194ba1abf5c292b59dbd8738c3ac66f01b593cf3724 \ - --hash=sha256:46de7d852e10a83187da6af88d6e2cd51ba5ab336bf2ce1659bc879141570de3 \ - --hash=sha256:4a081c3775a7ac850743dd345a1a4fb1175af3954c4c7a1f7508ab645f72dcb5 \ - --hash=sha256:4a1f4c54d71f748ee6c9ee86565ca0eba2ab9c5d88f9adacc1fe7c3b09a3f299 \ - --hash=sha256:4bab0e8ab066527e09856a6a345e2e0c10061f2640e9281323da9a04b94bdec1 \ - --hash=sha256:59f5205928dbe92513038c50dfb4f939395f8f781e176259a40f37d7a291313f \ - --hash=sha256:5cc5f15b1b68bcb04fa290abc87070d57a8c0d2cb3a01bafeba6a7df52cd8641 \ - --hash=sha256:74e968631537f52579c19c2b93e428b634dc385eb7808071bd9ff759d837fb39 \ - --hash=sha256:83d8aab01afd397a0fee0017df4397fff96bef639d6176f94b747305324732aa \ - --hash=sha256:9ac04c6dc3d792d92b1d6ff461511853fa166a0e22f4475fe60ad647555d1caf \ - --hash=sha256:a1170b18050bc57fae3840a51084e8252fd319c0fc6043d68c8501deb0e25846 \ - --hash=sha256:b1becc9f09c85c473e91cf869b592d5d87fb8b89672988de33776b20b4c53ce1 \ - --hash=sha256:b686b2b547890c8ed17ebeabded0050ce377180a56daefa20822b4d66d3a5dea \ - --hash=sha256:df4a23cd03f00d5cdc603252650df82cdd1923ceef6811cb029cc9d11a9a7a61 \ - --hash=sha256:f49a0de45217366b85597c2edb4be8bd61c9f26f533b854b058dded4352dd89d +hidapi==0.14.0 \ + --hash=sha256:01929fbbe206ebcb0bad9b8e925e16de0aa8f872bf80a263f599e519866d9900 \ + --hash=sha256:0fb47a0a8c3a6797306ea9eb8d1bdad68e5493ef5c8fa2e644501d56f2677551 \ + --hash=sha256:1370bc6a364fd292accd580a8d7bac4219932144d149f3a513bb472581eac421 \ + --hash=sha256:15f1fd34b0719d1e4d1bbc0bce325b318ee3e85c36fac0d23c6fb9d7f4d611db \ + --hash=sha256:1b4052f17321f5f0b641e020eae87db5bb0103f893198e61b2495358db83ddab \ + --hash=sha256:1c0959d89bc95acb4f9e6d58c8562281e22694959e42c10108193a1362b4fcd9 \ + --hash=sha256:2906ad143ec40009c33348ab4b3f7a9bdaa87b65bdc55983399bed47ee90a818 \ + --hash=sha256:2e635c037d28e1ceded2043d81b879d81348a278d1ae668954a5a7a7d383f7d7 \ + --hash=sha256:349976417f7f3371c7133a6427ed8f4faa06fbd93e9b5309d86689f25f191150 \ + --hash=sha256:365d7c9fdcae71ae41797dc2dd062dfed4362d1b36d21fa62afbc16c5ec3cd5a \ + --hash=sha256:3ed9f993a6f8a611c11ef213968c6972e17d7e8b27936349884c475dc0309e71 \ + --hash=sha256:4046bbfc67c5587ca638b875858569a8787e6955eff5dea4e424044de09fe7e4 \ + --hash=sha256:48e2cf77626f3cfdda9624de3be7f9c55e37efbb39882d2e96a92d38893a09cb \ + --hash=sha256:4c78ff5c46128bdf68b2c4e4b08fac7765ef79f6ee7e17c8a2f7d3090a591d97 \ + --hash=sha256:5299d74d96bdc9eaa83496c972048db0027d012a08440b33bdb6dd10a7491da9 \ + --hash=sha256:60c034ec3ef3e5679232d9e6c003c4848e4772032d683f0b91ddb84b87d8698d \ + --hash=sha256:651c2382e974e866d78334cdde3c290a04fcbab4cec940c0d3586d77d11b9566 \ + --hash=sha256:7ef0f40a02e0b56fe2e7c93dfc9810245f2feeaa0c2ea76654d0768722883639 \ + --hash=sha256:810ad22831e4a150c2d6f27141fcf2826fd085ccacf4262d5c742c90aa81cd54 \ + --hash=sha256:833a32c3e44780f37d46dffd559b8e245034c92ae25060f752e4f34e9c7efe24 \ + --hash=sha256:93697007df8ba38ab3ae3e777a6875cd1775fc720afe27e4c624cecbab7720de \ + --hash=sha256:96ecea60915212e59940db41c2a91709ebd4ec6a04e03b0db37a4ddb6825bee6 \ + --hash=sha256:9e245719a5ede83c779dd99a4553002ae684d92d0f3e4274dcf06882b063f127 \ + --hash=sha256:9fdc08eb19f2fffb989124d1dbea3aa62dd0036615bbf464ceafee0353673bf4 \ + --hash=sha256:a6edc57962a9f30bff73fc0cc80915c9da9ab3e0892c601263198f8d21d8dfff \ + --hash=sha256:a7cb029286ced5426a381286526d9501846409701a29c2538615c3d1a612b8be \ + --hash=sha256:b054abf40b5aa7122314af59d0244fa274a50c4276d20695d8b7ff69564beb95 \ + --hash=sha256:b264c6a1a1a0cacacc82299785415bec91184cb3e4a77d127c40016086705327 \ + --hash=sha256:b4513311fad7e499ebb0d7a26178557b85044983199a280cb95c2038902fe1a0 \ + --hash=sha256:b4a0feac62d80eca36e2c8035fe4f57c440fbfcd9273a909112cb5bd9baae449 \ + --hash=sha256:bb87cf8f23c15346bc1487e6f39d11b37d3ff7788037d3760b7907ea325b6d2c \ + --hash=sha256:c1425f523258d25d8f32a6493978532477c4d7507f5f9252417b1d629427871e \ + --hash=sha256:c1b1ded4a823cc5c2075a622b48d02bc0a72f57579ea24c956ef29649a49eb66 \ + --hash=sha256:c8bba64d6ed49fa7ea4f4515986450223f5c744be448c846fb0614bc53b536bd \ + --hash=sha256:de293e7291b1ec813a97e42625c2c0a41b0d25d495b3dc5864bbb3dbbb5a719d \ + --hash=sha256:dff930adb37d1bcaeca3cf0dcec00eb72c109aa42c84858809cbae2972d79661 \ + --hash=sha256:e6ef0bdc69310cfdff83faf96c75492ac3d8cf355af275904f1dd90a3c5f24a4 \ + --hash=sha256:e822e899c13eb1e3a575712d7be5bd03a9103f6027b00ab4351c8404cec5719d \ + --hash=sha256:ed112c9ba0adf41d7e04bf5389dc150ada4d94a6ef1cb56c325d5aed1e4e07d2 \ + --hash=sha256:f575381efa788e1a894c68439644817b152b8a68ead643e42c23ba28eeedc33b \ + --hash=sha256:f68bbf88805553911e7e5a9b91136c96a54042b6e3d82d39d733d2edb46ff9a6 idna==3.4 \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 05cff1acc..213cae63e 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -1,7 +1,4 @@ -# Prefer old hidapi as newer versions have issues on macOS -# (see #7738 and https://github.com/trezor/cython-hidapi/issues/142 ). -# Note: newer hidapi should also work. -hidapi<0.11 +hidapi # device plugin: trezor trezor[hidapi]>=0.13.0,<0.14 From 184281d2fcf6a45a2e375b14a46fed4904b6b878 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 22:54:26 +0000 Subject: [PATCH 0936/1143] version_info cmd: better version for hidapi was added in https://github.com/trezor/cython-hidapi/pull/143 --- electrum/plugin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/electrum/plugin.py b/electrum/plugin.py index 12a8cbae6..0194fb1a2 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -786,11 +786,15 @@ def version_info(cls) -> Mapping[str, Optional[str]]: except AttributeError: ret["libusb.path"] = None # add hidapi - from importlib.metadata import version try: - ret["hidapi.version"] = version("hidapi") # FIXME does not work in macOS binary - except ImportError: - ret["hidapi.version"] = None + import hid + ret["hidapi.version"] = hid.__version__ # available starting with 0.12.0.post2 + except Exception as e: + from importlib.metadata import version + try: + ret["hidapi.version"] = version("hidapi") + except ImportError: + ret["hidapi.version"] = None return ret def trigger_pairings( From f284b42fa8f96e61374624f8d994a115c1cd78fa Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 23:34:46 +0000 Subject: [PATCH 0937/1143] build: update pinned bitbox02 (partial rerun freeze_packages) --- contrib/deterministic-build/requirements-hw.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 542267c22..c7d01380e 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -1,9 +1,9 @@ base58==2.1.1 \ --hash=sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 \ --hash=sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c -bitbox02==6.1.1 \ - --hash=sha256:99503409d6c61899f8e11eb11e7a29866b2754cb02c03acf34cdef99755aedd8 \ - --hash=sha256:f37f1e571f06aa0a4005441ca1948b53e68e3bef0b642963303b42810e2b1486 +bitbox02==6.2.0 \ + --hash=sha256:5a8290bd270468ccdf2e6ff7174d25ea2b2f191e19734a79aa573c2b982c266f \ + --hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf btchip-python==0.1.32 \ --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55 cbor==1.0.0 \ From 6fbe765a3e0ec66bc3ec972a90deaa60c9307307 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 31 May 2023 11:29:14 +0200 Subject: [PATCH 0938/1143] qml: make ConfirmTxDialog flickable if content larger than window (fixes #8446) --- .../gui/qml/components/ConfirmTxDialog.qml | 242 +++++++++--------- 1 file changed, 125 insertions(+), 117 deletions(-) diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index 8fd8e0075..14fcc3a38 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -35,165 +35,173 @@ ElDialog { } ColumnLayout { - width: parent.width - height: parent.height + anchors.fill: parent spacing: 0 - GridLayout { + Flickable { Layout.fillWidth: true - Layout.leftMargin: constants.paddingLarge - Layout.rightMargin: constants.paddingLarge + Layout.fillHeight: true - columns: 2 + leftMargin: constants.paddingLarge + rightMargin: constants.paddingLarge - Label { - id: amountLabel - Layout.fillWidth: true - Layout.minimumWidth: implicitWidth - text: qsTr('Amount to send') - color: Material.accentColor - } - RowLayout { - Layout.fillWidth: true - Label { - id: btcValue - font.bold: true - font.family: FixedFont - } + contentHeight: rootLayout.height + clip: true + interactive: height < contentHeight - Label { - text: Config.baseUnit - color: Material.accentColor - } + GridLayout { + id: rootLayout + width: parent.width + + columns: 2 Label { - id: fiatValue + id: amountLabel Layout.fillWidth: true - font.pixelSize: constants.fontSizeMedium + Layout.minimumWidth: implicitWidth + text: qsTr('Amount to send') + color: Material.accentColor } - - Component.onCompleted: updateAmountText() - Connections { - target: finalizer - function onEffectiveAmountChanged() { - updateAmountText() + RowLayout { + Layout.fillWidth: true + Label { + id: btcValue + font.bold: true + font.family: FixedFont } - } - } - Label { - text: qsTr('Mining fee') - color: Material.accentColor - } + Label { + text: Config.baseUnit + color: Material.accentColor + } - FormattedAmount { - amount: finalizer.fee - } + Label { + id: fiatValue + Layout.fillWidth: true + font.pixelSize: constants.fontSizeMedium + } - Label { - visible: !finalizer.extraFee.isEmpty - text: qsTr('Extra fee') - color: Material.accentColor - } + Component.onCompleted: updateAmountText() + Connections { + target: finalizer + function onEffectiveAmountChanged() { + updateAmountText() + } + } + } - FormattedAmount { - visible: !finalizer.extraFee.isEmpty - amount: finalizer.extraFee - } + Label { + text: qsTr('Mining fee') + color: Material.accentColor + } - Label { - text: qsTr('Fee rate') - color: Material.accentColor - } + FormattedAmount { + amount: finalizer.fee + } - RowLayout { Label { - id: feeRate - text: finalizer.feeRate - font.family: FixedFont + visible: !finalizer.extraFee.isEmpty + text: qsTr('Extra fee') + color: Material.accentColor + } + + FormattedAmount { + visible: !finalizer.extraFee.isEmpty + amount: finalizer.extraFee } Label { - text: 'sat/vB' + text: qsTr('Fee rate') color: Material.accentColor } - } - Label { - text: qsTr('Target') - color: Material.accentColor - } + RowLayout { + Label { + id: feeRate + text: finalizer.feeRate + font.family: FixedFont + } - Label { - id: targetdesc - text: finalizer.target - } + Label { + text: 'sat/vB' + color: Material.accentColor + } + } - RowLayout { - Layout.columnSpan: 2 - Layout.fillWidth: true + Label { + text: qsTr('Target') + color: Material.accentColor + } + + Label { + id: targetdesc + text: finalizer.target + } - Slider { - id: feeslider + RowLayout { + Layout.columnSpan: 2 Layout.fillWidth: true - leftPadding: constants.paddingMedium - snapMode: Slider.SnapOnRelease - stepSize: 1 - from: 0 - to: finalizer.sliderSteps + Slider { + id: feeslider + Layout.fillWidth: true + leftPadding: constants.paddingMedium - onValueChanged: { - if (activeFocus) - finalizer.sliderPos = value - } - Component.onCompleted: { - value = finalizer.sliderPos - } - Connections { - target: finalizer - function onSliderPosChanged() { - feeslider.value = finalizer.sliderPos + snapMode: Slider.SnapOnRelease + stepSize: 1 + from: 0 + to: finalizer.sliderSteps + + onValueChanged: { + if (activeFocus) + finalizer.sliderPos = value + } + Component.onCompleted: { + value = finalizer.sliderPos + } + Connections { + target: finalizer + function onSliderPosChanged() { + feeslider.value = finalizer.sliderPos + } } } - } - FeeMethodComboBox { - id: target - feeslider: finalizer + FeeMethodComboBox { + id: target + feeslider: finalizer + } } - } - - InfoTextArea { - Layout.columnSpan: 2 - Layout.fillWidth: true - Layout.topMargin: constants.paddingLarge - Layout.bottomMargin: constants.paddingLarge - visible: finalizer.warning != '' - text: finalizer.warning - iconStyle: InfoTextArea.IconStyle.Warn - } - - Label { - text: qsTr('Outputs') - Layout.columnSpan: 2 - color: Material.accentColor - } - Repeater { - model: finalizer.outputs - delegate: TxOutput { + InfoTextArea { Layout.columnSpan: 2 Layout.fillWidth: true + Layout.topMargin: constants.paddingLarge + Layout.bottomMargin: constants.paddingLarge + visible: finalizer.warning != '' + text: finalizer.warning + iconStyle: InfoTextArea.IconStyle.Warn + } - allowShare: false - model: modelData + Label { + text: qsTr('Outputs') + Layout.columnSpan: 2 + color: Material.accentColor + } + + Repeater { + model: finalizer.outputs + delegate: TxOutput { + Layout.columnSpan: 2 + Layout.fillWidth: true + + allowShare: false + model: modelData + } } } } - Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } - FlatButton { id: sendButton Layout.fillWidth: true From 1c0ba83d106deed22e912b1e85bd99818a2d40f3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 May 2023 10:58:54 +0000 Subject: [PATCH 0939/1143] (trivial) qt wizard: add title to seed options dialog window --- electrum/gui/qt/seed_dialog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/seed_dialog.py b/electrum/gui/qt/seed_dialog.py index 1a2eb84a6..fd3022a63 100644 --- a/electrum/gui/qt/seed_dialog.py +++ b/electrum/gui/qt/seed_dialog.py @@ -66,6 +66,7 @@ class SeedLayout(QVBoxLayout): def seed_options(self): dialog = QDialog() + dialog.setWindowTitle(_("Seed Options")) vbox = QVBoxLayout(dialog) seed_types = [ From 22919bc15cf744e7e2cdd85a94e06514346dd8c6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 May 2023 12:50:32 +0000 Subject: [PATCH 0940/1143] update locale --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 3966f7c5d..1f5416b24 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 3966f7c5df7b3d367eb8b0f1aa88d123aba34426 +Subproject commit 1f5416b2448fa594c684bbb893a91f3d24a2b238 From bd5a78626275b6e3a0e443faa4395ca78cce0568 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 31 May 2023 12:54:31 +0000 Subject: [PATCH 0941/1143] update release notes for version 4.4.4 --- RELEASE-NOTES | 13 ++++++++++++- electrum/version.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 787767ad6..dc067ab42 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,8 +1,19 @@ +# Release 4.4.4 (May 31, 2023) + * QML GUI: + - fix creating multisig wallets involving BIP39 seeds (#8432) + - fix "cannot scroll to open a lightning channel" (#8446) + - wizard: "confirm seed" screen to normalize whitespaces (#8442) + - fix assert on address details screen (#8420) + * Qt GUI: + - better handle some expected errors in SwapDialog (#8430) + * libsecp256k1: bump bundled version to 0.3.2 (10574bb1) + + # Release 4.4.3 (May 11, 2023) * Intentionally break multisig wallets that have heterogeneous master keys. Versions 4.4.0 to 4.4.2 of Electrum for Android did not check that master keys used the same script type. This may have resulted - in the creation of multisig wallets that that cannot be spent from + in the creation of multisig wallets that cannot be spent from with any existing version of Electrum. It is not sure whether any users are affected by this; if there are any, we will publish instructions on how to spend those coins (#8417, #8418). diff --git a/electrum/version.py b/electrum/version.py index 3135c1310..5351c94a8 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.4.3' # version of the client package -APK_VERSION = '4.4.3.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.4.4' # version of the client package +APK_VERSION = '4.4.4.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From ccc012674fc5707145aadf035440f6d63c9d5bbc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Jun 2023 17:34:32 +0000 Subject: [PATCH 0942/1143] unconditionally raise ImportError if asserts are disabled I have reconsidered and now think that we should always hard-fail if asserts asserts are disabled. It is just easier to reason about the code knowing that asserts are evaluated. If an end-user or library user has a concrete use case where this is a problem, please open an issue and let us know. follow-up 0f541be6f11a372d202c99476e6d051184006bba 0e5464ca13ce2f993107b4a293982ea4bfc434b5 --- electrum/__init__.py | 14 ++++++++++++-- run_electrum | 13 ------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/electrum/__init__.py b/electrum/__init__.py index f806c548b..f4c355028 100644 --- a/electrum/__init__.py +++ b/electrum/__init__.py @@ -35,5 +35,15 @@ class GuiImportError(ImportError): __version__ = ELECTRUM_VERSION _logger = get_logger(__name__) -if not __debug__: - _logger.warning(f"__debug__ is False. running with asserts disabled!") + + +# Ensure that asserts are enabled. For sanity and paranoia, we require this. +# Code *should not rely* on asserts being enabled. In particular, safety and security checks should +# always explicitly raise exceptions. However, this rule is mistakenly broken occasionally... +try: + assert False +except AssertionError: + pass +else: + raise ImportError("Running with asserts disabled. Refusing to continue. Exiting...") + diff --git a/run_electrum b/run_electrum index 36d9817b9..79eddd15f 100755 --- a/run_electrum +++ b/run_electrum @@ -63,19 +63,6 @@ if is_pyinstaller: # causes ImportErrors and other runtime failures). (see #4072) _file = open(sys.executable, 'rb') -if is_binary_distributable: - # Ensure that asserts are enabled. - # Code *should not rely* on asserts being enabled. In particular, safety and security checks should - # always explicitly raise exceptions. However, this rule is mistakenly broken occasionally... - # In case we are a binary build, we know for a fact that we want the asserts, so enforce them. - # When running from source, defer to the user. (a warning is logged in __init__.py) - try: - assert False - except AssertionError: - pass - else: - sys.exit("Error: Running with asserts disabled, in a binary distributable! Please check build settings.") - def check_imports(): # pure-python dependencies need to be imported here for pyinstaller From eba328202406fb95cedb25ca3ee0b96f3f253257 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 1 Jun 2023 22:59:40 +0000 Subject: [PATCH 0943/1143] fix flake8: follow-up prev --- electrum/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/__init__.py b/electrum/__init__.py index f4c355028..587889e2d 100644 --- a/electrum/__init__.py +++ b/electrum/__init__.py @@ -41,7 +41,7 @@ class GuiImportError(ImportError): # Code *should not rely* on asserts being enabled. In particular, safety and security checks should # always explicitly raise exceptions. However, this rule is mistakenly broken occasionally... try: - assert False + assert False # noqa: B011 except AssertionError: pass else: From 8cd95f1f7f1de783e55d2ddeae6d8d91033be52b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 15 May 2023 16:19:48 +0200 Subject: [PATCH 0944/1143] qml: limit BIP39 cosigners script type to initial choice (bip39) or initial seed (electrum) --- .../qml/components/wizard/WCBIP39Refine.qml | 7 ++++++- .../gui/qml/components/wizard/WCHaveSeed.qml | 9 +++++++++ electrum/gui/qml/qebitcoin.py | 6 +++--- electrum/wizard.py | 20 +++++++++++-------- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml index 647c23c33..7f2599c9c 100644 --- a/electrum/gui/qml/components/wizard/WCBIP39Refine.qml +++ b/electrum/gui/qml/components/wizard/WCBIP39Refine.qml @@ -129,19 +129,24 @@ WizardComponent { property string scripttype: 'p2sh' text: qsTr('legacy multisig (p2sh)') visible: isMultisig + enabled: !cosigner || wizard_data['script_type'] == 'p2sh' + checked: cosigner ? wizard_data['script_type'] == 'p2sh' : false } RadioButton { ButtonGroup.group: scripttypegroup property string scripttype: 'p2wsh-p2sh' text: qsTr('p2sh-segwit multisig (p2wsh-p2sh)') visible: isMultisig + enabled: !cosigner || wizard_data['script_type'] == 'p2wsh-p2sh' + checked: cosigner ? wizard_data['script_type'] == 'p2wsh-p2sh' : false } RadioButton { ButtonGroup.group: scripttypegroup property string scripttype: 'p2wsh' - checked: isMultisig text: qsTr('native segwit multisig (p2wsh)') visible: isMultisig + enabled: !cosigner || wizard_data['script_type'] == 'p2wsh' + checked: cosigner ? wizard_data['script_type'] == 'p2wsh' : isMultisig } InfoTextArea { diff --git a/electrum/gui/qml/components/wizard/WCHaveSeed.qml b/electrum/gui/qml/components/wizard/WCHaveSeed.qml index c8fa5c797..eb1998da0 100644 --- a/electrum/gui/qml/components/wizard/WCHaveSeed.qml +++ b/electrum/gui/qml/components/wizard/WCHaveSeed.qml @@ -31,6 +31,15 @@ WizardComponent { wizard_data['seed_type'] = bitcoin.seedType wizard_data['seed_extend'] = extendcb.checked wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' + + // determine script type from electrum seed type + // (used to limit script type options for bip39 cosigners) + if (wizard_data['wallet_type'] == 'multisig' && seed_variant_cb.currentValue == 'electrum') { + wizard_data['script_type'] = { + 'standard': 'p2sh', + 'segwit': 'p2wsh' + }[bitcoin.seedType] + } } } diff --git a/electrum/gui/qml/qebitcoin.py b/electrum/gui/qml/qebitcoin.py index 95897d39f..e3a7304da 100644 --- a/electrum/gui/qml/qebitcoin.py +++ b/electrum/gui/qml/qebitcoin.py @@ -30,15 +30,15 @@ def __init__(self, config, parent=None): self._validationMessage = '' self._words = None - @pyqtProperty('QString', notify=generatedSeedChanged) + @pyqtProperty(str, notify=generatedSeedChanged) def generatedSeed(self): return self._generated_seed - @pyqtProperty('QString', notify=seedTypeChanged) + @pyqtProperty(str, notify=seedTypeChanged) def seedType(self): return self._seed_type - @pyqtProperty('QString', notify=validationMessageChanged) + @pyqtProperty(str, notify=validationMessageChanged) def validationMessage(self): return self._validationMessage diff --git a/electrum/wizard.py b/electrum/wizard.py index 65ac64244..91a5801fe 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -275,7 +275,7 @@ def maybe_master_pubkey(self, wizard_data): self._logger.info('maybe_master_pubkey2') return - wizard_data['multisig_master_pubkey'] = self.keystore_from_data(wizard_data).get_master_public_key() + wizard_data['multisig_master_pubkey'] = self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key() def on_cosigner_keystore_type(self, wizard_data): t = wizard_data['cosigner_keystore_type'] @@ -308,10 +308,10 @@ def has_all_cosigner_data(self, wizard_data): def has_duplicate_masterkeys(self, wizard_data) -> bool: """Multisig wallets need distinct master keys. If True, need to prevent wallet-creation.""" xpubs = [] - xpubs.append(self.keystore_from_data(wizard_data).get_master_public_key()) + xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()) for cosigner in wizard_data['multisig_cosigner_data']: data = wizard_data['multisig_cosigner_data'][cosigner] - xpubs.append(self.keystore_from_data(data).get_master_public_key()) + xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], data).get_master_public_key()) assert xpubs return len(xpubs) != len(set(xpubs)) @@ -321,10 +321,10 @@ def has_heterogeneous_masterkeys(self, wizard_data) -> bool: If True, need to prevent wallet-creation. """ xpubs = [] - xpubs.append(self.keystore_from_data(wizard_data).get_master_public_key()) + xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], wizard_data).get_master_public_key()) for cosigner in wizard_data['multisig_cosigner_data']: data = wizard_data['multisig_cosigner_data'][cosigner] - xpubs.append(self.keystore_from_data(data).get_master_public_key()) + xpubs.append(self.keystore_from_data(wizard_data['wallet_type'], data).get_master_public_key()) assert xpubs try: k_xpub_type = xpub_type(xpubs[0]) @@ -339,14 +339,18 @@ def has_heterogeneous_masterkeys(self, wizard_data) -> bool: return True return False - def keystore_from_data(self, data): + def keystore_from_data(self, wallet_type, data): if 'seed' in data: if data['seed_variant'] == 'electrum': return keystore.from_seed(data['seed'], data['seed_extra_words'], True) elif data['seed_variant'] == 'bip39': root_seed = keystore.bip39_to_seed(data['seed'], data['seed_extra_words']) derivation = normalize_bip32_derivation(data['derivation_path']) - return keystore.from_bip43_rootseed(root_seed, derivation, xtype='p2wsh') + if wallet_type == 'multisig': + script = data['script_type'] if data['script_type'] != 'p2sh' else 'standard' + else: + script = data['script_type'] if data['script_type'] != 'p2pkh' else 'standard' + return keystore.from_bip43_rootseed(root_seed, derivation, xtype=script) else: raise Exception('Unsupported seed variant %s' % data['seed_variant']) elif 'master_key' in data: @@ -448,7 +452,7 @@ def create_storage(self, path, data): db.put('wallet_type', '%dof%d' % (data['multisig_signatures'],data['multisig_participants'])) db.put('x1/', k.dump()) for cosigner in data['multisig_cosigner_data']: - cosigner_keystore = self.keystore_from_data(data['multisig_cosigner_data'][cosigner]) + cosigner_keystore = self.keystore_from_data('multisig', data['multisig_cosigner_data'][cosigner]) if not isinstance(cosigner_keystore, keystore.Xpub): raise Exception(f"unexpected keystore(cosigner) type={type(cosigner_keystore)} in multisig. not bip32.") if k_xpub_type != xpub_type(cosigner_keystore.xpub): From 2a4d2ac0094fe546eef7588d50e1926ee278555a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 2 Jun 2023 13:35:51 +0200 Subject: [PATCH 0945/1143] payserver: fix import --- electrum/plugins/payserver/payserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index 9300741c1..b220c3cf5 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -146,7 +146,7 @@ async def get_request(self, r): return web.json_response(request) async def get_bip70_request(self, r): - from .paymentrequest import make_request + from electrum.paymentrequest import make_request key = r.match_info['key'] request = self.wallet.get_request(key) if not request: From a1c24c6261f8d22d9bc64ba7c44f3011147ef54f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Jun 2023 13:48:48 +0000 Subject: [PATCH 0946/1143] jade(hw): fix sign_transaction() same as 9e13246be845b4810f625a4986c03ec9d4b3c676 fixes https://github.com/spesmilo/electrum/issues/8463 regression from https://github.com/spesmilo/electrum/pull/8230 --- electrum/plugins/jade/jade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index 56b835a88..f4638eafd 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -299,7 +299,7 @@ def sign_transaction(self, tx, password): change[index] = {'path':path, 'variant': desc.to_legacy_electrum_script_type()} # The txn itself - txn_bytes = bytes.fromhex(tx.serialize_to_network()) + txn_bytes = bytes.fromhex(tx.serialize_to_network(include_sigs=False)) # Request Jade generate the signatures for our inputs. # Change details are passed to be validated on the hw (user does not confirm) From 3a1c4299cadf213f4f2ea0cf2b29b24ac011bf4d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 15:35:39 +0000 Subject: [PATCH 0947/1143] build: bump libusb version --- contrib/make_libusb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/make_libusb.sh b/contrib/make_libusb.sh index 7305fca13..a5fc6d34d 100755 --- a/contrib/make_libusb.sh +++ b/contrib/make_libusb.sh @@ -1,7 +1,7 @@ #!/bin/bash -LIBUSB_VERSION="c6a35c56016ea2ab2f19115d2ea1e85e0edae155" -# ^ tag v1.0.24 +LIBUSB_VERSION="4239bc3a50014b8e6a5a2a59df1fff3b7469543b" +# ^ tag v1.0.26 set -e From 66f219cdf387a6f84c2318b77e764629e4b0505f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 15:22:43 +0000 Subject: [PATCH 0948/1143] win build: update wine --- contrib/build-wine/Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/build-wine/Dockerfile b/contrib/build-wine/Dockerfile index 8fb6acf5c..cf002a0a8 100644 --- a/contrib/build-wine/Dockerfile +++ b/contrib/build-wine/Dockerfile @@ -47,12 +47,12 @@ RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \ apt-add-repository https://dl.winehq.org/wine-builds/debian/ && \ apt-get update -q && \ apt-get install -qy --allow-downgrades \ - wine-stable-amd64:amd64=7.0.0.0~bullseye-1 \ - wine-stable-i386:i386=7.0.0.0~bullseye-1 \ - wine-stable:amd64=7.0.0.0~bullseye-1 \ - winehq-stable:amd64=7.0.0.0~bullseye-1 \ - libvkd3d1:amd64=1.2~bullseye-1 \ - libvkd3d1:i386=1.2~bullseye-1 \ + wine-stable-amd64:amd64=8.0.1~bullseye-1 \ + wine-stable-i386:i386=8.0.1~bullseye-1 \ + wine-stable:amd64=8.0.1~bullseye-1 \ + winehq-stable:amd64=8.0.1~bullseye-1 \ + libvkd3d1:amd64=1.3~bullseye-1 \ + libvkd3d1:i386=1.3~bullseye-1 \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ From 21cf85afcaccd1d74bb8967374324ccf12c0bdc7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Jun 2023 15:02:51 +0000 Subject: [PATCH 0949/1143] win build: bump python version (3.9.13->3.10.11) --- contrib/build-wine/prepare-wine.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index b8f24a435..f4042f162 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -4,7 +4,7 @@ PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" PYINSTALLER_COMMIT="0fe956a2c6157e1b276819de1a050c242de70a29" # ^ latest commit from "v4" branch, somewhat after "4.10" tag -PYTHON_VERSION=3.9.13 +PYTHON_VERSION=3.10.11 # Let's begin! From 16c3c3c4c08e510499259e571e91ddf168d7768a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 15:34:18 +0000 Subject: [PATCH 0950/1143] appimage build: bump python version (3.9.15->3.10.11) --- contrib/build-linux/appimage/make_appimage.sh | 11 ++- ...> python-3.10-reproducible-buildinfo.diff} | 10 +- .../patches/python-3.10-reproducible-pyc.diff | 97 +++++++++++++++++++ 3 files changed, 110 insertions(+), 8 deletions(-) rename contrib/build-linux/appimage/patches/{python-3.9-reproducible-buildinfo.diff => python-3.10-reproducible-buildinfo.diff} (59%) create mode 100644 contrib/build-linux/appimage/patches/python-3.10-reproducible-pyc.diff diff --git a/contrib/build-linux/appimage/make_appimage.sh b/contrib/build-linux/appimage/make_appimage.sh index ec8c9aeb3..5acf46349 100755 --- a/contrib/build-linux/appimage/make_appimage.sh +++ b/contrib/build-linux/appimage/make_appimage.sh @@ -19,8 +19,8 @@ git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clo export GCC_STRIP_BINARIES="1" # pinned versions -PYTHON_VERSION=3.9.15 -PY_VER_MAJOR="3.9" # as it appears in fs paths +PYTHON_VERSION=3.10.11 +PY_VER_MAJOR="3.10" # as it appears in fs paths PKG2APPIMAGE_COMMIT="a9c85b7e61a3a883f4a35c41c5decb5af88b6b5d" VERSION=$(git describe --tags --dirty --always) @@ -41,7 +41,7 @@ download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/AppImage/AppI verify_hash "$CACHEDIR/appimagetool" "df3baf5ca5facbecfc2f3fa6713c29ab9cefa8fd8c1eac5d283b79cab33e4acb" download_if_not_exist "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz" -verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "12daff6809528d9f6154216950423c9e30f0e47336cb57c6aa0b4387dd5eb4b2" +verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "3c3bc3048303721c904a03eb8326b631e921f11cc3be2988456a42f115daf04c" @@ -55,8 +55,9 @@ tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$CACHEDIR" cd "$CACHEDIR/Python-$PYTHON_VERSION" LC_ALL=C export BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%b %d %Y") LC_ALL=C export BUILD_TIME=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%H:%M:%S") - # Patch taken from Ubuntu http://archive.ubuntu.com/ubuntu/pool/main/p/python3.9/python3.9_3.9.5-3~21.04.debian.tar.xz - patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.9-reproducible-buildinfo.diff" + # Patches taken from Ubuntu http://archive.ubuntu.com/ubuntu/pool/main/p/python3.10/python3.10_3.10.7-1ubuntu0.3.debian.tar.xz + patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.10-reproducible-buildinfo.diff" + patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.10-reproducible-pyc.diff" ./configure \ --cache-file="$CACHEDIR/python.config.cache" \ --prefix="$APPDIR/usr" \ diff --git a/contrib/build-linux/appimage/patches/python-3.9-reproducible-buildinfo.diff b/contrib/build-linux/appimage/patches/python-3.10-reproducible-buildinfo.diff similarity index 59% rename from contrib/build-linux/appimage/patches/python-3.9-reproducible-buildinfo.diff rename to contrib/build-linux/appimage/patches/python-3.10-reproducible-buildinfo.diff index 16693a157..4f3063abd 100644 --- a/contrib/build-linux/appimage/patches/python-3.9-reproducible-buildinfo.diff +++ b/contrib/build-linux/appimage/patches/python-3.10-reproducible-buildinfo.diff @@ -1,13 +1,17 @@ -# DP: Build getbuildinfo.o with DATE/TIME values when defined +Description: Build reproduceable date and time into build info + Build information is encoded into getbuildinfo.o at build time. + Use the date and time from the debian changelog, to make this reproduceable. + +Forwarded: no --- a/Makefile.pre.in +++ b/Makefile.pre.in -@@ -795,6 +795,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \ +@@ -796,6 +796,8 @@ Modules/getbuildinfo.o: $(PARSER_OBJS) \ -DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \ -DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \ -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ + $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \ + $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \ -o $@ $(srcdir)/Modules/getbuildinfo.c - + Modules/getpath.o: $(srcdir)/Modules/getpath.c Makefile diff --git a/contrib/build-linux/appimage/patches/python-3.10-reproducible-pyc.diff b/contrib/build-linux/appimage/patches/python-3.10-reproducible-pyc.diff new file mode 100644 index 000000000..456cb33a3 --- /dev/null +++ b/contrib/build-linux/appimage/patches/python-3.10-reproducible-pyc.diff @@ -0,0 +1,97 @@ +From 36ae9beb04763d498df2114657bfbbcfe58bf913 Mon Sep 17 00:00:00 2001 +From: Brandt Bucher +Date: Mon, 23 Aug 2021 18:34:17 -0700 +Subject: [PATCH] Serialize frozenset elements deterministically + +--- + Lib/test/test_marshal.py | 25 +++++++++++++++ + .../2021-08-23-21-39-59.bpo-37596.ojRcwB.rst | 2 ++ + Python/marshal.c | 32 +++++++++++++++++++ + 3 files changed, 59 insertions(+) + create mode 100644 Misc/NEWS.d/next/Library/2021-08-23-21-39-59.bpo-37596.ojRcwB.rst + +--- a/Lib/test/test_marshal.py ++++ b/Lib/test/test_marshal.py +@@ -1,5 +1,6 @@ + from test import support + from test.support import os_helper ++from test.support.script_helper import assert_python_ok + import array + import io + import marshal +@@ -318,6 +319,31 @@ class BugsTestCase(unittest.TestCase): + for i in range(len(data)): + self.assertRaises(EOFError, marshal.loads, data[0: i]) + ++ def test_deterministic_sets(self): ++ # bpo-37596: To support reproducible builds, sets and frozensets need to ++ # have their elements serialized in a consistent order (even when they ++ # have been scrambled by hash randomization): ++ for kind in ("set", "frozenset"): ++ for elements in ( ++ "float('nan'), b'a', b'b', b'c', 'x', 'y', 'z'", ++ # Also test for bad interactions with backreferencing: ++ "('string', 1), ('string', 2), ('string', 3)", ++ ): ++ s = f"{kind}([{elements}])" ++ with self.subTest(s): ++ # First, make sure that our test case still has different ++ # orders under hash seeds 0 and 1. If this check fails, we ++ # need to update this test with different elements: ++ args = ["-c", f"print({s})"] ++ _, repr_0, _ = assert_python_ok(*args, PYTHONHASHSEED="0") ++ _, repr_1, _ = assert_python_ok(*args, PYTHONHASHSEED="1") ++ self.assertNotEqual(repr_0, repr_1) ++ # Then, perform the actual test: ++ args = ["-c", f"import marshal; print(marshal.dumps({s}))"] ++ _, dump_0, _ = assert_python_ok(*args, PYTHONHASHSEED="0") ++ _, dump_1, _ = assert_python_ok(*args, PYTHONHASHSEED="1") ++ self.assertEqual(dump_0, dump_1) ++ + LARGE_SIZE = 2**31 + pointer_size = 8 if sys.maxsize > 0xFFFFFFFF else 4 + +--- a/Python/marshal.c ++++ b/Python/marshal.c +@@ -502,9 +502,41 @@ w_complex_object(PyObject *v, char flag, + W_TYPE(TYPE_SET, p); + n = PySet_GET_SIZE(v); + W_SIZE(n, p); ++ // bpo-37596: To support reproducible builds, sets and frozensets need ++ // to have their elements serialized in a consistent order (even when ++ // they have been scrambled by hash randomization). To ensure this, we ++ // use an order equivalent to sorted(v, key=marshal.dumps): ++ PyObject *pairs = PyList_New(0); ++ if (pairs == NULL) { ++ p->error = WFERR_NOMEMORY; ++ return; ++ } + while (_PySet_NextEntry(v, &pos, &value, &hash)) { ++ PyObject *dump = PyMarshal_WriteObjectToString(value, p->version); ++ if (dump == NULL) { ++ p->error = WFERR_UNMARSHALLABLE; ++ goto anyset_done; ++ } ++ PyObject *pair = PyTuple_Pack(2, dump, value); ++ Py_DECREF(dump); ++ if (pair == NULL || PyList_Append(pairs, pair)) { ++ p->error = WFERR_NOMEMORY; ++ Py_XDECREF(pair); ++ goto anyset_done; ++ } ++ Py_DECREF(pair); ++ } ++ if (PyList_Sort(pairs)) { ++ p->error = WFERR_NOMEMORY; ++ goto anyset_done; ++ } ++ for (Py_ssize_t i = 0; i < n; i++) { ++ PyObject *pair = PyList_GET_ITEM(pairs, i); ++ value = PyTuple_GET_ITEM(pair, 1); + w_object(value, p); + } ++ anyset_done: ++ Py_DECREF(pairs); + } + else if (PyCode_Check(v)) { + PyCodeObject *co = (PyCodeObject *)v; From 6442dbb02173fba652b7abd7c79735cf823f0c85 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 15:31:24 +0000 Subject: [PATCH 0951/1143] mac build: bump python version (3.9.13->3.10.11) --- contrib/osx/make_osx.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index 3f5f0e03d..aedabdda8 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -3,8 +3,8 @@ set -e # Parameterize -PYTHON_VERSION=3.9.13 -PY_VER_MAJOR="3.9" # as it appears in fs paths +PYTHON_VERSION=3.10.11 +PY_VER_MAJOR="3.10" # as it appears in fs paths PACKAGE=Electrum GIT_REPO=https://github.com/spesmilo/electrum @@ -71,11 +71,11 @@ function DoCodeSignMaybe { # ARGS: infoName fileOrDirName } info "Installing Python $PYTHON_VERSION" -PKG_FILE="python-${PYTHON_VERSION}-macosx10.9.pkg" +PKG_FILE="python-${PYTHON_VERSION}-macos11.pkg" if [ ! -f "$CACHEDIR/$PKG_FILE" ]; then curl -o "$CACHEDIR/$PKG_FILE" "https://www.python.org/ftp/python/${PYTHON_VERSION}/$PKG_FILE" fi -echo "167c4e2d9f172a617ba6f3b08783cf376dec429386378066eb2f865c98030dd7 $CACHEDIR/$PKG_FILE" | shasum -a 256 -c \ +echo "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc $CACHEDIR/$PKG_FILE" | shasum -a 256 -c \ || fail "python pkg checksum mismatched" sudo installer -pkg "$CACHEDIR/$PKG_FILE" -target / \ || fail "failed to install python" From 9b1fb0e5fed402c81e01185cc79ec1a8722d173d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 15:39:37 +0000 Subject: [PATCH 0952/1143] android build: bump python, openssl --- contrib/android/p4a_recipes/hostpython3/__init__.py | 4 ++-- contrib/android/p4a_recipes/openssl/__init__.py | 4 ++-- contrib/android/p4a_recipes/python3/__init__.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/android/p4a_recipes/hostpython3/__init__.py b/contrib/android/p4a_recipes/hostpython3/__init__.py index 5a020a279..69dee19a3 100644 --- a/contrib/android/p4a_recipes/hostpython3/__init__.py +++ b/contrib/android/p4a_recipes/hostpython3/__init__.py @@ -11,8 +11,8 @@ class HostPython3RecipePinned(util.InheritedRecipeMixin, HostPython3Recipe): - version = "3.8.15" - sha512sum = "625af8fa23e7a7daba6302d147ccf80da36b8b5cce7a99976583bf8e07f1ca71c11529b6286e7732d69c00398dfa8422243f33641e2164e4299237663103ae99" + version = "3.8.16" + sha512sum = "59940a0f646e9ec320c3ee40b1a960da6418e4365ba05c179f36235a3a50fd151ddd5f5d295c40ab291a9e7cb760abe1f61511a2460336f08189297d1c22f09c" recipe = HostPython3RecipePinned() diff --git a/contrib/android/p4a_recipes/openssl/__init__.py b/contrib/android/p4a_recipes/openssl/__init__.py index ae022020f..3cc89d8a8 100644 --- a/contrib/android/p4a_recipes/openssl/__init__.py +++ b/contrib/android/p4a_recipes/openssl/__init__.py @@ -11,8 +11,8 @@ class OpenSSLRecipePinned(util.InheritedRecipeMixin, OpenSSLRecipe): - url_version = "1.1.1s" - sha512sum = "2ef983f166b5e1bf456ca37938e7e39d58d4cd85e9fc4b5174a05f5c37cc5ad89c3a9af97a6919bcaab128a8a92e4bdc8a045e5d9156d90768da8f73ac67c5b9" + url_version = "1.1.1t" + sha512sum = "628676c9c3bc1cf46083d64f61943079f97f0eefd0264042e40a85dbbd988f271bfe01cd1135d22cc3f67a298f1d078041f8f2e97b0da0d93fe172da573da18c" recipe = OpenSSLRecipePinned() diff --git a/contrib/android/p4a_recipes/python3/__init__.py b/contrib/android/p4a_recipes/python3/__init__.py index 382d178c1..cad02a7a1 100644 --- a/contrib/android/p4a_recipes/python3/__init__.py +++ b/contrib/android/p4a_recipes/python3/__init__.py @@ -11,8 +11,8 @@ class Python3RecipePinned(util.InheritedRecipeMixin, Python3Recipe): - version = "3.8.15" - sha512sum = "625af8fa23e7a7daba6302d147ccf80da36b8b5cce7a99976583bf8e07f1ca71c11529b6286e7732d69c00398dfa8422243f33641e2164e4299237663103ae99" + version = "3.8.16" + sha512sum = "59940a0f646e9ec320c3ee40b1a960da6418e4365ba05c179f36235a3a50fd151ddd5f5d295c40ab291a9e7cb760abe1f61511a2460336f08189297d1c22f09c" recipe = Python3RecipePinned() From 98f240d6cd1be2be854e11e7eb244ea4a09ee8d7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Jun 2023 21:47:04 +0000 Subject: [PATCH 0953/1143] commands: version_info: include python version/path --- electrum/commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/commands.py b/electrum/commands.py index 336e69a47..4dfa7db6b 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -585,6 +585,8 @@ async def version_info(self): ret = { "electrum.version": ELECTRUM_VERSION, "electrum.path": os.path.dirname(os.path.realpath(__file__)), + "python.version": sys.version, + "python.path": sys.executable, } # add currently running GUI if self.daemon and self.daemon.gui_object: From e455677284e2bb0f0e6b2bbcc38815aae89323e2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 May 2023 15:27:53 +0000 Subject: [PATCH 0954/1143] win build: bump pyinstaller (4.10->5.11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In `build-electrum-git.sh`, the `-w` CLI arg is removed: it was apparently ignored as we are using a .spec file, and pyinstaller 5.0+ is now raising a hard error (see https://github.com/pyinstaller/pyinstaller/issues/6660). ``` option(s) not allowed: --console/--nowindowed/--windowed/--noconsole makespec options not valid when a .spec file is given ``` ----- In ecc_fast.py, we don't sys.exit() anymore as pyinstaller 5.0+ tries to import electrum during the Analysis phase. see https://github.com/pyinstaller/pyinstaller/pull/6171 ``` 57912 INFO: Looking for dynamic libraries 1746 INFO: gettext setting initial language to None 1932 ERROR: libsecp256k1 library failed to load. exceptions: [FileNotFoundError("Could not find module 'C:\\python3\\lib\\site-packages\\electrum\\libsecp256k1-2.dll' (or one of its depende ncies). Try using the full path with constructor syntax."), FileNotFoundError("Could not find module 'C:\\python3\\lib\\site-packages\\electrum\\libsecp256k1-1.dll' (or one of its dependenc ies). Try using the full path with constructor syntax."), FileNotFoundError("Could not find module 'C:\\python3\\lib\\site-packages\\electrum\\libsecp256k1-0.dll' (or one of its dependencie s). Try using the full path with constructor syntax."), FileNotFoundError("Could not find module 'libsecp256k1-2.dll' (or one of its dependencies). Try using the full path with constructor syntax."), FileNotFoundError("Could not find module 'libsecp256k1-1.dll' (or one of its dependencies). Try using the full path with constructor syntax."), FileNotFoundError("Could not find module 'libsecp256k1-0.dll' (or one of its dependencies). Try using the full path with constructor syntax.")] Traceback (most recent call last): File "C:\python3\lib\runpy.py", line 196, in _run_module_as_main return _run_code(code, main_globals, None, File "C:\python3\lib\runpy.py", line 86, in _run_code exec(code, run_globals) File "C:\python3\scripts\pyinstaller.exe\__main__.py", line 7, in File "C:\python3\lib\site-packages\PyInstaller\__main__.py", line 194, in _console_script_run run() File "C:\python3\lib\site-packages\PyInstaller\__main__.py", line 180, in run run_build(pyi_config, spec_file, **vars(args)) File "C:\python3\lib\site-packages\PyInstaller\__main__.py", line 61, in run_build PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs) File "C:\python3\lib\site-packages\PyInstaller\building\build_main.py", line 1006, in main build(specfile, distpath, workpath, clean_build) File "C:\python3\lib\site-packages\PyInstaller\building\build_main.py", line 928, in build exec(code, spec_namespace) File "deterministic.spec", line 55, in a = Analysis([home+'run_electrum', File "C:\python3\lib\site-packages\PyInstaller\building\build_main.py", line 428, in __init__ self.__postinit__() File "C:\python3\lib\site-packages\PyInstaller\building\datastruct.py", line 184, in __postinit__ self.assemble() File "C:\python3\lib\site-packages\PyInstaller\building\build_main.py", line 736, in assemble isolated.call(find_binary_dependencies, self.binaries, self.binding_redirects, collected_packages) File "C:\python3\lib\site-packages\PyInstaller\isolated\_parent.py", line 372, in call return isolated.call(function, *args, **kwargs) File "C:\python3\lib\site-packages\PyInstaller\isolated\_parent.py", line 302, in call raise RuntimeError(f"Child process call to {function.__name__}() failed with:\n" + output) RuntimeError: Child process call to find_binary_dependencies() failed with: File "C:\python3\lib\site-packages\PyInstaller\isolated\_child.py", line 63, in run_next_command output = function(*args, **kwargs) File "C:\python3\lib\site-packages\PyInstaller\building\build_main.py", line 177, in find_binary_dependencies __import__(package) File "C:\python3\lib\site-packages\electrum\__init__.py", line 20, in from .wallet import Wallet File "C:\python3\lib\site-packages\electrum\wallet.py", line 53, in from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath File "C:\python3\lib\site-packages\electrum\bip32.py", line 11, in from . import constants File "C:\python3\lib\site-packages\electrum\constants.py", line 30, in from . import bitcoin File "C:\python3\lib\site-packages\electrum\bitcoin.py", line 35, in from . import ecc File "C:\python3\lib\site-packages\electrum\ecc.py", line 39, in from .ecc_fast import _libsecp256k1, SECP256K1_EC_UNCOMPRESSED File "C:\python3\lib\site-packages\electrum\ecc_fast.py", line 151, in sys.exit(f"Error: Failed to load libsecp256k1.") SystemExit: Error: Failed to load libsecp256k1. 🗯 ERROR: build-electrum-git failed ``` Also, the -OO flag is removed from wine python, for similar reasons: pyinstaller imports electrum, and in electrum/__init__.py, we raise if -O is used: https://github.com/spesmilo/electrum/blob/9b1fb0e5fed402c81e01185cc79ec1a8722d173d/electrum/__init__.py#L40 --- contrib/build-wine/build-electrum-git.sh | 2 +- contrib/build-wine/make_win.sh | 2 +- contrib/build-wine/prepare-wine.sh | 8 ++++---- electrum/ecc_fast.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index 07f610f18..cce7e38e9 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -56,7 +56,7 @@ rm -rf dist/ # build standalone and portable versions info "Running pyinstaller..." -ELECTRUM_CMDLINE_NAME="$NAME_ROOT-$VERSION" wine "$WINE_PYHOME/scripts/pyinstaller.exe" --noconfirm --ascii --clean -w deterministic.spec +ELECTRUM_CMDLINE_NAME="$NAME_ROOT-$VERSION" wine "$WINE_PYHOME/scripts/pyinstaller.exe" --noconfirm --ascii --clean deterministic.spec # set timestamps in dist, in order to make the installer reproducible pushd dist diff --git a/contrib/build-wine/make_win.sh b/contrib/build-wine/make_win.sh index 6cda8dcab..dc3151254 100755 --- a/contrib/build-wine/make_win.sh +++ b/contrib/build-wine/make_win.sh @@ -31,7 +31,7 @@ export DLL_TARGET_DIR="$CACHEDIR/dlls" export WINEPREFIX="/opt/wine64" export WINEDEBUG=-all export WINE_PYHOME="c:/python3" -export WINE_PYTHON="wine $WINE_PYHOME/python.exe -OO -B" +export WINE_PYTHON="wine $WINE_PYHOME/python.exe -B" . "$CONTRIB"/build_tools_util.sh diff --git a/contrib/build-wine/prepare-wine.sh b/contrib/build-wine/prepare-wine.sh index f4042f162..a65945983 100755 --- a/contrib/build-wine/prepare-wine.sh +++ b/contrib/build-wine/prepare-wine.sh @@ -1,8 +1,8 @@ #!/bin/bash PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" -PYINSTALLER_COMMIT="0fe956a2c6157e1b276819de1a050c242de70a29" -# ^ latest commit from "v4" branch, somewhat after "4.10" tag +PYINSTALLER_COMMIT="413cce49ff28d87fad4472f4953489226ec90c84" +# ^ tag "v5.11.0" PYTHON_VERSION=3.10.11 @@ -69,7 +69,7 @@ info "Building PyInstaller." else fail "unexpected WIN_ARCH: $WIN_ARCH" fi - if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Windows-$PYINST_ARCH/runw.exe" ]; then + if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe" ]; then info "pyinstaller already built, skipping" exit 0 fi @@ -94,7 +94,7 @@ info "Building PyInstaller." CFLAGS="-static" popd # sanity check bootloader is there: - [[ -e "PyInstaller/bootloader/Windows-$PYINST_ARCH/runw.exe" ]] || fail "Could not find runw.exe in target dir!" + [[ -e "PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe" ]] || fail "Could not find runw.exe in target dir!" ) || fail "PyInstaller build failed" info "Installing PyInstaller." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location ./pyinstaller diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index ba75498f2..a27e21149 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -148,7 +148,7 @@ def load_library(): if _libsecp256k1 is None: # hard fail: - sys.exit(f"Error: Failed to load libsecp256k1.") + raise ImportError("Failed to load libsecp256k1") def version_info() -> dict: From 48a831184913137ef5cd55991c6bc769ed0bd40f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Jun 2023 22:43:36 +0000 Subject: [PATCH 0955/1143] mac build: bump pyinstaller (5.3->5.11) --- contrib/osx/make_osx.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index aedabdda8..254eb3ccb 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -122,11 +122,8 @@ brew install autoconf automake libtool gettext coreutils pkgconfig info "Building PyInstaller." PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" -PYINSTALLER_COMMIT="fbf7948be85177dd44b41217e9f039e1d176de6b" -# ^ tag "5.3" -# TODO test newer versions of pyinstaller for build-reproducibility. -# we are using this version for now due to change in code-signing behaviour -# (https://github.com/pyinstaller/pyinstaller/pull/5581) +PYINSTALLER_COMMIT="413cce49ff28d87fad4472f4953489226ec90c84" +# ^ tag "v5.11.0" ( if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Darwin-64bit/runw" ]; then info "pyinstaller already built, skipping" From 9b14b87936778eb15de4a46079ecf6a53bffd897 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 2 Jun 2023 23:12:36 +0000 Subject: [PATCH 0956/1143] win build: tighten `pip install` with `--no-binary` somewhat related https://github.com/spesmilo/electrum/pull/7918 --- contrib/build-wine/build-electrum-git.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contrib/build-wine/build-electrum-git.sh b/contrib/build-wine/build-electrum-git.sh index cce7e38e9..d772ffc12 100755 --- a/contrib/build-wine/build-electrum-git.sh +++ b/contrib/build-wine/build-electrum-git.sh @@ -37,12 +37,13 @@ info "Installing requirements..." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements.txt info "Installing dependencies specific to binaries..." -# TODO use "--no-binary :all:" (but we don't have a C compiler...) +# TODO tighten "--no-binary :all:" (but we don't have a C compiler...) $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ + --no-binary :all: --only-binary cffi,cryptography,PyQt5,PyQt5-Qt5,PyQt5-sip \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-binaries.txt info "Installing hardware wallet requirements..." -# TODO use "--no-binary :all:" (but we don't have a C compiler...) $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ + --no-binary :all: --only-binary cffi,cryptography,hidapi \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-hw.txt pushd $WINEPREFIX/drive_c/electrum From 033ad0feb9baf4dd080fadd784bad2bf8fd1e92b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 4 Jun 2023 03:07:06 +0000 Subject: [PATCH 0957/1143] lnworker: fix rebalance_channels fixes https://github.com/spesmilo/electrum/issues/8468 --- electrum/commands.py | 6 +++++- electrum/lnworker.py | 11 ++++++----- electrum/submarine_swaps.py | 2 +- electrum/wallet.py | 4 +++- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 4dfa7db6b..e70b66e35 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1269,7 +1269,11 @@ async def rebalance_channels(self, from_scid, dest_scid, amount, wallet: Abstrac from_channel = wallet.lnworker.get_channel_by_scid(from_scid) dest_channel = wallet.lnworker.get_channel_by_scid(dest_scid) amount_sat = satoshis(amount) - success, log = await wallet.lnworker.rebalance_channels(from_channel, dest_channel, amount_sat * 1000) + success, log = await wallet.lnworker.rebalance_channels( + from_channel, + dest_channel, + amount_msat=amount_sat * 1000, + ) return { 'success': success, 'log': [x.formatted_tuple() for x in log] diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 563b66a0b..07adf9ecd 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1842,8 +1842,7 @@ def get_bolt11_invoice( self._bolt11_cache[payment_hash] = pair return pair - def create_payment_info(self, amount_sat: Optional[int], write_to_disk=True) -> bytes: - amount_msat = amount_sat * 1000 if amount_sat else None + def create_payment_info(self, *, amount_msat: Optional[int], write_to_disk=True) -> bytes: payment_preimage = os.urandom(32) payment_hash = sha256(payment_preimage) info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) @@ -2289,17 +2288,19 @@ def suggest_swap_to_receive(self, amount_sat): for chan, swap_recv_amount in suggestions: return (chan, swap_recv_amount) - async def rebalance_channels(self, chan1, chan2, amount_msat): + async def rebalance_channels(self, chan1: Channel, chan2: Channel, *, amount_msat: int): if chan1 == chan2: raise Exception('Rebalance requires two different channels') if self.uses_trampoline() and chan1.node_id == chan2.node_id: raise Exception('Rebalance requires channels from different trampolines') - lnaddr, invoice = self.add_reqest( + payment_hash = self.create_payment_info(amount_msat=amount_msat) + lnaddr, invoice = self.get_bolt11_invoice( + payment_hash=payment_hash, amount_msat=amount_msat, message='rebalance', expiry=3600, fallback_address=None, - channels = [chan2] + channels=[chan2], ) return await self.pay_invoice( invoice, channels=[chan1]) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 43e11951e..a7f966e7e 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -262,7 +262,7 @@ async def normal_swap( privkey = os.urandom(32) pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) amount_msat = lightning_amount_sat * 1000 - payment_hash = self.lnworker.create_payment_info(lightning_amount_sat) + payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat) lnaddr, invoice = self.lnworker.get_bolt11_invoice( payment_hash=payment_hash, amount_msat=amount_msat, diff --git a/electrum/wallet.py b/electrum/wallet.py index f2efbc441..6d17e755d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2625,7 +2625,9 @@ def create_request(self, amount_sat: int, message: str, exp_delay: int, address: address = address or None # converts "" to None exp_delay = exp_delay or 0 timestamp = int(Request._get_cur_time()) - payment_hash = self.lnworker.create_payment_info(amount_sat, write_to_disk=False) if self.has_lightning() else None + payment_hash = None # type: Optional[bytes] + if self.has_lightning(): + payment_hash = self.lnworker.create_payment_info(amount_msat=amount_sat * 1000, write_to_disk=False) outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] height = self.adb.get_local_height() req = Request( From 3ab47e1c457721294c00ec4300f7d2e05e948c64 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 5 Jun 2023 15:29:35 +0000 Subject: [PATCH 0958/1143] (trivial) convert more config keys --- run_electrum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_electrum b/run_electrum index 79eddd15f..d388de169 100755 --- a/run_electrum +++ b/run_electrum @@ -338,8 +338,8 @@ def main(): config_options = args.__dict__ f = lambda key: config_options[key] is not None and key not in config_variables.get(args.cmd, {}).keys() config_options = {key: config_options[key] for key in filter(f, config_options.keys())} - if config_options.get('server'): - config_options['auto_connect'] = False + if config_options.get(SimpleConfig.NETWORK_SERVER.key()): + config_options[SimpleConfig.NETWORK_AUTO_CONNECT.key()] = False config_options['cwd'] = cwd = os.getcwd() From cdab59f620c910c3fc0745b80e4a13277c04569f Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 11 Jun 2023 23:01:53 +0200 Subject: [PATCH 0959/1143] :sparkles: remove thousand separator when copying numbers to clipboard from contextual menus --- electrum/gui/qt/main_window.py | 15 +++++++++++++-- electrum/gui/qt/my_treeview.py | 8 ++++++++ electrum/gui/qt/transaction_dialog.py | 4 ++-- electrum/simple_config.py | 3 ++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2eefc1ec8..b6f9ef4e6 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -852,11 +852,22 @@ def timer_actions(self): self.send_tab.payto_e.on_timer_check_text() self.notify_transactions() - def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str: + def format_amount( + self, + amount_sat, + is_diff=False, + whitespaces=False, + ignore_thousands_sep: bool = False, + ) -> str: """Formats amount as string, converting to desired unit. E.g. 500_000 -> '0.005' """ - return self.config.format_amount(amount_sat, is_diff=is_diff, whitespaces=whitespaces) + return self.config.format_amount( + amount_sat, + is_diff=is_diff, + whitespaces=whitespaces, + ignore_thousands_sep=ignore_thousands_sep, + ) def format_amount_and_units(self, amount_sat, *, timestamp: int = None) -> str: """Returns string with both bitcoin and fiat amounts, in desired units. diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index da79f9554..d617d3eb2 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -24,6 +24,7 @@ # SOFTWARE. import asyncio +import contextlib import enum import os.path import time @@ -451,6 +452,13 @@ def add_copy_menu(self, menu: QMenu, idx) -> QMenu: return cc def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: + if title in { + "Amount", + "Balance", + } or title.endswith(" Value") or title.endswith(" Acquisition price") or title.endswith(" Capital Gains"): + with contextlib.suppress(Exception): + # remove formatting for numbers + text = text.replace(" ", "") self.main_window.do_copy(text, title=title) def showEvent(self, e: 'QShowEvent'): diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index b9e5bda8e..59a43bf36 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -314,7 +314,7 @@ def on_context_menu_for_inputs(self, pos: QPoint): copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))] txin_value = self.wallet.adb.get_txin_value(txin) if txin_value: - value_str = self.main_window.format_amount(txin_value) + value_str = self.main_window.format_amount(txin_value, ignore_thousands_sep=True) copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))] for item in show_list: @@ -356,7 +356,7 @@ def on_context_menu_for_outputs(self, pos: QPoint): show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))] copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))] txout_value = self.tx.outputs()[txout_idx].value - value_str = self.main_window.format_amount(txout_value) + value_str = self.main_window.format_amount(txout_value, ignore_thousands_sep=True) copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))] for item in show_list: diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 41ad1b47f..91b6d940d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -785,6 +785,7 @@ def format_amount( is_diff=False, whitespaces=False, precision=None, + ignore_thousands_sep: bool=False, ) -> str: if precision is None: precision = self.amt_precision_post_satoshi @@ -795,7 +796,7 @@ def format_amount( is_diff=is_diff, whitespaces=whitespaces, precision=precision, - add_thousands_sep=self.amt_add_thousands_sep, + add_thousands_sep=False if ignore_thousands_sep else self.amt_add_thousands_sep, ) def format_amount_and_units(self, *args, **kwargs) -> str: From 1b0747280587040ff6d94b5bd95920a6c18c9e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20L=C3=89VEIL?= Date: Tue, 13 Jun 2023 00:39:31 +0200 Subject: [PATCH 0960/1143] :bug: fix #8469 - fiat balance sorting (#8478) in address list window --- electrum/gui/qt/address_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index 51effed92..ef9545fab 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -262,6 +262,7 @@ def refresh_row(self, key, row): address_item[self.Columns.COIN_BALANCE].setText(balance_text) address_item[self.Columns.COIN_BALANCE].setData(balance, self.ROLE_SORT_ORDER) address_item[self.Columns.FIAT_BALANCE].setText(fiat_balance_str) + address_item[self.Columns.FIAT_BALANCE].setData(balance, self.ROLE_SORT_ORDER) address_item[self.Columns.NUM_TXS].setText("%d"%num) c = ColorScheme.BLUE.as_color(True) if self.wallet.is_frozen_address(address) else self._default_bg_brush address_item[self.Columns.ADDRESS].setBackground(c) From 703ec0935540afe07c30b71be5517a974a721bc3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 13 Jun 2023 00:24:56 +0000 Subject: [PATCH 0961/1143] addr_sync: expand docstring for get_tx_fee --- electrum/address_synchronizer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index b42be0474..bfb46b324 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -742,7 +742,14 @@ def get_tx_delta(self, tx_hash: str, address: str) -> int: return delta def get_tx_fee(self, txid: str) -> Optional[int]: - """ Returns tx_fee or None. Use server fee only if tx is unconfirmed and not mine""" + """Returns tx_fee or None. Use server fee only if tx is unconfirmed and not mine. + + Note: being fast is prioritised over completeness here. We try to avoid deserializing + the tx, as that is expensive if we are called for the whole history. We sometimes + incorrectly early-exit and return None, e.g. for not-all-ismine-input txs, + where we could calculate the fee if we deserialized (but to see if we have all + the parent txs available, we would have to deserialize first). + """ # check if stored fee is available fee = self.db.get_tx_fee(txid, trust_server=False) if fee is not None: From 23f2412da71940b5283fe3cb0b29683290129298 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 13 Jun 2023 15:59:18 +0000 Subject: [PATCH 0962/1143] qt: follow-up "rm thousand sep when copying numbers to clipboard" follow-up https://github.com/spesmilo/electrum/pull/8479 --- electrum/exchange_rate.py | 11 +++++++---- electrum/gui/qt/address_list.py | 7 ++++++- electrum/gui/qt/history_list.py | 25 +++++++++++++++++-------- electrum/gui/qt/invoice_list.py | 2 ++ electrum/gui/qt/main_window.py | 5 +++-- electrum/gui/qt/my_treeview.py | 7 ------- electrum/gui/qt/request_list.py | 2 ++ electrum/gui/qt/transaction_dialog.py | 4 ++-- electrum/gui/qt/utxo_list.py | 7 ++++++- electrum/simple_config.py | 6 ++++-- 10 files changed, 49 insertions(+), 27 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index e3bdcc79e..801ce47a8 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -651,13 +651,16 @@ def get_fiat_status_text(self, btc_balance, base_unit, decimal_point): def fiat_value(self, satoshis, rate) -> Decimal: return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate) - def value_str(self, satoshis, rate) -> str: - return self.format_fiat(self.fiat_value(satoshis, rate)) + def value_str(self, satoshis, rate, *, add_thousands_sep: bool = None) -> str: + fiat_val = self.fiat_value(satoshis, rate) + return self.format_fiat(fiat_val, add_thousands_sep=add_thousands_sep) - def format_fiat(self, value: Decimal) -> str: + def format_fiat(self, value: Decimal, *, add_thousands_sep: bool = None) -> str: if value.is_nan(): return _("No data") - return self.ccy_amount_str(value, add_thousands_sep=True) + if add_thousands_sep is None: + add_thousands_sep = True + return self.ccy_amount_str(value, add_thousands_sep=add_thousands_sep) def history_rate(self, d_t: Optional[datetime]) -> Decimal: if d_t is None: diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py index ef9545fab..470448837 100644 --- a/electrum/gui/qt/address_list.py +++ b/electrum/gui/qt/address_list.py @@ -250,19 +250,24 @@ def refresh_row(self, key, row): c, u, x = self.wallet.get_addr_balance(address) balance = c + u + x balance_text = self.main_window.format_amount(balance, whitespaces=True) + balance_text_nots = self.main_window.format_amount(balance, whitespaces=False, add_thousands_sep=False) # create item fx = self.main_window.fx if self.should_show_fiat(): rate = fx.exchange_rate() - fiat_balance_str = fx.value_str(balance, rate) + fiat_balance_str = fx.value_str(balance, rate, add_thousands_sep=True) + fiat_balance_str_nots = fx.value_str(balance, rate, add_thousands_sep=False) else: fiat_balance_str = '' + fiat_balance_str_nots = '' address_item = [self.std_model.item(row, col) for col in self.Columns] address_item[self.Columns.LABEL].setText(label) address_item[self.Columns.COIN_BALANCE].setText(balance_text) address_item[self.Columns.COIN_BALANCE].setData(balance, self.ROLE_SORT_ORDER) + address_item[self.Columns.COIN_BALANCE].setData(balance_text_nots, self.ROLE_CLIPBOARD_DATA) address_item[self.Columns.FIAT_BALANCE].setText(fiat_balance_str) address_item[self.Columns.FIAT_BALANCE].setData(balance, self.ROLE_SORT_ORDER) + address_item[self.Columns.FIAT_BALANCE].setData(fiat_balance_str_nots, self.ROLE_CLIPBOARD_DATA) address_item[self.Columns.NUM_TXS].setText("%d"%num) c = ColorScheme.BLUE.as_color(True) if self.wallet.is_frozen_address(address) else self._default_bg_brush address_item[self.Columns.ADDRESS].setBackground(c) diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py index ce4951fef..3151db04a 100644 --- a/electrum/gui/qt/history_list.py +++ b/electrum/gui/qt/history_list.py @@ -156,7 +156,7 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria return QVariant(d[col]) if role == MyTreeView.ROLE_EDIT_KEY: return QVariant(get_item_key(tx_item)) - if role not in (Qt.DisplayRole, Qt.EditRole): + if role not in (Qt.DisplayRole, Qt.EditRole, MyTreeView.ROLE_CLIPBOARD_DATA): if col == HistoryColumns.STATUS and role == Qt.DecorationRole: icon = "lightning" if is_lightning else TX_ICONS[status] return QVariant(read_QIcon(icon)) @@ -189,6 +189,13 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria blue_brush = QBrush(QColor("#1E1EFF")) return QVariant(blue_brush) return QVariant() + + add_thousands_sep = None + whitespaces = True + if role == MyTreeView.ROLE_CLIPBOARD_DATA: + add_thousands_sep = False + whitespaces = False + if col == HistoryColumns.STATUS: return QVariant(status_str) elif col == HistoryColumns.DESCRIPTION and 'label' in tx_item: @@ -197,23 +204,23 @@ def get_data_for_role(self, index: QModelIndex, role: Qt.ItemDataRole) -> QVaria bc_value = tx_item['bc_value'].value if 'bc_value' in tx_item else 0 ln_value = tx_item['ln_value'].value if 'ln_value' in tx_item else 0 value = bc_value + ln_value - v_str = window.format_amount(value, is_diff=True, whitespaces=True) + v_str = window.format_amount(value, is_diff=True, whitespaces=whitespaces, add_thousands_sep=add_thousands_sep) return QVariant(v_str) elif col == HistoryColumns.BALANCE: balance = tx_item['balance'].value - balance_str = window.format_amount(balance, whitespaces=True) + balance_str = window.format_amount(balance, whitespaces=whitespaces, add_thousands_sep=add_thousands_sep) return QVariant(balance_str) elif col == HistoryColumns.FIAT_VALUE and 'fiat_value' in tx_item: - value_str = window.fx.format_fiat(tx_item['fiat_value'].value) + value_str = window.fx.format_fiat(tx_item['fiat_value'].value, add_thousands_sep=add_thousands_sep) return QVariant(value_str) elif col == HistoryColumns.FIAT_ACQ_PRICE and \ tx_item['value'].value < 0 and 'acquisition_price' in tx_item: # fixme: should use is_mine acq = tx_item['acquisition_price'].value - return QVariant(window.fx.format_fiat(acq)) + return QVariant(window.fx.format_fiat(acq, add_thousands_sep=add_thousands_sep)) elif col == HistoryColumns.FIAT_CAP_GAINS and 'capital_gain' in tx_item: cg = tx_item['capital_gain'].value - return QVariant(window.fx.format_fiat(cg)) + return QVariant(window.fx.format_fiat(cg, add_thousands_sep=add_thousands_sep)) elif col == HistoryColumns.TXID: return QVariant(tx_hash) if not is_lightning else QVariant('') elif col == HistoryColumns.SHORT_ID: @@ -733,10 +740,12 @@ def add_copy_menu(self, menu, idx): continue column_title = self.hm.headerData(column, Qt.Horizontal, Qt.DisplayRole) idx2 = idx.sibling(idx.row(), column) - column_data = (self.hm.data(idx2, Qt.DisplayRole).value() or '').strip() + clipboard_data = self.hm.data(idx2, self.ROLE_CLIPBOARD_DATA).value() + if clipboard_data is None: + clipboard_data = (self.hm.data(idx2, Qt.DisplayRole).value() or '').strip() cc.addAction( column_title, - lambda text=column_data, title=column_title: + lambda text=clipboard_data, title=column_title: self.place_text_on_clipboard(text, title=title)) return cc diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 3e641e2fe..946d92ef2 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -120,6 +120,7 @@ def update(self): status = self.wallet.get_invoice_status(item) amount = item.get_amount_sat() amount_str = self.main_window.format_amount(amount, whitespaces=True) if amount else "" + amount_str_nots = self.main_window.format_amount(amount, whitespaces=True, add_thousands_sep=False) if amount else "" timestamp = item.time or 0 labels = [""] * len(self.Columns) labels[self.Columns.DATE] = format_time(timestamp) if timestamp else _('Unknown') @@ -133,6 +134,7 @@ def update(self): items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID) #items[self.Columns.DATE].setData(item.type, role=ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER) + items[self.Columns.AMOUNT].setData(amount_str_nots.strip(), role=self.ROLE_CLIPBOARD_DATA) self.std_model.insertRow(idx, items) self.filter() self.proxy.setDynamicSortFilter(True) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index b6f9ef4e6..afaecfb68 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -857,7 +857,8 @@ def format_amount( amount_sat, is_diff=False, whitespaces=False, - ignore_thousands_sep: bool = False, + *, + add_thousands_sep: bool = None, ) -> str: """Formats amount as string, converting to desired unit. E.g. 500_000 -> '0.005' @@ -866,7 +867,7 @@ def format_amount( amount_sat, is_diff=is_diff, whitespaces=whitespaces, - ignore_thousands_sep=ignore_thousands_sep, + add_thousands_sep=add_thousands_sep, ) def format_amount_and_units(self, amount_sat, *, timestamp: int = None) -> str: diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index d617d3eb2..25b4de01d 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -452,13 +452,6 @@ def add_copy_menu(self, menu: QMenu, idx) -> QMenu: return cc def place_text_on_clipboard(self, text: str, *, title: str = None) -> None: - if title in { - "Amount", - "Balance", - } or title.endswith(" Value") or title.endswith(" Acquisition price") or title.endswith(" Capital Gains"): - with contextlib.suppress(Exception): - # remove formatting for numbers - text = text.replace(" ", "") self.main_window.do_copy(text, title=title) def showEvent(self, e: 'QShowEvent'): diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 730010c0b..55514915b 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -145,6 +145,7 @@ def update(self): message = req.get_message() date = format_time(timestamp) amount_str = self.main_window.format_amount(amount) if amount else "" + amount_str_nots = self.main_window.format_amount(amount, add_thousands_sep=False) if amount else "" labels = [""] * len(self.Columns) labels[self.Columns.DATE] = date labels[self.Columns.DESCRIPTION] = message @@ -157,6 +158,7 @@ def update(self): #items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(key, ROLE_KEY) items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER) + items[self.Columns.AMOUNT].setData(amount_str_nots.strip(), self.ROLE_CLIPBOARD_DATA) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) self.std_model.insertRow(self.std_model.rowCount(), items) self.filter() diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 59a43bf36..579a08e25 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -314,7 +314,7 @@ def on_context_menu_for_inputs(self, pos: QPoint): copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))] txin_value = self.wallet.adb.get_txin_value(txin) if txin_value: - value_str = self.main_window.format_amount(txin_value, ignore_thousands_sep=True) + value_str = self.main_window.format_amount(txin_value, add_thousands_sep=False) copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))] for item in show_list: @@ -356,7 +356,7 @@ def on_context_menu_for_outputs(self, pos: QPoint): show_list += [(_("Address Details"), lambda: self.main_window.show_address(addr, parent=self))] copy_list += [(_("Copy Address"), lambda: self.main_window.do_copy(addr))] txout_value = self.tx.outputs()[txout_idx].value - value_str = self.main_window.format_amount(txout_value, ignore_thousands_sep=True) + value_str = self.main_window.format_amount(txout_value, add_thousands_sep=False) copy_list += [(_("Copy Amount"), lambda: self.main_window.do_copy(value_str))] for item in show_list: diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index adb966383..7f8bdc3d3 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -101,12 +101,17 @@ def update(self): name = utxo.prevout.to_str() self._utxo_dict[name] = utxo labels = [""] * len(self.Columns) + amount_str = self.main_window.format_amount( + utxo.value_sats(), whitespaces=True) + amount_str_nots = self.main_window.format_amount( + utxo.value_sats(), whitespaces=False, add_thousands_sep=False) labels[self.Columns.OUTPOINT] = str(utxo.short_id) labels[self.Columns.ADDRESS] = utxo.address - labels[self.Columns.AMOUNT] = self.main_window.format_amount(utxo.value_sats(), whitespaces=True) + labels[self.Columns.AMOUNT] = amount_str utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) utxo_item[self.Columns.OUTPOINT].setData(name, self.ROLE_PREVOUT_STR) + utxo_item[self.Columns.AMOUNT].setData(amount_str_nots, self.ROLE_CLIPBOARD_DATA) utxo_item[self.Columns.ADDRESS].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.AMOUNT].setFont(QFont(MONOSPACE_FONT)) utxo_item[self.Columns.PARENTS].setFont(QFont(MONOSPACE_FONT)) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 91b6d940d..d97c8d8ef 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -785,10 +785,12 @@ def format_amount( is_diff=False, whitespaces=False, precision=None, - ignore_thousands_sep: bool=False, + add_thousands_sep: bool = None, ) -> str: if precision is None: precision = self.amt_precision_post_satoshi + if add_thousands_sep is None: + add_thousands_sep = self.amt_add_thousands_sep return format_satoshis( amount_sat, num_zeros=self.num_zeros, @@ -796,7 +798,7 @@ def format_amount( is_diff=is_diff, whitespaces=whitespaces, precision=precision, - add_thousands_sep=False if ignore_thousands_sep else self.amt_add_thousands_sep, + add_thousands_sep=add_thousands_sep, ) def format_amount_and_units(self, *args, **kwargs) -> str: From 87cba8bf324024072f23e74f0edf646f2d637ca8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 13 Jun 2023 16:52:50 +0000 Subject: [PATCH 0963/1143] trustedcoin: stricter client-side checks for 2fa fee --- electrum/plugins/trustedcoin/trustedcoin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index b79a77a29..da4df6752 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -314,8 +314,10 @@ def extra_fee(self): return 0 n = self.num_prepay() price = int(self.price_per_tx[n]) - if price > 100000 * n: - raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n)) + # sanity check: price capped at 0.5 mBTC per tx or 20 mBTC total + # (note that the server can influence our choice of n by sending unexpected values) + if price > min(50_000 * n, 2_000_000): + raise Exception(f"too high trustedcoin fee ({price} for {n} txns)") return price def make_unsigned_transaction( From 03615c2cfc5f1d7845d319fd9405828aaaf5c04b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Jun 2023 00:58:21 +0000 Subject: [PATCH 0964/1143] commands: onchain_history: reuse daemon.fx if available --- electrum/commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index e70b66e35..77838f22e 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -773,8 +773,7 @@ async def onchain_history(self, year=None, show_addresses=False, show_fiat=False kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) if show_fiat: from .exchange_rate import FxThread - fx = FxThread(config=self.config) - kwargs['fx'] = fx + kwargs['fx'] = self.daemon.fx if self.daemon else FxThread(config=self.config) return json_normalize(wallet.get_detailed_history(**kwargs)) From 09b9fb83741def4ddbd7cf6b5f4e0f287b19fbfc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Jun 2023 15:42:28 +0000 Subject: [PATCH 0965/1143] exchange_rate: try harder to refresh quote when cache is expiring Previously we polled every 2.5 minutes to get the fx spot price, and had a 10 minute cache expiry during which the latest spot price was valid. On Android, this often resulted in having no price available (showing "No data" in GUI) when putting the app in the foreground after e.g. a half-hour sleep in the background: often there would be no fx price until the next tick, which could take 2.5 minutes. (btw in some cases I saw the application trying to get new quotes from the network as soon as the app was put in the foreground but it seems those happened so fast that the network was not ready yet and DNS lookups failed) Now we make the behaviour a bit more complex: we still fetch the price every 2.5 mins, and the cache is still valid for 10 mins, however if the last price is >7.5 mins old, we become more aggressive and go into an exponential backoff, initially trying a request every few seconds. For the Android scenario, this means there might be "No data" for fx for a few seconds after a long sleep, however if there is a working network, it should soon get a fresh fx spot price quote. --- electrum/exchange_rate.py | 67 +++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py index 801ce47a8..f4268905f 100644 --- a/electrum/exchange_rate.py +++ b/electrum/exchange_rate.py @@ -10,7 +10,7 @@ from decimal import Decimal from typing import Sequence, Optional, Mapping, Dict, Union, Any -from aiorpcx.curio import timeout_after, TaskTimeout +from aiorpcx.curio import timeout_after, TaskTimeout, ignore_after import aiohttp from . import util @@ -18,6 +18,7 @@ from .i18n import _ from .util import (ThreadJob, make_dir, log_exceptions, OldTaskGroup, make_aiohttp_session, resource_path, EventListener, event_listener, to_decimal) +from .util import NetworkRetryManager from .network import Network from .simple_config import SimpleConfig from .logging import Logger @@ -34,8 +35,9 @@ 'BTC': 8, 'LTC': 8, 'XRP': 6, 'ETH': 18, } -POLL_PERIOD_SPOT_RATE = 150 # approx. every 2.5 minutes, try to refresh spot price -EXPIRY_SPOT_RATE = 600 # spot price becomes stale after 10 minutes +SPOT_RATE_REFRESH_TARGET = 150 # approx. every 2.5 minutes, try to refresh spot price +SPOT_RATE_CLOSE_TO_STALE = 450 # try harder to fetch an update if price is getting old +SPOT_RATE_EXPIRY = 600 # spot price becomes stale after 10 minutes -> we no longer show/use it class ExchangeBase(Logger): @@ -83,13 +85,16 @@ async def update_safe(self, ccy: str) -> None: self._quotes = await self.get_rates(ccy) assert all(isinstance(rate, (Decimal, type(None))) for rate in self._quotes.values()), \ f"fx rate must be Decimal, got {self._quotes}" - self._quotes_timestamp = time.time() - self.logger.info("received fx quotes") except (aiohttp.ClientError, asyncio.TimeoutError) as e: self.logger.info(f"failed fx quotes: {repr(e)}") + self.on_quotes() except Exception as e: self.logger.exception(f"failed fx quotes: {repr(e)}") - self.on_quotes() + self.on_quotes() + else: + self.logger.info("received fx quotes") + self._quotes_timestamp = time.time() + self.on_quotes(received_new_data=True) def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]: filename = os.path.join(cache_dir, self.name() + '_'+ ccy) @@ -169,7 +174,7 @@ def get_cached_spot_quote(self, ccy: str) -> Decimal: rate = self._quotes.get(ccy) if rate is None: return Decimal('NaN') - if self._quotes_timestamp + EXPIRY_SPOT_RATE < time.time(): + if self._quotes_timestamp + SPOT_RATE_EXPIRY < time.time(): # Our rate is stale. Probably better to return no rate than an incorrect one. return Decimal('NaN') return Decimal(rate) @@ -504,10 +509,17 @@ def get_exchanges_by_ccy(history=True): return dictinvert(d) -class FxThread(ThreadJob, EventListener): +class FxThread(ThreadJob, EventListener, NetworkRetryManager[str]): def __init__(self, *, config: SimpleConfig): ThreadJob.__init__(self) + NetworkRetryManager.__init__( + self, + max_retry_delay_normal=SPOT_RATE_REFRESH_TARGET, + init_retry_delay_normal=SPOT_RATE_REFRESH_TARGET, + max_retry_delay_urgent=SPOT_RATE_REFRESH_TARGET, + init_retry_delay_urgent=1, + ) # note: we poll every 5 seconds for action, so we won't attempt connections more frequently than that. self.config = config self.register_callbacks() self.ccy = self.get_currency() @@ -522,6 +534,7 @@ def __init__(self, *, config: SimpleConfig): @event_listener def on_event_proxy_set(self, *args): + self._clear_addr_retry_times() self._trigger.set() @staticmethod @@ -559,17 +572,28 @@ def ccy_precision(self, ccy=None) -> int: async def run(self): while True: - # every few minutes, refresh spot price - try: - async with timeout_after(POLL_PERIOD_SPOT_RATE): - await self._trigger.wait() - self._trigger.clear() - # we were manually triggered, so get historical rates - if self.is_enabled() and self.has_history(): - self.exchange.get_historical_rates(self.ccy, self.cache_dir) - except TaskTimeout: - pass - if self.is_enabled(): + # keep polling and see if we should refresh spot price or historical prices + manually_triggered = False + async with ignore_after(5): + await self._trigger.wait() + self._trigger.clear() + manually_triggered = True + if not self.is_enabled(): + continue + if manually_triggered and self.has_history(): # maybe refresh historical prices + self.exchange.get_historical_rates(self.ccy, self.cache_dir) + now = time.time() + if not manually_triggered and self.exchange._quotes_timestamp + SPOT_RATE_REFRESH_TARGET > now: + continue # last quote still fresh + # If the last quote is relatively recent, we poll at fixed time intervals. + # Once it gets close to cache expiry, we change to an exponential backoff, to try to get + # a quote before it expires. Also, on Android, we might come back from a sleep after a long time, + # with the last quote close to expiry or already expired, in that case we go into exponential backoff. + is_urgent = self.exchange._quotes_timestamp + SPOT_RATE_CLOSE_TO_STALE < now + addr_name = "spot-urgent" if is_urgent else "spot" # this separates retry-counters + if self._can_retry_addr(addr_name, urgent=is_urgent): + self._trying_addr_now(addr_name) + # refresh spot price await self.exchange.update_safe(self.ccy) def is_enabled(self) -> bool: @@ -599,6 +623,7 @@ def set_currency(self, ccy: str): self.on_quotes() def trigger_update(self): + self._clear_addr_retry_times() loop = util.get_asyncio_loop() loop.call_soon_threadsafe(self._trigger.set) @@ -614,7 +639,9 @@ def set_exchange(self, name): self.trigger_update() self.exchange.read_historical_rates(self.ccy, self.cache_dir) - def on_quotes(self): + def on_quotes(self, *, received_new_data: bool = False): + if received_new_data: + self._clear_addr_retry_times() util.trigger_callback('on_quotes') def on_history(self): From 4a211adcaa1f7f0ac170f7d3b7ffb853f12235bd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Jun 2023 17:02:07 +0000 Subject: [PATCH 0966/1143] trezor plugin: allow multiple change outputs see https://github.com/spesmilo/electrum/issues/3920 --- electrum/plugins/trezor/trezor.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 39571b760..1e3528cfc 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -362,7 +362,7 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): prev_tx = {bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items()} client = self.get_client(keystore) inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore) - outputs = self.tx_outputs(tx, keystore=keystore) + outputs = self.tx_outputs(tx, keystore=keystore, firmware_version=client.client.version) signatures, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, @@ -442,7 +442,7 @@ def _make_multisig(self, desc: descriptor.MultisigDescriptor): signatures=[b''] * len(pubkeys), m=desc.thresh) - def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'): + def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore', firmware_version: Sequence[int]): def create_output_by_derivation(): desc = txout.script_descriptor @@ -483,14 +483,18 @@ def create_output_by_address(): address = txout.address use_create_by_derivation = False - if txout.is_mine and not has_change: - # prioritise hiding outputs on the 'change' branch from user - # because no more than one change address allowed - # note: ^ restriction can be removed once we require fw - # that has https://github.com/trezor/trezor-mcu/pull/306 - if txout.is_change == any_output_on_change_branch: + if txout.is_mine: + if tuple(firmware_version) >= (1, 6, 1): use_create_by_derivation = True - has_change = True + else: + if not has_change: + # prioritise hiding outputs on the 'change' branch from user + # because no more than one change address allowed + # note: ^ restriction can be removed once we require fw 1.6.1 + # that has https://github.com/trezor/trezor-mcu/pull/306 + if txout.is_change == any_output_on_change_branch: + use_create_by_derivation = True + has_change = True if use_create_by_derivation: txoutputtype = create_output_by_derivation() From eef9680743e8a7ab6ad760c68ea63c396532bd65 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 14 Jun 2023 17:03:47 +0000 Subject: [PATCH 0967/1143] trezor plugin: support external pre-signed inputs closes https://github.com/spesmilo/electrum/issues/8324 --- electrum/plugins/trezor/trezor.py | 24 +++++++++++++++--------- electrum/transaction.py | 4 +++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index 1e3528cfc..6184d8601 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -371,7 +371,7 @@ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): serialize=False, prev_txes=prev_tx) sighash = Sighash.to_sigbytes(Sighash.ALL).hex() - signatures = [(x.hex() + sighash) for x in signatures] + signatures = [((x.hex() + sighash) if x else None) for x in signatures] tx.update_signatures(signatures) @runs_in_hwd_thread @@ -412,17 +412,23 @@ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - desc = txin.script_descriptor - assert desc - if multi := desc.get_simple_multisig(): - txinputtype.multisig = self._make_multisig(multi) - txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type()) - my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) - if full_path: - txinputtype.address_n = full_path + if txin.is_complete(): + txinputtype.script_type = InputScriptType.EXTERNAL + assert txin.scriptpubkey + txinputtype.script_pubkey = txin.scriptpubkey + else: + desc = txin.script_descriptor + assert desc + if multi := desc.get_simple_multisig(): + txinputtype.multisig = self._make_multisig(multi) + txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type()) + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) + if full_path: + txinputtype.address_n = full_path txinputtype.amount = txin.value_sats() txinputtype.script_sig = txin.script_sig + txinputtype.witness = txin.witness txinputtype.sequence = txin.nsequence inputs.append(txinputtype) diff --git a/electrum/transaction.py b/electrum/transaction.py index 22a55709d..154ab55a7 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -2145,7 +2145,7 @@ def _serialize_as_base64(self) -> str: raw_bytes = self.serialize_as_bytes() return base64.b64encode(raw_bytes).decode('ascii') - def update_signatures(self, signatures: Sequence[str]): + def update_signatures(self, signatures: Sequence[Union[str, None]]): """Add new signatures to a transaction `signatures` is expected to be a list of sigs with signatures[i] @@ -2159,6 +2159,8 @@ def update_signatures(self, signatures: Sequence[str]): for i, txin in enumerate(self.inputs()): pubkeys = [pk.hex() for pk in txin.pubkeys] sig = signatures[i] + if sig is None: + continue if bfh(sig) in list(txin.part_sigs.values()): continue pre_hash = sha256d(bfh(self.serialize_preimage(i))) From 295734fc5374b133fd7ff79568a4ad22b2e40ed3 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 5 Oct 2021 09:13:01 +0200 Subject: [PATCH 0968/1143] storage: encapsulate type conversions of stored objects using decorators (instead of overloading JsonDB._convert_dict and _convert_value) - stored_in for elements of a StoreDict - stored_as for singletons - extra register methods are defined for key conversions This commit was adapted from the jsonpatch branch --- electrum/invoices.py | 4 +- electrum/json_db.py | 65 +++++++++++++++++++++++++++++++ electrum/lnutil.py | 42 ++++++++++++++------ electrum/submarine_swaps.py | 3 +- electrum/transaction.py | 1 + electrum/util.py | 3 -- electrum/wallet_db.py | 76 ++++++++++--------------------------- 7 files changed, 120 insertions(+), 74 deletions(-) diff --git a/electrum/invoices.py b/electrum/invoices.py index c90ad75a6..b184388b3 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -4,7 +4,7 @@ import attr -from .json_db import StoredObject +from .json_db import StoredObject, stored_in from .i18n import _ from .util import age, InvoiceError, format_satoshis from .lnutil import hex_to_bytes @@ -244,6 +244,7 @@ def as_dict(self, status): return d +@stored_in('invoices') @attr.s class Invoice(BaseInvoice): lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] @@ -303,6 +304,7 @@ def to_debug_json(self) -> Dict[str, Any]: return d +@stored_in('payment_requests') @attr.s class Request(BaseInvoice): payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes) # type: Optional[bytes] diff --git a/electrum/json_db.py b/electrum/json_db.py index 00f249c16..41fadbe8d 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -45,6 +45,28 @@ def wrapper(self, *args, **kwargs): return wrapper +registered_names = {} +registered_dicts = {} +registered_dict_keys = {} +registered_parent_keys = {} + + +def stored_as(name, _type=dict): + """ decorator that indicates the storage key of a stored object""" + def decorator(func): + registered_names[name] = func, _type + return func + return decorator + +def stored_in(name, _type=dict): + """ decorator that indicates the storage key of an element in a StoredDict""" + def decorator(func): + registered_dicts[name] = func, _type + return func + return decorator + + + class StoredObject: db = None @@ -195,3 +217,46 @@ def dump(self, *, human_readable: bool = True) -> str: def _should_convert_to_stored_dict(self, key) -> bool: return True + + def register_dict(self, name, method, _type): + registered_dicts[name] = method, _type + + def register_name(self, name, method, _type): + registered_names[name] = method, _type + + def register_dict_key(self, name, method): + registered_dict_keys[name] = method + + def register_parent_key(self, name, method): + registered_parent_keys[name] = method + + def _convert_dict(self, path, key, v): + + if key in registered_dicts: + constructor, _type = registered_dicts[key] + if _type == dict: + v = dict((k, constructor(**x)) for k, x in v.items()) + elif _type == tuple: + v = dict((k, constructor(*x)) for k, x in v.items()) + else: + v = dict((k, constructor(x)) for k, x in v.items()) + + if key in registered_dict_keys: + convert_key = registered_dict_keys[key] + elif path and path[-1] in registered_parent_keys: + convert_key = registered_parent_keys.get(path[-1]) + else: + convert_key = None + if convert_key: + v = dict((convert_key(k), x) for k, x in v.items()) + + return v + + def _convert_value(self, path, key, v): + if key in registered_names: + constructor, _type = registered_names[key] + if _type == dict: + v = constructor(**v) + else: + v = constructor(v) + return v diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 4ab8e3949..93ec18b1e 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -52,7 +52,7 @@ def ln_dummy_address(): return redeem_script_to_address('p2wsh', '') -from .json_db import StoredObject +from .json_db import StoredObject, stored_in, stored_as def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]: @@ -181,6 +181,7 @@ def cross_validate_params( raise Exception(f"feerate lower than min relay fee. {initial_feerate_per_kw} sat/kw.") +@stored_as('local_config') @attr.s class LocalConfig(ChannelConfig): channel_seed = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes] @@ -214,17 +215,20 @@ def validate_params(self, *, funding_sat: int) -> None: if self.htlc_minimum_msat < HTLC_MINIMUM_MSAT_MIN: raise Exception(f"{conf_name}. htlc_minimum_msat too low: {self.htlc_minimum_msat} msat < {HTLC_MINIMUM_MSAT_MIN}") +@stored_as('remote_config') @attr.s class RemoteConfig(ChannelConfig): next_per_commitment_point = attr.ib(type=bytes, converter=hex_to_bytes) current_per_commitment_point = attr.ib(default=None, type=bytes, converter=hex_to_bytes) +@stored_in('fee_updates') @attr.s class FeeUpdate(StoredObject): rate = attr.ib(type=int) # in sat/kw ctn_local = attr.ib(default=None, type=int) ctn_remote = attr.ib(default=None, type=int) +@stored_as('constraints') @attr.s class ChannelConstraints(StoredObject): capacity = attr.ib(type=int) # in sat @@ -248,10 +252,12 @@ def channel_id(self): chan_id, _ = channel_id_from_funding_tx(self.funding_txid, self.funding_index) return chan_id +@stored_in('onchain_channel_backups') @attr.s class OnchainChannelBackupStorage(ChannelBackupStorage): node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes) +@stored_in('imported_channel_backups') @attr.s class ImportedChannelBackupStorage(ChannelBackupStorage): node_id = attr.ib(type=bytes, converter=hex_to_bytes) @@ -320,6 +326,7 @@ class ScriptHtlc(NamedTuple): # FIXME duplicate of TxOutpoint in transaction.py?? +@stored_as('funding_outpoint') @attr.s class Outpoint(StoredObject): txid = attr.ib(type=str) @@ -484,8 +491,17 @@ def get_prefix(index, pos): get_per_commitment_secret_from_seed(element.secret, to_index, zeros), to_index) -ShachainElement = namedtuple("ShachainElement", ["secret", "index"]) -ShachainElement.__str__ = lambda self: f"ShachainElement({self.secret.hex()},{self.index})" +class ShachainElement(NamedTuple): + secret: bytes + index: int + + def __str__(self): + return "ShachainElement(" + self.secret.hex() + "," + str(self.index) + ")" + + @stored_in('buckets', tuple) + def read(*x): + return ShachainElement(bfh(x[0]), int(x[1])) + def get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) -> bytes: """Generate per commitment secret.""" @@ -1226,6 +1242,7 @@ def __str__(self): return hex(self._value_) +@stored_as('channel_type', _type=None) class ChannelType(IntFlag): OPTION_LEGACY_CHANNEL = 0 OPTION_STATIC_REMOTEKEY = 1 << 12 @@ -1546,15 +1563,16 @@ class UpdateAddHtlc: timestamp = attr.ib(type=int, kw_only=True) htlc_id = attr.ib(type=int, kw_only=True, default=None) - @classmethod - def from_tuple(cls, amount_msat, payment_hash, cltv_expiry, htlc_id, timestamp) -> 'UpdateAddHtlc': - return cls(amount_msat=amount_msat, - payment_hash=payment_hash, - cltv_expiry=cltv_expiry, - htlc_id=htlc_id, - timestamp=timestamp) - - def to_tuple(self): + @stored_in('adds', tuple) + def from_tuple(amount_msat, payment_hash, cltv_expiry, htlc_id, timestamp) -> 'UpdateAddHtlc': + return UpdateAddHtlc( + amount_msat=amount_msat, + payment_hash=payment_hash, + cltv_expiry=cltv_expiry, + htlc_id=htlc_id, + timestamp=timestamp) + + def to_json(self): return (self.amount_msat, self.payment_hash, self.cltv_expiry, self.htlc_id, self.timestamp) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index a7f966e7e..7918283d5 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -19,7 +19,7 @@ from .bitcoin import dust_threshold from .logging import Logger from .lnutil import hex_to_bytes -from .json_db import StoredObject +from .json_db import StoredObject, stored_in from . import constants from .address_synchronizer import TX_HEIGHT_LOCAL from .i18n import _ @@ -87,6 +87,7 @@ def __str__(self): return _("The swap server errored or is unreachable.") +@stored_in('submarine_swaps') @attr.s class SwapData(StoredObject): is_reverse = attr.ib(type=bool) diff --git a/electrum/transaction.py b/electrum/transaction.py index 154ab55a7..248642905 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -53,6 +53,7 @@ from .logging import get_logger from .util import ShortID, OldTaskGroup from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address +from .json_db import stored_in if TYPE_CHECKING: from .wallet import Abstract_Wallet diff --git a/electrum/util.py b/electrum/util.py index d09a31a18..c31ba5711 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -297,9 +297,6 @@ class MyEncoder(json.JSONEncoder): def default(self, obj): # note: this does not get called for namedtuples :( https://bugs.python.org/issue30343 from .transaction import Transaction, TxOutput - from .lnutil import UpdateAddHtlc - if isinstance(obj, UpdateAddHtlc): - return obj.to_tuple() if isinstance(obj, Transaction): return obj.serialize() if isinstance(obj, TxOutput): diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 1336b609e..2b38ad7de 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -41,12 +41,10 @@ from .keystore import bip44_derivation from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .logging import Logger -from .lnutil import LOCAL, REMOTE, FeeUpdate, UpdateAddHtlc, LocalConfig, RemoteConfig, ChannelType -from .lnutil import ImportedChannelBackupStorage, OnchainChannelBackupStorage -from .lnutil import ChannelConstraints, Outpoint, ShachainElement -from .json_db import StoredDict, JsonDB, locked, modifier, StoredObject + +from .lnutil import LOCAL, REMOTE, HTLCOwner, ChannelType +from .json_db import StoredDict, JsonDB, locked, modifier, StoredObject, stored_in, stored_as from .plugin import run_hook, plugin_loaders -from .submarine_swaps import SwapData from .version import ELECTRUM_VERSION if TYPE_CHECKING: @@ -61,12 +59,14 @@ # old versions from overwriting new format +@stored_in('tx_fees', tuple) class TxFeesValue(NamedTuple): fee: Optional[int] = None is_calculated_by_us: bool = False num_inputs: Optional[int] = None +@stored_as('db_metadata') @attr.s class DBMetadata(StoredObject): creation_timestamp = attr.ib(default=None, type=int) @@ -91,6 +91,20 @@ class WalletDB(JsonDB): def __init__(self, raw, *, manual_upgrades: bool): JsonDB.__init__(self, {}) + # register dicts that require value conversions not handled by constructor + self.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None) + self.register_dict('prevouts_by_scripthash', lambda x: set(tuple(k) for k in x), None) + self.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None) + # register dicts that require key conversion + for key in [ + 'adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets', + 'unacked_updates', 'unfulfilled_htlcs', 'fail_htlc_reasons', 'onion_keys']: + self.register_dict_key(key, int) + for key in ['log']: + self.register_dict_key(key, lambda x: HTLCOwner(int(x))) + for key in ['locked_in', 'fails', 'settles']: + self.register_parent_key(key, lambda x: HTLCOwner(int(x))) + self._manual_upgrades = manual_upgrades self._called_after_upgrade_tasks = False if raw: # loading existing db @@ -1560,58 +1574,6 @@ def clear_history(self): self.tx_fees.clear() self._prevouts_by_scripthash.clear() - def _convert_dict(self, path, key, v): - if key == 'transactions': - # note: for performance, "deserialize=False" so that we will deserialize these on-demand - v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items()) - if key == 'invoices': - v = dict((k, Invoice(**x)) for k, x in v.items()) - if key == 'payment_requests': - v = dict((k, Request(**x)) for k, x in v.items()) - elif key == 'adds': - v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items()) - elif key == 'fee_updates': - v = dict((k, FeeUpdate(**x)) for k, x in v.items()) - elif key == 'submarine_swaps': - v = dict((k, SwapData(**x)) for k, x in v.items()) - elif key == 'imported_channel_backups': - v = dict((k, ImportedChannelBackupStorage(**x)) for k, x in v.items()) - elif key == 'onchain_channel_backups': - v = dict((k, OnchainChannelBackupStorage(**x)) for k, x in v.items()) - elif key == 'tx_fees': - v = dict((k, TxFeesValue(*x)) for k, x in v.items()) - elif key == 'prevouts_by_scripthash': - v = dict((k, {(prevout, value) for (prevout, value) in x}) for k, x in v.items()) - elif key == 'buckets': - v = dict((k, ShachainElement(bfh(x[0]), int(x[1]))) for k, x in v.items()) - elif key == 'data_loss_protect_remote_pcp': - v = dict((k, bfh(x)) for k, x in v.items()) - # convert htlc_id keys to int - if key in ['adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets', - 'unacked_updates', 'unfulfilled_htlcs', 'fail_htlc_reasons', 'onion_keys']: - v = dict((int(k), x) for k, x in v.items()) - # convert keys to HTLCOwner - if key == 'log' or (path and path[-1] in ['locked_in', 'fails', 'settles']): - if "1" in v: - v[LOCAL] = v.pop("1") - v[REMOTE] = v.pop("-1") - return v - - def _convert_value(self, path, key, v): - if key == 'local_config': - v = LocalConfig(**v) - elif key == 'remote_config': - v = RemoteConfig(**v) - elif key == 'constraints': - v = ChannelConstraints(**v) - elif key == 'funding_outpoint': - v = Outpoint(**v) - elif key == 'channel_type': - v = ChannelType(v) - elif key == 'db_metadata': - v = DBMetadata(**v) - return v - def _should_convert_to_stored_dict(self, key) -> bool: if key == 'keystore': return False From 606c51bc4e403c9b81a3abd5681dfc2c999e1699 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 18 Jun 2023 13:49:56 +0200 Subject: [PATCH 0969/1143] follow-up previous commit --- electrum/json_db.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index 41fadbe8d..226c8480d 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -197,12 +197,6 @@ def get_dict(self, name) -> dict: self.data[name] = {} return self.data[name] - def _convert_dict(self, path, key, v): - return v - - def _convert_value(self, path, key, v): - return v - @locked def dump(self, *, human_readable: bool = True) -> str: """Serializes the DB as a string. @@ -231,7 +225,6 @@ def register_parent_key(self, name, method): registered_parent_keys[name] = method def _convert_dict(self, path, key, v): - if key in registered_dicts: constructor, _type = registered_dicts[key] if _type == dict: @@ -240,7 +233,6 @@ def _convert_dict(self, path, key, v): v = dict((k, constructor(*x)) for k, x in v.items()) else: v = dict((k, constructor(x)) for k, x in v.items()) - if key in registered_dict_keys: convert_key = registered_dict_keys[key] elif path and path[-1] in registered_parent_keys: @@ -249,7 +241,6 @@ def _convert_dict(self, path, key, v): convert_key = None if convert_key: v = dict((convert_key(k), x) for k, x in v.items()) - return v def _convert_value(self, path, key, v): From 39f8664402dabb377d99640472eead909e51bd75 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 19 Jun 2023 14:46:56 +0200 Subject: [PATCH 0970/1143] make submarine swap server url configurable --- electrum/simple_config.py | 14 ++++++++++++-- electrum/submarine_swaps.py | 10 +--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index d97c8d8ef..2ddac15b7 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -840,8 +840,15 @@ def __setattr__(self, name, value): f"Either use config.cv.{name}.set() or assign to config.{name} instead.") return CVLookupHelper() - # config variables -----> + def get_swapserver_url(self): + if constants.net == constants.BitcoinMainnet: + return wallet.config.SWAPSERVER_URL_MAINNET + elif constants.net == constants.BitcoinTestnet: + return wallet.config.SWAPSERVER_URL_TESTNET + else: + return wallet.config.SWAPSERVER_URL_REGTEST + # config variables -----> NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool) NETWORK_PROXY = ConfigVar('proxy', default=None) @@ -951,7 +958,10 @@ def __setattr__(self, name, value): SSL_CERTFILE_PATH = ConfigVar('ssl_certfile', default='', type_=str) SSL_KEYFILE_PATH = ConfigVar('ssl_keyfile', default='', type_=str) - + # submarine swap server + SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str) + SWAPSERVER_URL_TESTNET = ConfigVar('swapserver_url_testnet', default='https://swaps.electrum.org/testnet', type_=str) + SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='https://localhost/api', type_=str) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 7918283d5..4e7c7fe8e 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -32,9 +32,6 @@ from .simple_config import SimpleConfig -API_URL_MAINNET = 'https://swaps.electrum.org/api' -API_URL_TESTNET = 'https://swaps.electrum.org/testnet' -API_URL_REGTEST = 'https://localhost/api' @@ -159,12 +156,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): if swap.is_reverse and swap.prepay_hash is not None: self.prepayments[swap.prepay_hash] = bytes.fromhex(k) # api url - if constants.net == constants.BitcoinMainnet: - self.api_url = API_URL_MAINNET - elif constants.net == constants.BitcoinTestnet: - self.api_url = API_URL_TESTNET - else: - self.api_url = API_URL_REGTEST + self.api_url = wallet.config.get_swapserver_url() # init default min & max self.init_min_max_values() From cc57648a0c3daf9fd17d45dbd1e715cb62da0596 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 19 Jun 2023 14:49:25 +0200 Subject: [PATCH 0971/1143] follow-up previous commit --- electrum/simple_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 2ddac15b7..dedb93b4d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -842,11 +842,11 @@ def __setattr__(self, name, value): def get_swapserver_url(self): if constants.net == constants.BitcoinMainnet: - return wallet.config.SWAPSERVER_URL_MAINNET + return self.SWAPSERVER_URL_MAINNET elif constants.net == constants.BitcoinTestnet: - return wallet.config.SWAPSERVER_URL_TESTNET + return self.SWAPSERVER_URL_TESTNET else: - return wallet.config.SWAPSERVER_URL_REGTEST + return self.SWAPSERVER_URL_REGTEST # config variables -----> NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) From 52817b220d2d458360b0cf0a54ca407654b0d37e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 19 Jun 2023 16:01:12 +0000 Subject: [PATCH 0972/1143] update locale --- contrib/deterministic-build/electrum-locale | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/deterministic-build/electrum-locale b/contrib/deterministic-build/electrum-locale index 1f5416b24..b59f076ec 160000 --- a/contrib/deterministic-build/electrum-locale +++ b/contrib/deterministic-build/electrum-locale @@ -1 +1 @@ -Subproject commit 1f5416b2448fa594c684bbb893a91f3d24a2b238 +Subproject commit b59f076ec55d7347e7dcecaed6e4cf5e6a2a77d3 From 1e3f8106674a116c2f333a2a2556b8860ab35d6d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 19 Jun 2023 15:52:32 +0000 Subject: [PATCH 0973/1143] update release notes for version 4.4.5 --- RELEASE-NOTES | 9 +++++++++ electrum/version.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index dc067ab42..37016036c 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,12 @@ +# Release 4.4.5 (June 20, 2023) + * Hardware wallets: + - jade: fix regression in sign_transaction (#8463) + * Lightning: + - fix "rebalance_channels" function (#8468) + * enforce that we run with python asserts enabled, + regardless of platform (d1c88108) + + # Release 4.4.4 (May 31, 2023) * QML GUI: - fix creating multisig wallets involving BIP39 seeds (#8432) diff --git a/electrum/version.py b/electrum/version.py index 5351c94a8..4239e271a 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.4.4' # version of the client package -APK_VERSION = '4.4.4.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.4.5' # version of the client package +APK_VERSION = '4.4.5.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested From fbf41b582a151c6bfa74caf0507d3455bddedba9 Mon Sep 17 00:00:00 2001 From: gruve-p Date: Fri, 16 Jun 2023 13:55:58 +0200 Subject: [PATCH 0974/1143] kivy: fix fx history rates --- electrum/gui/kivy/uix/dialogs/fx_dialog.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/fx_dialog.py b/electrum/gui/kivy/uix/dialogs/fx_dialog.py index ef348b873..3eeee5d27 100644 --- a/electrum/gui/kivy/uix/dialogs/fx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/fx_dialog.py @@ -89,12 +89,9 @@ def __init__(self, app, plugins, config, callback): self.config = config self.callback = callback self.fx = self.app.fx - if self.fx.get_history_config(allow_none=True) is None: - # If nothing is set, force-enable it. (Note that as fiat rates itself - # are disabled by default, it is enough to set this here. If they - # were enabled by default, this would be too late.) - self.fx.set_history_config(True) - self.has_history_rates = self.fx.get_history_config() + if not self.fx.config.cv.FX_HISTORY_RATES.is_set(): + self.fx.config.FX_HISTORY_RATES = True # override default + self.has_history_rates = self.fx.config.FX_HISTORY_RATES Factory.Popup.__init__(self) self.add_currencies() @@ -128,7 +125,7 @@ def add_currencies(self): self.ids.ccy.text = my_ccy def on_checkbox_history(self, checked): - self.fx.set_history_config(checked) + self.fx.config.FX_HISTORY_RATES = bool(checked) self.has_history_rates = checked self.add_currencies() self.on_currency(self.ids.ccy.text) From 4177f8be82fa4940b0e8921f75f45a38162a4a7f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 21 Jun 2023 08:55:17 +0200 Subject: [PATCH 0975/1143] lnworker.create_routes_for_payment: fix MPP detection --- electrum/lnworker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 07adf9ecd..011cb831a 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1624,11 +1624,12 @@ async def create_routes_for_payment( random.shuffle(my_active_channels) split_configurations = self.suggest_splits(amount_msat, my_active_channels, invoice_features, r_tags) for sc in split_configurations: - is_mpp = len(sc.config.items()) > 1 - routes = [] + is_multichan_mpp = len(sc.config.items()) > 1 + is_mpp = sum(len(x) for x in list(sc.config.values())) > 1 if is_mpp and not invoice_features.supports(LnFeatures.BASIC_MPP_OPT): continue self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}") + routes = [] try: if self.uses_trampoline(): per_trampoline_channel_amounts = defaultdict(list) @@ -1705,7 +1706,7 @@ async def create_routes_for_payment( min_cltv_expiry=min_cltv_expiry, r_tags=r_tags, invoice_features=invoice_features, - my_sending_channels=[channel] if is_mpp else my_active_channels, + my_sending_channels=[channel] if is_multichan_mpp else my_active_channels, full_path=full_path, ) ) From 922981e586a122d31f495d69984a1256f4f885d9 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 21 Jun 2023 15:22:17 +0000 Subject: [PATCH 0976/1143] lnpeer: improve logging in maybe_forward_htlc --- electrum/lnpeer.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index be53811f7..488bb13da 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1568,6 +1568,7 @@ def on_update_add_htlc(self, chan: Channel, payload): def maybe_forward_htlc( self, *, + incoming_chan: Channel, htlc: UpdateAddHtlc, processed_onion: ProcessedOnionPacket) -> Tuple[bytes, int]: @@ -1578,35 +1579,42 @@ def maybe_forward_htlc( # (same for trampoline forwarding) # - we could check for the exposure to dust HTLCs, see: # https://github.com/ACINQ/eclair/pull/1985 + + def log_fail_reason(reason: str): + self.logger.debug( + f"maybe_forward_htlc. will FAIL HTLC: inc_chan={incoming_chan.get_id_for_log()}. " + f"{reason}. inc_htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}") + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS if not forwarding_enabled: - self.logger.info(f"forwarding is disabled. failing htlc.") + log_fail_reason("forwarding is disabled") raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') chain = self.network.blockchain() if chain.is_tip_stale(): raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') try: - next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] + next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] # type: bytes except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid) local_height = chain.height() if next_chan is None: - self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}") + log_fail_reason(f"cannot find next_chan {next_chan_scid.hex()}") raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:] outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big") outgoing_chan_upd_message = outgoing_chan_upd_len + outgoing_chan_upd if not next_chan.can_send_update_add_htlc(): - self.logger.info(f"cannot forward htlc. next_chan {next_chan_scid} cannot send ctx updates. " - f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}") + log_fail_reason( + f"next_chan {next_chan.get_id_for_log()} cannot send ctx updates. " + f"chan state {next_chan.get_state()!r}, peer state: {next_chan.peer_state!r}") raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) try: next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"] except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') if not next_chan.can_pay(next_amount_msat_htlc): - self.logger.info(f"cannot forward htlc due to transient errors (likely due to insufficient funds)") + log_fail_reason(f"transient error (likely due to insufficient funds): not next_chan.can_pay(amt)") raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) try: next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"] @@ -1627,10 +1635,12 @@ def maybe_forward_htlc( if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees: data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_message raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data) - self.logger.info(f'forwarding htlc to {next_chan.node_id.hex()}') + self.logger.info( + f"maybe_forward_htlc. will forward HTLC: inc_chan={incoming_chan.short_channel_id}. inc_htlc={str(htlc)}. " + f"next_chan={next_chan.get_id_for_log()}.") next_peer = self.lnworker.peers.get(next_chan.node_id) if next_peer is None: - self.logger.info(f"failed to forward htlc: next_peer offline ({next_chan.node_id.hex()})") + log_fail_reason(f"next_peer offline ({next_chan.node_id.hex()})") raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) next_htlc = UpdateAddHtlc( amount_msat=next_amount_msat_htlc, @@ -1649,7 +1659,7 @@ def maybe_forward_htlc( onion_routing_packet=processed_onion.next_packet.to_bytes() ) except BaseException as e: - self.logger.info(f"failed to forward htlc: error sending message. {e}") + log_fail_reason(f"error sending message to next_peer={next_chan.node_id.hex()}") raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_message) next_peer.maybe_send_commitment(next_chan) return next_chan_scid, next_htlc.htlc_id @@ -2358,6 +2368,7 @@ def process_unfulfilled_htlc( if not self.lnworker.enable_htlc_forwarding: return None, None, None next_chan_id, next_htlc_id = self.maybe_forward_htlc( + incoming_chan=chan, htlc=htlc, processed_onion=processed_onion) fw_info = (next_chan_id.hex(), next_htlc_id) From 1ff413080439dbabbc63c298163995d04f81c8eb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 22 Jun 2023 15:45:30 +0000 Subject: [PATCH 0977/1143] contrib/docker_notes.md: add notes re debian apt mirror, and envvars related https://github.com/spesmilo/electrum/issues/8496 --- contrib/android/Readme.md | 6 ++- contrib/build-linux/appimage/README.md | 6 ++- contrib/build-linux/sdist/README.md | 4 +- contrib/build-wine/README.md | 10 +++-- contrib/docker_notes.md | 51 +++++++++++++++++++++++++- 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/contrib/android/Readme.md b/contrib/android/Readme.md index 92f57d20a..60b0b0a61 100644 --- a/contrib/android/Readme.md +++ b/contrib/android/Readme.md @@ -15,7 +15,9 @@ similar system. 1. Install Docker - See `contrib/docker_notes.md`. + See [`contrib/docker_notes.md`](../docker_notes.md). + + (worth reading even if you already have docker) 2. Build binaries @@ -27,7 +29,7 @@ similar system. If you want reproducibility, try instead e.g.: ``` - $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh qml all release-unsigned + $ ELECBUILD_COMMIT=HEAD ./build.sh qml all release-unsigned ``` 3. The generated binary is in `./dist`. diff --git a/contrib/build-linux/appimage/README.md b/contrib/build-linux/appimage/README.md index 7680aafc6..841fe9fe5 100644 --- a/contrib/build-linux/appimage/README.md +++ b/contrib/build-linux/appimage/README.md @@ -14,7 +14,9 @@ see [issue #5159](https://github.com/spesmilo/electrum/issues/5159). 1. Install Docker - See `contrib/docker_notes.md`. + See [`contrib/docker_notes.md`](../../docker_notes.md). + + (worth reading even if you already have docker) 2. Build binary @@ -23,7 +25,7 @@ see [issue #5159](https://github.com/spesmilo/electrum/issues/5159). ``` If you want reproducibility, try instead e.g.: ``` - $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh + $ ELECBUILD_COMMIT=HEAD ./build.sh ``` 3. The generated binary is in `./dist`. diff --git a/contrib/build-linux/sdist/README.md b/contrib/build-linux/sdist/README.md index abad65adf..34d7150a7 100644 --- a/contrib/build-linux/sdist/README.md +++ b/contrib/build-linux/sdist/README.md @@ -19,7 +19,9 @@ the source-only one, also includes: 1. Install Docker - See `contrib/docker_notes.md`. + See [`contrib/docker_notes.md`](../../docker_notes.md). + + (worth reading even if you already have docker) 2. Build tarball diff --git a/contrib/build-wine/README.md b/contrib/build-wine/README.md index 8c1fd83e7..645a75edd 100644 --- a/contrib/build-wine/README.md +++ b/contrib/build-wine/README.md @@ -8,7 +8,9 @@ similar system. 1. Install Docker - See `contrib/docker_notes.md`. + See [`contrib/docker_notes.md`](../docker_notes.md). + + (worth reading even if you already have docker) Note: older versions of Docker might not work well (see [#6971](https://github.com/spesmilo/electrum/issues/6971)). @@ -21,7 +23,7 @@ similar system. ``` If you want reproducibility, try instead e.g.: ``` - $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh + $ ELECBUILD_COMMIT=HEAD ./build.sh ``` 3. The generated binaries are in `./contrib/build-wine/dist`. @@ -33,7 +35,7 @@ similar system. Electrum Windows builds are signed with a Microsoft Authenticode™ code signing certificate in addition to the GPG-based signatures. -The advantage of using Authenticode is that Electrum users won't receive a +The advantage of using Authenticode is that Electrum users won't receive a Windows SmartScreen warning when starting it. The release signing procedure involves a signer (the holder of the @@ -57,7 +59,7 @@ certificate/key) and one or multiple trusted verifiers: ## Verify Integrity of signed binary -Every user can verify that the official binary was created from the source code in this +Every user can verify that the official binary was created from the source code in this repository. To do so, the Authenticode signature needs to be stripped since the signature is not reproducible. diff --git a/contrib/docker_notes.md b/contrib/docker_notes.md index e1ab3b8a7..68ab30a6b 100644 --- a/contrib/docker_notes.md +++ b/contrib/docker_notes.md @@ -1,4 +1,28 @@ -# Notes about using Docker in the build scripts +# Using the build scripts + +Most of our build scripts are docker-based. +(All, except the macOS build, which is a separate beast and always has to be special-cased +at the cost of significant maintenance burden...) + +Typically, the build flow is: + +- build a docker image, based on debian + - the apt sources mirror used is `snapshot.debian.org` + - (except for the source tarball build, which is simple enough not to need this) + - this helps with historical reproducibility + - note that `snapshot.debian.org` is often slow and sometimes keeps timing out :/ + (see #8496) + - a potential alternative would be `snapshot.notset.fr`, but that mirror is missing + e.g. `binary-i386`, which is needed for the wine/windows build. + - if you are just trying to build for yourself and don't need reproducibility, + you can just switch back to the default debian apt sources mirror. + - docker caches the build (locally), and so this step only needs to be rerun + if we update the Dockerfile. This caching happens automatically and by default. + - you can disable the caching by setting envvar `ELECBUILD_NOCACHE=1`. See below. +- create a docker container from the image, and build the final binary inside the container + + +## Notes about using Docker - To install Docker: @@ -18,4 +42,27 @@ $ sudo usermod -aG docker ${USER} ``` (and then reboot or similar for it to take effect) - + + +## Environment variables + +- `ELECBUILD_COMMIT` + + When unset or empty, we build directly from the local git clone. These builds + are *not* reproducible. + + When non-empty, it should be set to a git ref. We will create a fresh git clone + checked out at that reference in `/tmp/electrum_build/`, and build there. + +- `ELECBUILD_NOCACHE=1` + + A non-empty value forces a rebuild of the docker image. + + Before we started using `snapshot.debian.org` for apt sources, + setting this was necessary to properly test historical reproducibility. + (we were version-pinning packages installed using `apt`, but it was not realistic to + version-pin all transitive dependencies, and sometimes an update of those resulted in + changes to our binary builds) + + I think setting this is no longer necessary for building reproducibly. + From 759eaf1cf57c8743cee01e26dc116c5d92accc4f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 23 Jun 2023 12:16:14 +0200 Subject: [PATCH 0978/1143] json_db: register extra types outside of constructor --- electrum/json_db.py | 23 +++++++++++------------ electrum/wallet_db.py | 29 +++++++++++++++-------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index 226c8480d..b4b56a38d 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -50,6 +50,17 @@ def wrapper(self, *args, **kwargs): registered_dict_keys = {} registered_parent_keys = {} +def register_dict(name, method, _type): + registered_dicts[name] = method, _type + +def register_name(name, method, _type): + registered_names[name] = method, _type + +def register_dict_key(name, method): + registered_dict_keys[name] = method + +def register_parent_key(name, method): + registered_parent_keys[name] = method def stored_as(name, _type=dict): """ decorator that indicates the storage key of a stored object""" @@ -212,18 +223,6 @@ def dump(self, *, human_readable: bool = True) -> str: def _should_convert_to_stored_dict(self, key) -> bool: return True - def register_dict(self, name, method, _type): - registered_dicts[name] = method, _type - - def register_name(self, name, method, _type): - registered_names[name] = method, _type - - def register_dict_key(self, name, method): - registered_dict_keys[name] = method - - def register_parent_key(self, name, method): - registered_parent_keys[name] = method - def _convert_dict(self, path, key, v): if key in registered_dicts: constructor, _type = registered_dicts[key] diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 2b38ad7de..cf889d453 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -43,6 +43,7 @@ from .logging import Logger from .lnutil import LOCAL, REMOTE, HTLCOwner, ChannelType +from . import json_db from .json_db import StoredDict, JsonDB, locked, modifier, StoredObject, stored_in, stored_as from .plugin import run_hook, plugin_loaders from .version import ELECTRUM_VERSION @@ -86,25 +87,25 @@ def to_str(self) -> str: # separate tracking issues class WalletFileExceptionVersion51(WalletFileException): pass +# register dicts that require value conversions not handled by constructor +json_db.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None) +json_db.register_dict('prevouts_by_scripthash', lambda x: set(tuple(k) for k in x), None) +json_db.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None) +# register dicts that require key conversion +for key in [ + 'adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets', + 'unacked_updates', 'unfulfilled_htlcs', 'fail_htlc_reasons', 'onion_keys']: + json_db.register_dict_key(key, int) +for key in ['log']: + json_db.register_dict_key(key, lambda x: HTLCOwner(int(x))) +for key in ['locked_in', 'fails', 'settles']: + json_db.register_parent_key(key, lambda x: HTLCOwner(int(x))) + class WalletDB(JsonDB): def __init__(self, raw, *, manual_upgrades: bool): JsonDB.__init__(self, {}) - # register dicts that require value conversions not handled by constructor - self.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None) - self.register_dict('prevouts_by_scripthash', lambda x: set(tuple(k) for k in x), None) - self.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None) - # register dicts that require key conversion - for key in [ - 'adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets', - 'unacked_updates', 'unfulfilled_htlcs', 'fail_htlc_reasons', 'onion_keys']: - self.register_dict_key(key, int) - for key in ['log']: - self.register_dict_key(key, lambda x: HTLCOwner(int(x))) - for key in ['locked_in', 'fails', 'settles']: - self.register_parent_key(key, lambda x: HTLCOwner(int(x))) - self._manual_upgrades = manual_upgrades self._called_after_upgrade_tasks = False if raw: # loading existing db From 8075c0d02a3e8f5ed409dd7de094f181df92d52d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 23 Jun 2023 16:01:03 +0000 Subject: [PATCH 0979/1143] lnurl: add encode_lnurl() for console usage, fix tests --- electrum/lnurl.py | 18 ++++++++++++++++-- electrum/tests/test_lnurl.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/electrum/lnurl.py b/electrum/lnurl.py index 67e942790..d6d1bcc8b 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -11,8 +11,9 @@ import aiohttp.client_exceptions from aiohttp import ClientResponse -from electrum.segwit_addr import bech32_decode, Encoding, convertbits -from electrum.lnaddr import LnDecodeException +from electrum import segwit_addr +from electrum.segwit_addr import bech32_decode, Encoding, convertbits, bech32_encode +from electrum.lnaddr import LnDecodeException, LnEncodeException from electrum.network import Network from electrum.logging import get_logger @@ -45,6 +46,19 @@ def decode_lnurl(lnurl: str) -> str: return url +def encode_lnurl(url: str) -> str: + """Encode url to bech32 lnurl string.""" + try: + url = url.encode("utf-8") + except UnicodeError as e: + raise LnEncodeException("invalid url") from e + bech32_data = convertbits(url, 8, 5, True) + assert bech32_data + lnurl = bech32_encode( + encoding=segwit_addr.Encoding.BECH32, hrp="lnurl", data=bech32_data) + return lnurl.upper() + + def _is_url_safe_enough_for_lnurl(url: str) -> bool: u = urllib.parse.urlparse(url) if u.scheme.lower() == "https": diff --git a/electrum/tests/test_lnurl.py b/electrum/tests/test_lnurl.py index 48ef8a3a5..88c9238e2 100644 --- a/electrum/tests/test_lnurl.py +++ b/electrum/tests/test_lnurl.py @@ -9,4 +9,14 @@ def test_decode(self): "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9" ) url = lnurl.decode_lnurl(LNURL) - self.assertTrue("https://service.io/?q=3fc3645b439ce8e7", url) + self.assertEqual("https://service.io/?q=3fc3645b439ce8e7", url) + + def test_encode(self): + lnurl_ = lnurl.encode_lnurl("https://jhoenicke.de/.well-known/lnurlp/mempool") + self.assertEqual( + "LNURL1DP68GURN8GHJ76NGDAJKU6TRDDJJUER99UH8WETVDSKKKMN0WAHZ7MRWW4EXCUP0D4JK6UR0DAKQHMHNX2", + lnurl_) + + def test_lightning_address_to_url(self): + url = lnurl.lightning_address_to_url("mempool@jhoenicke.de") + self.assertEqual("https://jhoenicke.de/.well-known/lnurlp/mempool", url) From 888291a8f82136c06adb2f5e378fb8d0afef1b44 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 23 Jun 2023 16:23:12 +0000 Subject: [PATCH 0980/1143] qml: fix lnurl-pay when config.BTC_AMOUNTS_ADD_THOUSANDS_SEP is True when paying an lnurl-pay that provides an amount interval, the amount field is editable by the user and it expects no spaces --- electrum/gui/qml/components/LnurlPayRequestDialog.qml | 2 +- electrum/gui/qml/qeconfig.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/LnurlPayRequestDialog.qml b/electrum/gui/qml/components/LnurlPayRequestDialog.qml index 6cc2030df..c920f71cd 100644 --- a/electrum/gui/qml/components/LnurlPayRequestDialog.qml +++ b/electrum/gui/qml/components/LnurlPayRequestDialog.qml @@ -64,7 +64,7 @@ ElDialog { BtcField { id: amountBtc Layout.preferredWidth: rootLayout.width /3 - text: Config.formatSats(invoiceParser.lnurlData['min_sendable_sat']) + text: Config.formatSatsForEditing(invoiceParser.lnurlData['min_sendable_sat']) enabled: invoiceParser.lnurlData['min_sendable_sat'] != invoiceParser.lnurlData['max_sendable_sat'] color: Material.foreground // override gray-out on disabled fiatfield: amountFiat diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py index 159e3e0d1..49456c3fb 100644 --- a/electrum/gui/qml/qeconfig.py +++ b/electrum/gui/qml/qeconfig.py @@ -211,6 +211,15 @@ def userKnowsPressAndHold(self, userKnowsPressAndHold): self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD = userKnowsPressAndHold self.userKnowsPressAndHoldChanged.emit() + @pyqtSlot('qint64', result=str) + @pyqtSlot(QEAmount, result=str) + def formatSatsForEditing(self, satoshis): + if isinstance(satoshis, QEAmount): + satoshis = satoshis.satsInt + return self.config.format_amount( + satoshis, + add_thousands_sep=False, + ) @pyqtSlot('qint64', result=str) @pyqtSlot('qint64', bool, result=str) From ca93af2b8a1dcf3f13ceeed4f0e30a807f62bff3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 23 Jun 2023 19:51:57 +0000 Subject: [PATCH 0981/1143] ln: some clean-up for option_scid_alias - qt chan details dlg: show both local and remote aliases - lnchannel: more descriptive names, add clarification in doctstrings, and also save the "local_scid_alias" in the wallet file (to remember if we sent it) - lnpeer: - resend channel_ready msg after reestablish, to upgrade old existing channels to having local_scid_alias - forwarding bugfix, to follow BOLT-04: > - if it returns a `channel_update`: > - MUST set `short_channel_id` to the `short_channel_id` used by the incoming onion. --- electrum/gui/qt/channel_details.py | 9 +++---- electrum/lnchannel.py | 38 ++++++++++++++++++++++-------- electrum/lnpeer.py | 18 ++++++++------ electrum/lnworker.py | 4 ++-- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py index 970ce9ed9..39d3ebde4 100644 --- a/electrum/gui/qt/channel_details.py +++ b/electrum/gui/qt/channel_details.py @@ -5,7 +5,7 @@ import PyQt5.QtCore as QtCore from PyQt5.QtWidgets import QLabel, QLineEdit, QHBoxLayout, QGridLayout -from electrum.util import EventListener +from electrum.util import EventListener, ShortID from electrum.i18n import _ from electrum.util import format_time from electrum.lnutil import format_short_channel_id, LOCAL, REMOTE, UpdateAddHtlc, Direction @@ -181,9 +181,10 @@ def get_common_form(self, chan): channel_id_e = ShowQRLineEdit(chan.channel_id.hex(), self.window.config, title=_("Channel ID")) form.addRow(QLabel(_('Channel ID') + ':'), channel_id_e) form.addRow(QLabel(_('Short Channel ID') + ':'), QLabel(str(chan.short_channel_id))) - alias = chan.get_remote_alias() - if alias: - form.addRow(QLabel(_('Alias') + ':'), QLabel('0x'+alias.hex())) + if local_scid_alias := chan.get_local_scid_alias(): + form.addRow(QLabel('Local SCID Alias:'), QLabel(str(ShortID(local_scid_alias)))) + if remote_scid_alias := chan.get_remote_scid_alias(): + form.addRow(QLabel('Remote SCID Alias:'), QLabel(str(ShortID(remote_scid_alias)))) form.addRow(QLabel(_('State') + ':'), SelectableLabel(chan.get_state_for_GUI())) self.capacity = self.format_sat(chan.get_capacity()) form.addRow(QLabel(_('Capacity') + ':'), SelectableLabel(self.capacity)) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 1825addc9..f84b79044 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -532,7 +532,7 @@ def get_capacity(self): def is_backup(self): return True - def get_remote_alias(self) -> Optional[bytes]: + def get_remote_scid_alias(self) -> Optional[bytes]: return None def create_sweeptxs_for_their_ctx(self, ctx): @@ -640,15 +640,27 @@ def __init__(self, state: 'StoredDict', *, name=None, lnworker=None, initial_fee self.should_request_force_close = False self.unconfirmed_closing_txid = None # not a state, only for GUI - def get_local_alias(self) -> bytes: - # deterministic, same secrecy level as wallet master pubkey - wallet_fingerprint = bytes(self.lnworker.wallet.get_fingerprint(), "utf8") - return sha256(wallet_fingerprint + self.channel_id)[0:8] + def get_local_scid_alias(self, *, create_new_if_needed: bool = False) -> Optional[bytes]: + """Get scid_alias to be used for *outgoing* HTLCs. + (called local as we choose the value) + """ + if alias := self.storage.get('local_scid_alias'): + return bytes.fromhex(alias) + elif create_new_if_needed: + # deterministic, same secrecy level as wallet master pubkey + wallet_fingerprint = bytes(self.lnworker.wallet.get_fingerprint(), "utf8") + alias = sha256(wallet_fingerprint + self.channel_id)[0:8] + self.storage['local_scid_alias'] = alias.hex() + return alias + return None - def save_remote_alias(self, alias: bytes): + def save_remote_scid_alias(self, alias: bytes): self.storage['alias'] = alias.hex() - def get_remote_alias(self) -> Optional[bytes]: + def get_remote_scid_alias(self) -> Optional[bytes]: + """Get scid_alias to be used for *incoming* HTLCs. + (called remote as the remote chooses the value) + """ alias = self.storage.get('alias') return bytes.fromhex(alias) if alias else None @@ -697,6 +709,7 @@ def set_remote_update(self, payload: dict) -> None: This message contains info we need to populate private route hints when creating invoices. """ + assert payload['short_channel_id'] in [self.short_channel_id, self.get_local_scid_alias()] from .channel_db import ChannelDB ChannelDB.verify_channel_update(payload, start_node=self.node_id) raw = payload['raw'] @@ -719,11 +732,16 @@ def get_peer_addresses(self) -> Iterator[LNPeerAddr]: net_addr = NetAddress.from_string(net_addr_str) yield LNPeerAddr(host=str(net_addr.host), port=net_addr.port, pubkey=self.node_id) - def get_outgoing_gossip_channel_update(self) -> bytes: - if self._outgoing_channel_update is not None: + def get_outgoing_gossip_channel_update(self, *, scid: ShortChannelID = None) -> bytes: + """ + scid: to be put into the channel_update message instead of the real scid, as this might be an scid alias + """ + if self._outgoing_channel_update is not None and scid is None: return self._outgoing_channel_update if not self.lnworker: raise Exception('lnworker not set for channel!') + if scid is None: + scid = self.short_channel_id sorted_node_ids = list(sorted([self.node_id, self.get_local_pubkey()])) channel_flags = b'\x00' if sorted_node_ids[0] == self.get_local_pubkey() else b'\x01' now = int(time.time()) @@ -731,7 +749,7 @@ def get_outgoing_gossip_channel_update(self) -> bytes: chan_upd = encode_msg( "channel_update", - short_channel_id=self.short_channel_id, + short_channel_id=scid, channel_flags=channel_flags, message_flags=b'\x01', cltv_expiry_delta=self.forwarding_cltv_expiry_delta, diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 488bb13da..0b3b2561e 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -385,7 +385,7 @@ def maybe_save_remote_update(self, payload): if not self.channels: return for chan in self.channels.values(): - if payload['short_channel_id'] in [chan.short_channel_id, chan.get_local_alias()]: + if payload['short_channel_id'] in [chan.short_channel_id, chan.get_local_scid_alias()]: chan.set_remote_update(payload) self.logger.info(f"saved remote channel_update gossip msg for chan {chan.get_id_for_log()}") break @@ -1272,7 +1272,10 @@ def resend_revoke_and_ack(): resend_revoke_and_ack() chan.peer_state = PeerState.GOOD + chan_just_became_ready = False if chan.is_funded() and their_next_local_ctn == next_local_ctn == 1: + chan_just_became_ready = True + if chan_just_became_ready or self.features.supports(LnFeatures.OPTION_SCID_ALIAS_OPT): self.send_channel_ready(chan) # checks done if chan.is_funded() and chan.config[LOCAL].funding_locked_received: @@ -1289,11 +1292,11 @@ def send_channel_ready(self, chan: Channel): get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big')) channel_ready_tlvs = {} - if self.their_features.supports(LnFeatures.OPTION_SCID_ALIAS_OPT): + if self.features.supports(LnFeatures.OPTION_SCID_ALIAS_OPT): # LND requires that we send an alias if the option has been negotiated in INIT. # otherwise, the channel will not be marked as active. # This does not apply if the channel was previously marked active without an alias. - channel_ready_tlvs['short_channel_id'] = {'alias':chan.get_local_alias()} + channel_ready_tlvs['short_channel_id'] = {'alias': chan.get_local_scid_alias(create_new_if_needed=True)} # note: if 'channel_ready' was not yet received, we might send it multiple times self.send_message( @@ -1309,7 +1312,7 @@ def on_channel_ready(self, chan: Channel, payload): # save remote alias for use in invoices scid_alias = payload.get('channel_ready_tlvs', {}).get('short_channel_id', {}).get('alias') if scid_alias: - chan.save_remote_alias(scid_alias) + chan.save_remote_scid_alias(scid_alias) if not chan.config[LOCAL].funding_locked_received: their_next_point = payload["second_per_commitment_point"] @@ -1593,15 +1596,16 @@ def log_fail_reason(reason: str): if chain.is_tip_stale(): raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') try: - next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] # type: bytes + _next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"] # type: bytes + next_chan_scid = ShortChannelID(_next_chan_scid) except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid) local_height = chain.height() if next_chan is None: - log_fail_reason(f"cannot find next_chan {next_chan_scid.hex()}") + log_fail_reason(f"cannot find next_chan {next_chan_scid}") raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') - outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:] + outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update(scid=next_chan_scid)[2:] outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big") outgoing_chan_upd_message = outgoing_chan_upd_len + outgoing_chan_upd if not next_chan.can_send_update_add_htlc(): diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 011cb831a..89ca65136 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1729,7 +1729,7 @@ def create_route_for_payment( my_sending_channels: List[Channel], full_path: Optional[LNPaymentPath]) -> LNPaymentRoute: - my_sending_aliases = set(chan.get_local_alias() for chan in my_sending_channels) + my_sending_aliases = set(chan.get_local_scid_alias() for chan in my_sending_channels) my_sending_channels = {chan.short_channel_id: chan for chan in my_sending_channels if chan.short_channel_id is not None} # Collect all private edges from route hints. @@ -2049,7 +2049,7 @@ def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=No scid_to_my_channels = {chan.short_channel_id: chan for chan in channels if chan.short_channel_id is not None} for chan in channels: - alias_or_scid = chan.get_remote_alias() or chan.short_channel_id + alias_or_scid = chan.get_remote_scid_alias() or chan.short_channel_id assert isinstance(alias_or_scid, bytes), alias_or_scid channel_info = get_mychannel_info(chan.short_channel_id, scid_to_my_channels) # note: as a fallback, if we don't have a channel update for the From f7a8e55a6aa76a2c3eda3173912889fcf5ce8e32 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Jun 2023 14:03:24 +0000 Subject: [PATCH 0982/1143] lnworker: (trivial) clean-up which bolt-09 feature flags we set --- electrum/lnworker.py | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 89ca65136..8ece6a28e 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -172,27 +172,33 @@ class ErrorAddingPeer(Exception): pass # set some feature flags as baseline for both LNWallet and LNGossip # note that e.g. DATA_LOSS_PROTECT is needed for LNGossip as many peers require it -BASE_FEATURES = LnFeatures(0)\ - | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT\ - | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT\ - | LnFeatures.VAR_ONION_OPT\ - | LnFeatures.PAYMENT_SECRET_OPT\ - | LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT\ +BASE_FEATURES = ( + LnFeatures(0) + | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT + | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT + | LnFeatures.VAR_ONION_OPT + | LnFeatures.PAYMENT_SECRET_OPT + | LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT +) # we do not want to receive unrequested gossip (see lnpeer.maybe_save_remote_update) -LNWALLET_FEATURES = BASE_FEATURES\ - | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ\ - | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ\ - | LnFeatures.GOSSIP_QUERIES_REQ\ - | LnFeatures.BASIC_MPP_OPT\ - | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM\ - | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\ - | LnFeatures.OPTION_CHANNEL_TYPE_OPT\ - | LnFeatures.OPTION_SCID_ALIAS_OPT\ - -LNGOSSIP_FEATURES = BASE_FEATURES\ - | LnFeatures.GOSSIP_QUERIES_OPT\ - | LnFeatures.GOSSIP_QUERIES_REQ\ +LNWALLET_FEATURES = ( + BASE_FEATURES + | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ + | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ + | LnFeatures.GOSSIP_QUERIES_REQ + | LnFeatures.BASIC_MPP_OPT + | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM + | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT + | LnFeatures.OPTION_CHANNEL_TYPE_OPT + | LnFeatures.OPTION_SCID_ALIAS_OPT +) + +LNGOSSIP_FEATURES = ( + BASE_FEATURES + | LnFeatures.GOSSIP_QUERIES_OPT + | LnFeatures.GOSSIP_QUERIES_REQ +) class LNWorker(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): From a3b0e97c886fa058672677ed57c60a6de8a5e0a1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Jun 2023 16:59:13 +0000 Subject: [PATCH 0983/1143] lnaddr: make min_cltv logic less error-prone round-tripping the value behaved unexpectedly before... --- electrum/lnaddr.py | 8 +++++--- electrum/tests/test_bolt11.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index adaadde58..8689be0a7 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -272,7 +272,6 @@ def __init__(self, *, paymenthash: bytes = None, amount=None, net: Type[Abstract self.pubkey = None self.net = constants.net if net is None else net # type: Type[AbstractNet] self._amount = amount # type: Optional[Decimal] # in bitcoins - self._min_final_cltv_expiry = 18 @property def amount(self) -> Optional[Decimal]: @@ -326,7 +325,10 @@ def __str__(self): ) def get_min_final_cltv_expiry(self) -> int: - return self._min_final_cltv_expiry + cltv = self.get_tag('c') + if cltv is None: + return 18 + return int(cltv) def get_tag(self, tag): for k, v in self.tags: @@ -482,7 +484,7 @@ def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr: addr.pubkey = pubkeybytes elif tag == 'c': - addr._min_final_cltv_expiry = tagdata.uint + addr.tags.append(('c', tagdata.uint)) elif tag == '9': features = tagdata.uint diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py index 677371102..7265bc874 100644 --- a/electrum/tests/test_bolt11.py +++ b/electrum/tests/test_bolt11.py @@ -146,6 +146,7 @@ def test_min_final_cltv_expiry_decoding(self): def test_min_final_cltv_expiry_roundtrip(self): for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025): lnaddr = LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv)]) + self.assertEqual(cltv, lnaddr.get_min_final_cltv_expiry()) invoice = lnencode(lnaddr, PRIVKEY) self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_expiry()) From e2ee79c378191669fd900cc628683523ff59bd29 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Jun 2023 17:08:41 +0000 Subject: [PATCH 0984/1143] lnaddr: add LnAddr.to_debug_json() method --- electrum/invoices.py | 12 +----------- electrum/lnaddr.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/electrum/invoices.py b/electrum/invoices.py index b184388b3..6f7300e36 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -290,17 +290,7 @@ def can_be_paid_onchain(self) -> bool: def to_debug_json(self) -> Dict[str, Any]: d = self.to_json() - d.update({ - 'pubkey': self._lnaddr.pubkey.serialize().hex(), - 'amount_BTC': str(self._lnaddr.amount), - 'rhash': self._lnaddr.paymenthash.hex(), - 'description': self._lnaddr.get_description(), - 'exp': self._lnaddr.get_expiry(), - 'time': self._lnaddr.date, - }) - if ln_routing_info := self._lnaddr.get_routing_info('r'): - # show the last hop of routing hints. (our invoices only have one hop) - d['r_tags'] = [str((a.hex(),b.hex(),c,d,e)) for a,b,c,d,e in ln_routing_info[-1]] + d["lnaddr"] = self._lnaddr.to_debug_json() return d diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index 8689be0a7..fea9e2ce6 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -6,7 +6,7 @@ from hashlib import sha256 from binascii import hexlify from decimal import Decimal -from typing import Optional, TYPE_CHECKING, Type +from typing import Optional, TYPE_CHECKING, Type, Dict, Any import random import bitstring @@ -354,6 +354,25 @@ def is_expired(self) -> bool: # we treat it as 0 seconds here (instead of never) return now > self.get_expiry() + self.date + def to_debug_json(self) -> Dict[str, Any]: + d = { + 'pubkey': self.pubkey.serialize().hex(), + 'amount_BTC': str(self.amount), + 'rhash': self.paymenthash.hex(), + 'payment_secret': self.payment_secret.hex() if self.payment_secret else None, + 'description': self.get_description(), + 'exp': self.get_expiry(), + 'time': self.date, + 'min_final_cltv_expiry': self.get_min_final_cltv_expiry(), + 'features': self.get_features().get_names(), + 'tags': self.tags, + 'unknown_tags': self.unknown_tags, + } + if ln_routing_info := self.get_routing_info('r'): + # show the last hop of routing hints. (our invoices only have one hop) + d['r_tags'] = [str((a.hex(),b.hex(),c,d,e)) for a,b,c,d,e in ln_routing_info[-1]] + return d + class SerializableKey: def __init__(self, pubkey): From c85139009a84539e891fcdf01ee2ed35c1cdbee2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 23 Jun 2023 20:02:25 +0000 Subject: [PATCH 0985/1143] qt chan details dlg: make some more labels selectable --- electrum/gui/qt/channel_details.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py index 39d3ebde4..2bcc079e1 100644 --- a/electrum/gui/qt/channel_details.py +++ b/electrum/gui/qt/channel_details.py @@ -180,11 +180,11 @@ def get_common_form(self, chan): form.addRow(QLabel(_('Remote Node') + ':'), remote_id_e) channel_id_e = ShowQRLineEdit(chan.channel_id.hex(), self.window.config, title=_("Channel ID")) form.addRow(QLabel(_('Channel ID') + ':'), channel_id_e) - form.addRow(QLabel(_('Short Channel ID') + ':'), QLabel(str(chan.short_channel_id))) + form.addRow(QLabel(_('Short Channel ID') + ':'), SelectableLabel(str(chan.short_channel_id))) if local_scid_alias := chan.get_local_scid_alias(): - form.addRow(QLabel('Local SCID Alias:'), QLabel(str(ShortID(local_scid_alias)))) + form.addRow(QLabel('Local SCID Alias:'), SelectableLabel(str(ShortID(local_scid_alias)))) if remote_scid_alias := chan.get_remote_scid_alias(): - form.addRow(QLabel('Remote SCID Alias:'), QLabel(str(ShortID(remote_scid_alias)))) + form.addRow(QLabel('Remote SCID Alias:'), SelectableLabel(str(ShortID(remote_scid_alias)))) form.addRow(QLabel(_('State') + ':'), SelectableLabel(chan.get_state_for_GUI())) self.capacity = self.format_sat(chan.get_capacity()) form.addRow(QLabel(_('Capacity') + ':'), SelectableLabel(self.capacity)) From 411098f293afd4898804be20a7693af8f3fe183e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 24 Jun 2023 12:12:32 +0200 Subject: [PATCH 0986/1143] move methods from wallet_db to json_db the goal of this commit is to call JsonDB.__init__ with data, not an empty dict --- electrum/json_db.py | 30 ++++++++++++++++++++++++++- electrum/wallet_db.py | 47 ++++++++++++++----------------------------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index b4b56a38d..cb1d8adb7 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -27,6 +27,7 @@ import json from . import util +from .util import WalletFileException from .logging import Logger JsonDBJsonEncoder = util.MyEncoder @@ -166,8 +167,20 @@ class JsonDB(Logger): def __init__(self, data): Logger.__init__(self) self.lock = threading.RLock() - self.data = data self._modified = False + # load data + if data: + self.load_data(data) + else: + self.data = {} + + def load_data(self, s): + try: + self.data = json.loads(s) + except Exception: + raise WalletFileException("Cannot read wallet file. (parsing failed)") + if not isinstance(self.data, dict): + raise WalletFileException("Malformed wallet file (not dict)") def set_modified(self, b): with self.lock: @@ -250,3 +263,18 @@ def _convert_value(self, path, key, v): else: v = constructor(v) return v + + def write(self, storage: 'WalletStorage'): + with self.lock: + self._write(storage) + + def _write(self, storage: 'WalletStorage'): + if threading.current_thread().daemon: + self.logger.warning('daemon thread cannot write db') + return + if not self.modified(): + return + json_str = self.dump(human_readable=not storage.is_encrypted()) + storage.write(json_str) + self.set_modified(False) + diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index cf889d453..255faed4a 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -104,21 +104,27 @@ class WalletFileExceptionVersion51(WalletFileException): pass class WalletDB(JsonDB): - def __init__(self, raw, *, manual_upgrades: bool): - JsonDB.__init__(self, {}) - self._manual_upgrades = manual_upgrades - self._called_after_upgrade_tasks = False - if raw: # loading existing db - self.load_data(raw) - self.load_plugins() - else: # creating new db + def __init__(self, data, *, manual_upgrades: bool): + JsonDB.__init__(self, data) + if not data: + # create new DB self.put('seed_version', FINAL_SEED_VERSION) self._add_db_creation_metadata() self._after_upgrade_tasks() + self._manual_upgrades = manual_upgrades + self._called_after_upgrade_tasks = False + if not self._manual_upgrades and self.requires_split(): + raise WalletFileException("This wallet has multiple accounts and must be split") + if not self.requires_upgrade(): + self._after_upgrade_tasks() + elif not self._manual_upgrades: + self.upgrade() + # load plugins that are conditional on wallet type + self.load_plugins() def load_data(self, s): try: - self.data = json.loads(s) + JsonDB.load_data(self, s) except Exception: try: d = ast.literal_eval(s) @@ -137,14 +143,6 @@ def load_data(self, s): if not isinstance(self.data, dict): raise WalletFileException("Malformed wallet file (not dict)") - if not self._manual_upgrades and self.requires_split(): - raise WalletFileException("This wallet has multiple accounts and must be split") - - if not self.requires_upgrade(): - self._after_upgrade_tasks() - elif not self._manual_upgrades: - self.upgrade() - def requires_split(self): d = self.get('accounts', {}) return len(d) > 1 @@ -1583,21 +1581,6 @@ def _should_convert_to_stored_dict(self, key) -> bool: return False return True - def write(self, storage: 'WalletStorage'): - with self.lock: - self._write(storage) - - @profiler - def _write(self, storage: 'WalletStorage'): - if threading.current_thread().daemon: - self.logger.warning('daemon thread cannot write db') - return - if not self.modified(): - return - json_str = self.dump(human_readable=not storage.is_encrypted()) - storage.write(json_str) - self.set_modified(False) - def is_ready_to_be_used_by_wallet(self): return not self.requires_upgrade() and self._called_after_upgrade_tasks From b8b36c7c30a4648498a88804a40fa172fb95a26b Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 24 Jun 2023 12:56:55 +0200 Subject: [PATCH 0987/1143] follow-up prev: fix flake8 test --- electrum/json_db.py | 3 +++ electrum/wallet_db.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index cb1d8adb7..850b46109 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -30,6 +30,9 @@ from .util import WalletFileException from .logging import Logger +if TYPE_CHECKING: + from .storage import WalletStorage + JsonDBJsonEncoder = util.MyEncoder def modifier(func): diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 255faed4a..6ef2fe341 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -48,8 +48,6 @@ from .plugin import run_hook, plugin_loaders from .version import ELECTRUM_VERSION -if TYPE_CHECKING: - from .storage import WalletStorage # seed_version is now used for the version of the wallet file From 97768a13b99023d25b0402ca9ef3914dd381af8c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 24 Jun 2023 13:00:15 +0200 Subject: [PATCH 0988/1143] follow-up previous commit --- electrum/json_db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/json_db.py b/electrum/json_db.py index 850b46109..1d1a9ce9f 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -25,6 +25,7 @@ import threading import copy import json +from typing import TYPE_CHECKING from . import util from .util import WalletFileException From 21e06b7065590bdca430bbba26fcda0fbf9fa2a8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 13 Jun 2023 17:19:23 +0200 Subject: [PATCH 0989/1143] lnpeer: new payment secret, derived without preimage. (this is needed for hold invoices) --- electrum/lnpeer.py | 8 ++++++-- electrum/lnutil.py | 1 + electrum/lnworker.py | 10 ++++++---- electrum/tests/test_lnpeer.py | 2 ++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 0b3b2561e..8dc0ebe0b 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1826,8 +1826,12 @@ def log_fail_reason(reason: str): raise exc_incorrect_or_unknown_pd preimage = self.lnworker.get_preimage(htlc.payment_hash) if payment_secret_from_onion: - if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage): - log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {derive_payment_secret_from_payment_preimage(preimage).hex()}') + expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)] + if preimage: + # legacy secret for old invoices + expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) + if payment_secret_from_onion not in expected_payment_secrets: + log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}') raise exc_incorrect_or_unknown_pd invoice_msat = info.amount_msat if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat): diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 93ec18b1e..2c3b6b1bb 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1538,6 +1538,7 @@ class LnKeyFamily(IntEnum): REVOCATION_ROOT = 5 | BIP32_PRIME NODE_KEY = 6 BACKUP_CIPHER = 7 | BIP32_PRIME + PAYMENT_SECRET_KEY = 8 | BIP32_PRIME def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 8ece6a28e..16af82587 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -217,6 +217,7 @@ def __init__(self, xprv, features: LnFeatures): self.lock = threading.RLock() self.node_keypair = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.NODE_KEY) self.backup_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.BACKUP_CIPHER).privkey + self.payment_secret_key = generate_keypair(BIP32Node.from_xkey(xprv), LnKeyFamily.PAYMENT_SECRET_KEY).privkey self._peers = {} # type: Dict[bytes, Peer] # pubkey -> Peer # needs self.lock self.taskgroup = OldTaskGroup() self.listen_server = None # type: Optional[asyncio.AbstractServer] @@ -1824,9 +1825,7 @@ def get_bolt11_invoice( routing_hints, trampoline_hints = self.calc_routing_hints_for_invoice(amount_msat, channels=channels) self.logger.info(f"creating bolt11 invoice with routing_hints: {routing_hints}") invoice_features = self.features.for_invoice() - payment_preimage = self.get_preimage(payment_hash) - if payment_preimage is None: # e.g. when export/importing requests between wallets - raise Exception("missing preimage for payment_hash") + payment_secret = self.get_payment_secret(payment_hash) amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None if expiry == 0: expiry = LN_EXPIRY_NEVER @@ -1843,12 +1842,15 @@ def get_bolt11_invoice( + routing_hints + trampoline_hints, date=timestamp, - payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage)) + payment_secret=payment_secret) invoice = lnencode(lnaddr, self.node_keypair.privkey) pair = lnaddr, invoice self._bolt11_cache[payment_hash] = pair return pair + def get_payment_secret(self, payment_hash): + return sha256(sha256(self.payment_secret_key) + payment_hash) + def create_payment_info(self, *, amount_msat: Optional[int], write_to_disk=True) -> bytes: payment_preimage = os.urandom(32) payment_hash = sha256(payment_preimage) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 99051c516..4fab6b85c 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -138,6 +138,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que Logger.__init__(self) NetworkRetryManager.__init__(self, max_retry_delay_normal=1, init_retry_delay_normal=1) self.node_keypair = local_keypair + self.payment_secret_key = os.urandom(256) # does not need to be deterministic in tests self._user_dir = tempfile.mkdtemp(prefix="electrum-lnpeer-test-") self.config = SimpleConfig({}, read_user_dir_function=lambda: self._user_dir) self.network = MockNetwork(tx_queue, config=self.config) @@ -239,6 +240,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln full_path=full_path)] get_payments = LNWallet.get_payments + get_payment_secret = LNWallet.get_payment_secret get_payment_info = LNWallet.get_payment_info save_payment_info = LNWallet.save_payment_info set_invoice_status = LNWallet.set_invoice_status From df5b98792ec15c92c9f600a84a3469925fca7e58 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 16 Jun 2023 11:20:11 +0200 Subject: [PATCH 0990/1143] lnworker: always call check_received_htlc (no only for MPP) This will be a generic placeholder to decide if we need to wait before settling a payment (to be used with hold invoices and bundled payments) --- electrum/lnpeer.py | 16 ++++++++-------- electrum/lnworker.py | 11 +++++++++-- electrum/tests/test_lnpeer.py | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 8dc0ebe0b..e94841f89 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1804,14 +1804,14 @@ def log_fail_reason(reason: str): # TODO fail here if invoice has set PAYMENT_SECRET_REQ payment_secret_from_onion = None - if total_msat > amt_to_forward: - mpp_status = self.lnworker.check_received_mpp_htlc(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) - if mpp_status is None: - return None, None - if mpp_status is False: - log_fail_reason(f"MPP_TIMEOUT") - raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') - assert mpp_status is True + payment_status = self.lnworker.check_received_htlc(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) + if payment_status is None: + return None, None + elif payment_status is False: + log_fail_reason(f"MPP_TIMEOUT") + raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') + else: + assert payment_status is True # if there is a trampoline_onion, maybe_fulfill_htlc will be called again if processed_onion.trampoline_onion_packet: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 16af82587..6e6f72b97 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1888,8 +1888,15 @@ def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> if write_to_disk: self.wallet.save_db() - def check_received_mpp_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddHtlc, expected_msat: int) -> Optional[bool]: - """ return MPP status: True (accepted), False (expired) or None """ + def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddHtlc, expected_msat: int) -> Optional[bool]: + """ return MPP status: True (accepted), False (expired) or None (waiting) + """ + + amt_to_forward = htlc.amount_msat # check this + if amt_to_forward >= expected_msat: + # not multi-part + return True + payment_hash = htlc.payment_hash is_expired, is_accepted, htlc_set = self.received_mpp_htlcs.get(payment_secret, (False, False, set())) if self.get_payment_status(payment_hash) == PR_PAID: diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 4fab6b85c..9a927dd7e 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -247,7 +247,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln set_request_status = LNWallet.set_request_status set_payment_status = LNWallet.set_payment_status get_payment_status = LNWallet.get_payment_status - check_received_mpp_htlc = LNWallet.check_received_mpp_htlc + check_received_htlc = LNWallet.check_received_htlc htlc_fulfilled = LNWallet.htlc_fulfilled htlc_failed = LNWallet.htlc_failed save_preimage = LNWallet.save_preimage From 2b1199647e06823db7a88a3f04270fef4421ffc9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 26 Jun 2023 09:33:25 +0200 Subject: [PATCH 0991/1143] bitcoin.construct_script: add values parameter (to be used in swapserver plugin) --- electrum/bitcoin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index aa73623f9..cb2431df5 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -317,10 +317,13 @@ def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: return witness -def construct_script(items: Sequence[Union[str, int, bytes, opcodes]]) -> str: +def construct_script(items: Sequence[Union[str, int, bytes, opcodes]], values=None) -> str: """Constructs bitcoin script from given items.""" script = '' - for item in items: + values = values or {} + for i, item in enumerate(items): + if i in values: + item = values[i] if isinstance(item, opcodes): script += item.hex() elif type(item) is int: From 1acf426fa9077e4537177938bd5ba8f7f6f77b4e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 15 Jun 2023 12:13:35 +0200 Subject: [PATCH 0992/1143] lnworker: add support for hold invoices (invoices for which we do not have the preimage) Callbacks and timeouts are registered with lnworker. If the preimage is not known after the timeout has expired, the payment is failed with MPP_TIMEOUT. --- electrum/lnworker.py | 20 +++++++++++++++++++- electrum/tests/test_lnpeer.py | 28 ++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 6e6f72b97..b76a50680 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -676,6 +676,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.trampoline_forwarding_failures = {} # todo: should be persisted # map forwarded htlcs (fw_info=(scid_hex, htlc_id)) to originating peer pubkeys self.downstream_htlc_to_upstream_peer_map = {} # type: Dict[Tuple[str, int], bytes] + self.hold_invoice_callbacks = {} # payment_hash -> callback, timeout def has_deterministic_node_id(self) -> bool: return bool(self.db.get('lightning_xprv')) @@ -1880,6 +1881,14 @@ def get_payment_info(self, payment_hash: bytes) -> Optional[PaymentInfo]: amount_msat, direction, status = self.payment_info[key] return PaymentInfo(payment_hash, amount_msat, direction, status) + def add_payment_info_for_hold_invoice(self, payment_hash, lightning_amount_sat): + info = PaymentInfo(payment_hash, lightning_amount_sat * 1000, RECEIVED, PR_UNPAID) + self.save_payment_info(info, write_to_disk=False) + + def register_callback_for_hold_invoice(self, payment_hash, cb, timeout: Optional[int] = None): + expiry = int(time.time()) + timeout + self.hold_invoice_callbacks[payment_hash] = cb, expiry + def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> None: key = info.payment_hash.hex() assert info.status in SAVED_PR_STATUS @@ -1891,13 +1900,22 @@ def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddHtlc, expected_msat: int) -> Optional[bool]: """ return MPP status: True (accepted), False (expired) or None (waiting) """ + payment_hash = htlc.payment_hash + preimage = self.get_preimage(payment_hash) + callback = self.hold_invoice_callbacks.get(payment_hash) + if not preimage and callback: + cb, timeout = callback + if int(time.time()) < timeout: + cb(payment_hash) + return None + else: + return False amt_to_forward = htlc.amount_msat # check this if amt_to_forward >= expected_msat: # not multi-part return True - payment_hash = htlc.payment_hash is_expired, is_accepted, htlc_set = self.received_mpp_htlcs.get(payment_secret, (False, False, set())) if self.get_payment_status(payment_hash) == PR_PAID: # payment_status is persisted diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 9a927dd7e..a383e9d73 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -173,6 +173,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.preimages = {} self.stopping_soon = False self.downstream_htlc_to_upstream_peer_map = {} + self.hold_invoice_callbacks = {} self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}") @@ -275,7 +276,8 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln _on_maybe_forwarded_htlc_resolved = LNWallet._on_maybe_forwarded_htlc_resolved _force_close_channel = LNWallet._force_close_channel suggest_splits = LNWallet.suggest_splits - + register_callback_for_hold_invoice = LNWallet.register_callback_for_hold_invoice + add_payment_info_for_hold_invoice = LNWallet.add_payment_info_for_hold_invoice class MockTransport: def __init__(self, name): @@ -731,10 +733,12 @@ async def turn_on_trampoline_alice(): async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash)) result, log = await w1.pay_invoice(pay_req) - self.assertTrue(result) - self.assertEqual(PR_PAID, w2.get_payment_status(lnaddr.paymenthash)) - raise PaymentDone() - async def f(): + if result is True: + self.assertEqual(PR_PAID, w2.get_payment_status(lnaddr.paymenthash)) + raise PaymentDone() + else: + raise PaymentFailure() + async def f(test_hold_invoice=False, test_timeout=False): if trampoline: await turn_on_trampoline_alice() async with OldTaskGroup() as group: @@ -744,6 +748,14 @@ async def f(): await group.spawn(p2.htlc_switch()) await asyncio.sleep(0.01) lnaddr, pay_req = self.prepare_invoice(w2) + if test_hold_invoice: + payment_hash = lnaddr.paymenthash + preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) + def cb(payment_hash): + if not test_timeout: + w2.save_preimage(payment_hash, preimage) + timeout = 1 if test_timeout else 60 + w2.register_callback_for_hold_invoice(payment_hash, cb, timeout) invoice_features = lnaddr.get_features() self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) await group.spawn(pay(lnaddr, pay_req)) @@ -752,7 +764,11 @@ async def f(): 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey), } with self.assertRaises(PaymentDone): - await f() + await f(test_hold_invoice=False) + with self.assertRaises(PaymentDone): + await f(test_hold_invoice=True, test_timeout=False) + with self.assertRaises(PaymentFailure): + await f(test_hold_invoice=True, test_timeout=True) @needs_test_with_all_chacha20_implementations async def test_simple_payment(self): From 14efb401d65f63a1c0c0b7d6d34e78cbb1ee6399 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Jun 2023 12:10:30 +0200 Subject: [PATCH 0993/1143] test_lnpeer: refactor tests for hold invoices --- electrum/tests/test_lnpeer.py | 46 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index a383e9d73..6a77aab7f 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -721,7 +721,7 @@ async def f(): with self.assertRaises(SuccessfulTest): await f() - async def _test_simple_payment(self, trampoline: bool): + async def _test_simple_payment(self, trampoline: bool, test_hold_invoice=False, test_timeout=False): """Alice pays Bob a single HTLC via direct channel.""" alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -738,7 +738,17 @@ async def pay(lnaddr, pay_req): raise PaymentDone() else: raise PaymentFailure() - async def f(test_hold_invoice=False, test_timeout=False): + lnaddr, pay_req = self.prepare_invoice(w2) + if test_hold_invoice: + payment_hash = lnaddr.paymenthash + preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) + def cb(payment_hash): + if not test_timeout: + w2.save_preimage(payment_hash, preimage) + timeout = 1 if test_timeout else 60 + w2.register_callback_for_hold_invoice(payment_hash, cb, timeout) + + async def f(): if trampoline: await turn_on_trampoline_alice() async with OldTaskGroup() as group: @@ -747,15 +757,6 @@ async def f(test_hold_invoice=False, test_timeout=False): await group.spawn(p2._message_loop()) await group.spawn(p2.htlc_switch()) await asyncio.sleep(0.01) - lnaddr, pay_req = self.prepare_invoice(w2) - if test_hold_invoice: - payment_hash = lnaddr.paymenthash - preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) - def cb(payment_hash): - if not test_timeout: - w2.save_preimage(payment_hash, preimage) - timeout = 1 if test_timeout else 60 - w2.register_callback_for_hold_invoice(payment_hash, cb, timeout) invoice_features = lnaddr.get_features() self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) await group.spawn(pay(lnaddr, pay_req)) @@ -763,20 +764,23 @@ def cb(payment_hash): electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey), } - with self.assertRaises(PaymentDone): - await f(test_hold_invoice=False) - with self.assertRaises(PaymentDone): - await f(test_hold_invoice=True, test_timeout=False) - with self.assertRaises(PaymentFailure): - await f(test_hold_invoice=True, test_timeout=True) + await f() @needs_test_with_all_chacha20_implementations async def test_simple_payment(self): - await self._test_simple_payment(trampoline=False) + for trampoline in [False, True]: + with self.assertRaises(PaymentDone): + await self._test_simple_payment(trampoline=trampoline) - @needs_test_with_all_chacha20_implementations - async def test_simple_payment_trampoline(self): - await self._test_simple_payment(trampoline=True) + async def test_simple_payment_with_hold_invoice(self): + for trampoline in [False, True]: + with self.assertRaises(PaymentDone): + await self._test_simple_payment(trampoline=trampoline, test_hold_invoice=True) + + async def test_simple_payment_with_hold_invoice_timing_out(self): + for trampoline in [False, True]: + with self.assertRaises(PaymentFailure): + await self._test_simple_payment(trampoline=trampoline, test_hold_invoice=True, test_timeout=True) @needs_test_with_all_chacha20_implementations async def test_payment_race(self): From 24296ca7c065bce8fcfaa65f2f4f0d596097395d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Jun 2023 12:49:30 +0200 Subject: [PATCH 0994/1143] test_lnpeer: follow-up 21e06b7065590bdca430bbba26fcda0fbf9fa2a8 --- electrum/tests/test_lnpeer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 6a77aab7f..a2782b0a8 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -36,7 +36,7 @@ from electrum.logging import console_stderr_handler, Logger from electrum.lnworker import PaymentInfo, RECEIVED from electrum.lnonion import OnionFailureCode -from electrum.lnutil import derive_payment_secret_from_payment_preimage, UpdateAddHtlc +from electrum.lnutil import UpdateAddHtlc from electrum.lnutil import LOCAL, REMOTE from electrum.invoices import PR_PAID, PR_UNPAID from electrum.interface import GracefulDisconnect @@ -538,7 +538,7 @@ def prepare_invoice( trampoline_hints = [] invoice_features = w2.features.for_invoice() if invoice_features.supports(LnFeatures.PAYMENT_SECRET_OPT): - payment_secret = derive_payment_secret_from_payment_preimage(payment_preimage) + payment_secret = w2.get_payment_secret(RHASH) else: payment_secret = None lnaddr1 = LnAddr( From 6c231e1d0779255d0d5ce3902e0b040684f3781a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Jun 2023 12:17:29 +0200 Subject: [PATCH 0995/1143] test_lnpeer: factorize code into TestPeer._activate_trampoline --- electrum/tests/test_lnpeer.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index a2782b0a8..6f9bd7d66 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -721,15 +721,16 @@ async def f(): with self.assertRaises(SuccessfulTest): await f() + async def _activate_trampoline(self, w): + if w.network.channel_db: + w.network.channel_db.stop() + await w.network.channel_db.stopped_event.wait() + w.network.channel_db = None + async def _test_simple_payment(self, trampoline: bool, test_hold_invoice=False, test_timeout=False): """Alice pays Bob a single HTLC via direct channel.""" alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) - async def turn_on_trampoline_alice(): - if w1.network.channel_db: - w1.network.channel_db.stop() - await w1.network.channel_db.stopped_event.wait() - w1.network.channel_db = None async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash)) result, log = await w1.pay_invoice(pay_req) @@ -750,7 +751,7 @@ def cb(payment_hash): async def f(): if trampoline: - await turn_on_trampoline_alice() + await self._activate_trampoline(w1) async with OldTaskGroup() as group: await group.spawn(p1._message_loop()) await group.spawn(p1.htlc_switch()) @@ -1143,11 +1144,6 @@ async def test_payment_multipart(self): async def _run_trampoline_payment(self, is_legacy, direct, drop_dave=None): if drop_dave is None: drop_dave = [] - async def turn_on_trampoline_alice(): - if graph.workers['alice'].network.channel_db: - graph.workers['alice'].network.channel_db.stop() - await graph.workers['alice'].network.channel_db.stopped_event.wait() - graph.workers['alice'].network.channel_db = None async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash)) @@ -1164,7 +1160,7 @@ def do_drop_dave(t): graph.workers[t].peers.pop(dave_node_id) async def f(): - await turn_on_trampoline_alice() + await self._activate_trampoline(graph.workers['alice']) async with OldTaskGroup() as group: for peer in peers: await group.spawn(peer._message_loop()) From c4eb7d83218680d991ab340ae58a45565d81f4ba Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 15 Jun 2023 12:00:56 +0200 Subject: [PATCH 0996/1143] lnworker: bundled payments - htlcs of bundled payments must arrive in the same MPP_TIMEOUT window, or they will be failed - add correspoding tests --- electrum/lnworker.py | 112 +++++++++++++++++++++++++--------- electrum/tests/test_lnpeer.py | 60 ++++++++++++++---- 2 files changed, 130 insertions(+), 42 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b76a50680..15d6bed85 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -677,6 +677,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): # map forwarded htlcs (fw_info=(scid_hex, htlc_id)) to originating peer pubkeys self.downstream_htlc_to_upstream_peer_map = {} # type: Dict[Tuple[str, int], bytes] self.hold_invoice_callbacks = {} # payment_hash -> callback, timeout + self.payment_bundles = [] # lists of hashes. todo:persist + def has_deterministic_node_id(self) -> bool: return bool(self.db.get('lightning_xprv')) @@ -1862,6 +1864,14 @@ def create_payment_info(self, *, amount_msat: Optional[int], write_to_disk=True) self.wallet.save_db() return payment_hash + def bundle_payments(self, hash_list): + self.payment_bundles.append(hash_list) + + def get_payment_bundle(self, payment_hash): + for hash_list in self.payment_bundles: + if payment_hash in hash_list: + return hash_list + def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True): assert sha256(preimage) == payment_hash self.preimages[payment_hash.hex()] = preimage.hex() @@ -1901,45 +1911,87 @@ def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddH """ return MPP status: True (accepted), False (expired) or None (waiting) """ payment_hash = htlc.payment_hash - preimage = self.get_preimage(payment_hash) - callback = self.hold_invoice_callbacks.get(payment_hash) - if not preimage and callback: - cb, timeout = callback - if int(time.time()) < timeout: - cb(payment_hash) - return None - else: - return False - amt_to_forward = htlc.amount_msat # check this - if amt_to_forward >= expected_msat: - # not multi-part - return True + self.update_mpp_with_received_htlc(payment_secret, short_channel_id, htlc, expected_msat) + is_expired, is_accepted = self.get_mpp_status(payment_secret) + if not is_accepted and not is_expired: + bundle = self.get_payment_bundle(payment_hash) + payment_hashes = bundle or [payment_hash] + payment_secrets = [self.get_payment_secret(h) for h in bundle] if bundle else [payment_secret] + first_timestamp = min([self.get_first_timestamp_of_mpp(x) for x in payment_secrets]) + if self.get_payment_status(payment_hash) == PR_PAID: + is_accepted = True + elif self.stopping_soon: + is_expired = True # try to time out pending HTLCs before shutting down + elif time.time() - first_timestamp > self.MPP_EXPIRY: + is_expired = True + elif all([self.is_mpp_amount_reached(x) for x in payment_secrets]): + preimage = self.get_preimage(payment_hash) + hold_invoice_callback = self.hold_invoice_callbacks.get(payment_hash) + if not preimage and hold_invoice_callback: + # for hold invoices, trigger callback + cb, timeout = hold_invoice_callback + if int(time.time()) < timeout: + cb(payment_hash) + else: + is_expired = True + elif bundle is not None: + is_accepted = all([bool(self.get_preimage(x)) for x in bundle]) + else: + # trampoline forwarding needs this to return True + is_accepted = True + + # set status for the bundle + if is_expired or is_accepted: + for x in payment_secrets: + if x in self.received_mpp_htlcs: + self.set_mpp_status(x, is_expired, is_accepted) - is_expired, is_accepted, htlc_set = self.received_mpp_htlcs.get(payment_secret, (False, False, set())) - if self.get_payment_status(payment_hash) == PR_PAID: - # payment_status is persisted - is_accepted = True - is_expired = False + self.maybe_cleanup_mpp_status(payment_secret, short_channel_id, htlc) + return True if is_accepted else (False if is_expired else None) + + def update_mpp_with_received_htlc(self, payment_secret, short_channel_id, htlc, expected_msat): + # add new htlc to set + is_expired, is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs.get(payment_secret, (False, False, expected_msat, set())) + assert expected_msat == _expected_msat key = (short_channel_id, htlc) if key not in htlc_set: htlc_set.add(key) + self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, _expected_msat, htlc_set + + def get_mpp_status(self, payment_secret): + is_expired, is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs[payment_secret] + return is_expired, is_accepted + + def set_mpp_status(self, payment_secret, is_expired, is_accepted): + _is_expired, _is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs[payment_secret] + self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, _expected_msat, htlc_set + + def is_mpp_amount_reached(self, payment_secret): + mpp = self.received_mpp_htlcs.get(payment_secret) + if not mpp: + return False + is_expired, is_accepted, _expected_msat, htlc_set = mpp + total = sum([_htlc.amount_msat for scid, _htlc in htlc_set]) + return total >= _expected_msat + + def get_first_timestamp_of_mpp(self, payment_secret): + mpp = self.received_mpp_htlcs.get(payment_secret) + if not mpp: + return int(time.time()) + is_expired, is_accepted, _expected_msat, htlc_set = mpp + return min([_htlc.timestamp for scid, _htlc in htlc_set]) + + def maybe_cleanup_mpp_status(self, payment_secret, short_channel_id, htlc): + is_expired, is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs[payment_secret] if not is_accepted and not is_expired: - total = sum([_htlc.amount_msat for scid, _htlc in htlc_set]) - first_timestamp = min([_htlc.timestamp for scid, _htlc in htlc_set]) - if self.stopping_soon: - is_expired = True # try to time out pending HTLCs before shutting down - elif time.time() - first_timestamp > self.MPP_EXPIRY: - is_expired = True - elif total == expected_msat: - is_accepted = True - if is_accepted or is_expired: - htlc_set.remove(key) + return + key = (short_channel_id, htlc) + htlc_set.remove(key) if len(htlc_set) > 0: - self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, htlc_set + self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, _expected_msat, htlc_set elif payment_secret in self.received_mpp_htlcs: self.received_mpp_htlcs.pop(payment_secret) - return True if is_accepted else (False if is_expired else None) def get_payment_status(self, payment_hash: bytes) -> int: info = self.get_payment_info(payment_hash) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 6f9bd7d66..964899b19 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -174,6 +174,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.stopping_soon = False self.downstream_htlc_to_upstream_peer_map = {} self.hold_invoice_callbacks = {} + self.payment_bundles = [] # lists of hashes. todo:persist self.logger.info(f"created LNWallet[{name}] with nodeID={local_keypair.pubkey.hex()}") @@ -279,6 +280,16 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln register_callback_for_hold_invoice = LNWallet.register_callback_for_hold_invoice add_payment_info_for_hold_invoice = LNWallet.add_payment_info_for_hold_invoice + update_mpp_with_received_htlc = LNWallet.update_mpp_with_received_htlc + get_mpp_status = LNWallet.get_mpp_status + set_mpp_status = LNWallet.set_mpp_status + is_mpp_amount_reached = LNWallet.is_mpp_amount_reached + get_first_timestamp_of_mpp = LNWallet.get_first_timestamp_of_mpp + maybe_cleanup_mpp_status = LNWallet.maybe_cleanup_mpp_status + bundle_payments = LNWallet.bundle_payments + get_payment_bundle = LNWallet.get_payment_bundle + + class MockTransport: def __init__(self, name): self.queue = asyncio.Queue() # incoming messages @@ -727,7 +738,14 @@ async def _activate_trampoline(self, w): await w.network.channel_db.stopped_event.wait() w.network.channel_db = None - async def _test_simple_payment(self, trampoline: bool, test_hold_invoice=False, test_timeout=False): + async def _test_simple_payment( + self, + test_trampoline: bool, + test_hold_invoice=False, + test_hold_timeout=False, + test_bundle=False, + test_bundle_timeout=False + ): """Alice pays Bob a single HTLC via direct channel.""" alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) @@ -746,12 +764,21 @@ async def pay(lnaddr, pay_req): def cb(payment_hash): if not test_timeout: w2.save_preimage(payment_hash, preimage) - timeout = 1 if test_timeout else 60 + timeout = 1 if test_hold_timeout else 60 w2.register_callback_for_hold_invoice(payment_hash, cb, timeout) + if test_bundle: + lnaddr2, pay_req2 = self.prepare_invoice(w2) + w2.bundle_payments([lnaddr.paymenthash, lnaddr2.paymenthash]) + + if test_trampoline: + await self._activate_trampoline(w1) + # declare bob as trampoline node + electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { + 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey), + } + async def f(): - if trampoline: - await self._activate_trampoline(w1) async with OldTaskGroup() as group: await group.spawn(p1._message_loop()) await group.spawn(p1.htlc_switch()) @@ -761,22 +788,31 @@ async def f(): invoice_features = lnaddr.get_features() self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) await group.spawn(pay(lnaddr, pay_req)) - # declare bob as trampoline node - electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { - 'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=w2.node_keypair.pubkey), - } + if test_bundle and not test_bundle_timeout: + await group.spawn(pay(lnaddr2, pay_req2)) + await f() @needs_test_with_all_chacha20_implementations async def test_simple_payment(self): - for trampoline in [False, True]: + for test_trampoline in [False, True]: + with self.assertRaises(PaymentDone): + await self._test_simple_payment(test_trampoline=test_trampoline) + + async def test_payment_bundle(self): + for test_trampoline in [False, True]: with self.assertRaises(PaymentDone): - await self._test_simple_payment(trampoline=trampoline) + await self._test_simple_payment(test_trampoline=test_trampoline, test_bundle=True) + + async def test_payment_bundle_timeout(self): + for test_trampoline in [False, True]: + with self.assertRaises(PaymentFailure): + await self._test_simple_payment(test_trampoline=test_trampoline, test_bundle=True, test_bundle_timeout=True) async def test_simple_payment_with_hold_invoice(self): - for trampoline in [False, True]: + for test_trampoline in [False, True]: with self.assertRaises(PaymentDone): - await self._test_simple_payment(trampoline=trampoline, test_hold_invoice=True) + await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True) async def test_simple_payment_with_hold_invoice_timing_out(self): for trampoline in [False, True]: From 7caa6ccf57bd0861a6fecfc11ac7d05a7b14272d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Jun 2023 13:22:24 +0200 Subject: [PATCH 0997/1143] test_lnpeer: fix variable names after rename --- electrum/tests/test_lnpeer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 964899b19..f78c04603 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -762,7 +762,7 @@ async def pay(lnaddr, pay_req): payment_hash = lnaddr.paymenthash preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) def cb(payment_hash): - if not test_timeout: + if not test_hold_timeout: w2.save_preimage(payment_hash, preimage) timeout = 1 if test_hold_timeout else 60 w2.register_callback_for_hold_invoice(payment_hash, cb, timeout) @@ -815,9 +815,9 @@ async def test_simple_payment_with_hold_invoice(self): await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True) async def test_simple_payment_with_hold_invoice_timing_out(self): - for trampoline in [False, True]: + for test_trampoline in [False, True]: with self.assertRaises(PaymentFailure): - await self._test_simple_payment(trampoline=trampoline, test_hold_invoice=True, test_timeout=True) + await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True, test_hold_timeout=True) @needs_test_with_all_chacha20_implementations async def test_payment_race(self): From c4c2123b4b2a0107c20e35daea69ec41e44227b2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Jun 2023 14:25:53 +0200 Subject: [PATCH 0998/1143] fix bundled payments: - prepayment should be accepted immediately once bundle is here - mpp timeout all parts, but accept only current part --- electrum/lnworker.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 15d6bed85..468596ace 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1923,8 +1923,6 @@ def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddH is_accepted = True elif self.stopping_soon: is_expired = True # try to time out pending HTLCs before shutting down - elif time.time() - first_timestamp > self.MPP_EXPIRY: - is_expired = True elif all([self.is_mpp_amount_reached(x) for x in payment_secrets]): preimage = self.get_preimage(payment_hash) hold_invoice_callback = self.hold_invoice_callbacks.get(payment_hash) @@ -1935,14 +1933,18 @@ def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddH cb(payment_hash) else: is_expired = True - elif bundle is not None: - is_accepted = all([bool(self.get_preimage(x)) for x in bundle]) else: - # trampoline forwarding needs this to return True + # note: preimage will be None for outer trampoline onion is_accepted = True - # set status for the bundle - if is_expired or is_accepted: + elif time.time() - first_timestamp > self.MPP_EXPIRY: + is_expired = True + + if is_accepted: + # accept only the current part of a bundle + self.set_mpp_status(payment_secret, is_expired, is_accepted) + elif is_expired: + # .. but expire all parts for x in payment_secrets: if x in self.received_mpp_htlcs: self.set_mpp_status(x, is_expired, is_accepted) From d83149f66880b6945fc4757c2ec2124677e4a3a0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 28 Jun 2023 16:12:13 +0200 Subject: [PATCH 0999/1143] qml: add workaround for android predictive back gestures in History component contributes to #8464 --- electrum/gui/qml/components/History.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 3b8a339a8..4134d9a1a 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -107,6 +107,18 @@ Pane { } } + MouseArea { + // cover list items, make left side insensitive to clicks + // this helps with the back gesture on newer androids + id: left_backgesture_hack + anchors { + top: listview.top + left: listview.left + bottom: listview.bottom + } + width: constants.fingerWidth + } + Rectangle { id: dragb anchors.right: vdragscroll.left From 15eb765eacf9d14049550e9253326d243518c0de Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 19 Mar 2023 13:32:43 +0100 Subject: [PATCH 1000/1143] payment_identifiers: - this separates GUI from core handling - the PaymentIdentifier class handles network requests - the GUI is agnostic about the type of PI --- electrum/gui/kivy/main_window.py | 5 +- electrum/gui/kivy/uix/screens.py | 6 +- electrum/gui/qml/qeapp.py | 2 +- electrum/gui/qt/__init__.py | 4 +- electrum/gui/qt/main_window.py | 5 +- electrum/gui/qt/paytoedit.py | 214 +----------- electrum/gui/qt/send_tab.py | 456 ++++++++++--------------- electrum/gui/qt/util.py | 1 + electrum/invoices.py | 2 +- electrum/payment_identifier.py | 559 +++++++++++++++++++++++++++++++ electrum/transaction.py | 3 +- electrum/util.py | 169 ---------- electrum/wallet.py | 3 +- electrum/x509.py | 3 +- run_electrum | 5 +- 15 files changed, 767 insertions(+), 670 deletions(-) create mode 100644 electrum/payment_identifier.py diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 6270436a2..c3bcba6a7 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -86,9 +86,8 @@ ) -from electrum.util import (NoDynamicFeeEstimates, NotEnoughFunds, - BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME, - UserFacingException) +from electrum.util import NoDynamicFeeEstimates, NotEnoughFunds, UserFacingException +from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog from .uix.dialogs.lightning_channels import LightningChannelsDialog, SwapDialog diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 23556ac92..73690c5af 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -18,8 +18,8 @@ from electrum import bitcoin, constants from electrum import lnutil from electrum.transaction import tx_from_any, PartialTxOutput -from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_lightning_payment_identifier, - InvoiceError, format_time, parse_max_spend, BITCOIN_BIP21_URI_SCHEME) +from electrum.util import TxMinedInfo, InvoiceError, format_time, parse_max_spend +from electrum.payment_identifier import parse_bip21_URI, BITCOIN_BIP21_URI_SCHEME, maybe_extract_lightning_payment_identifier, InvalidBitcoinURI from electrum.lnaddr import lndecode, LnInvoiceException from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data from electrum.logging import Logger @@ -208,7 +208,7 @@ def set_URI(self, text: str): def set_bip21(self, text: str): try: - uri = parse_URI(text, self.app.on_pr, loop=self.app.asyncio_loop) + uri = parse_bip21_URI(text) # bip70 not supported except InvalidBitcoinURI as e: self.app.show_info(_("Error parsing URI") + f":\n{e}") return diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index bf41bd932..24f861434 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -16,7 +16,7 @@ from electrum import version, constants from electrum.i18n import _ from electrum.logging import Logger, get_logger -from electrum.util import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME +from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue from electrum.network import Network diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index fdb54b98f..7891d1f21 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -87,7 +87,7 @@ def __init__(self, windows: Sequence[ElectrumWindow]): def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.FileOpen: if len(self.windows) >= 1: - self.windows[0].handle_payment_identifier(event.url().toString()) + self.windows[0].set_payment_identifier(event.url().toString()) return True return False @@ -393,7 +393,7 @@ def start_new_window( window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() if uri: - window.handle_payment_identifier(uri) + window.send_tab.set_payment_identifier(uri) return window def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wallet]: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index afaecfb68..8bb6d9c1e 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -58,9 +58,10 @@ from electrum.util import (format_time, get_asyncio_loop, UserCancelled, profiler, bfh, InvalidPassword, - UserFacingException, FailedToParsePaymentIdentifier, + UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, - AddTransactionException, BITCOIN_BIP21_URI_SCHEME, os_chmod) + AddTransactionException, os_chmod) +from electrum.payment_identifier import FailedToParsePaymentIdentifier, BITCOIN_BIP21_URI_SCHEME from electrum.invoices import PR_PAID, Invoice from electrum.transaction import (Transaction, PartialTxInput, PartialTransaction, PartialTxOutput) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 964a729da..5076b5292 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -33,7 +33,8 @@ from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout from electrum import bitcoin -from electrum.util import parse_max_spend, FailedToParsePaymentIdentifier +from electrum.util import parse_max_spend +from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier from electrum.transaction import PartialTxOutput from electrum.bitcoin import opcodes, construct_script from electrum.logging import Logger @@ -49,20 +50,10 @@ from .send_tab import SendTab -RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' - frozen_style = "QWidget {border:none;}" normal_style = "QPlainTextEdit { }" -class PayToLineError(NamedTuple): - line_content: str - exc: Exception - idx: int = 0 # index of line - is_multiline: bool = False - - - class ResizingTextEdit(QTextEdit): def __init__(self): @@ -109,12 +100,9 @@ def __init__(self, send_tab: 'SendTab'): self.amount_edit = self.send_tab.amount_e self.is_multiline = False - self.outputs = [] # type: List[PartialTxOutput] - self.errors = [] # type: List[PayToLineError] self.disable_checks = False self.is_alias = False self.payto_scriptpubkey = None # type: Optional[bytes] - self.lightning_invoice = None self.previous_payto = '' # editor methods self.setStyleSheet = self.editor.setStyleSheet @@ -180,6 +168,7 @@ def setTextNoCheck(self, text: str): self.setText(text) def do_clear(self): + self.is_multiline = False self.set_paytomany(False) self.disable_checks = False self.is_alias = False @@ -194,58 +183,6 @@ def setGreen(self): def setExpired(self): self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) - def parse_address_and_amount(self, line) -> PartialTxOutput: - try: - x, y = line.split(',') - except ValueError: - raise Exception("expected two comma-separated values: (address, amount)") from None - scriptpubkey = self.parse_output(x) - amount = self.parse_amount(y) - return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount) - - def parse_output(self, x) -> bytes: - try: - address = self.parse_address(x) - return bytes.fromhex(bitcoin.address_to_script(address)) - except Exception: - pass - try: - script = self.parse_script(x) - return bytes.fromhex(script) - except Exception: - pass - raise Exception("Invalid address or script.") - - def parse_script(self, x): - script = '' - for word in x.split(): - if word[0:3] == 'OP_': - opcode_int = opcodes[word] - script += construct_script([opcode_int]) - else: - bytes.fromhex(word) # to test it is hex data - script += construct_script([word]) - return script - - def parse_amount(self, x): - x = x.strip() - if not x: - raise Exception("Amount is empty") - if parse_max_spend(x): - return x - p = pow(10, self.amount_edit.decimal_point()) - try: - return int(p * Decimal(x)) - except decimal.InvalidOperation: - raise Exception("Invalid amount") - - def parse_address(self, line): - r = line.strip() - m = re.match('^'+RE_ALIAS+'$', r) - address = str(m.group(2) if m else r) - assert bitcoin.is_address(address) - return address - def _on_input_btn(self, text: str): self.setText(text) @@ -257,6 +194,7 @@ def _on_text_changed(self): if self.is_multiline and not self._is_paytomany: self.set_paytomany(True) self.text_edit.setText(text) + self.text_edit.setFocus() def on_timer_check_text(self): if self.editor.hasFocus(): @@ -265,149 +203,33 @@ def on_timer_check_text(self): self._check_text(text, full_check=True) def _check_text(self, text, *, full_check: bool): - """ - side effects: self.is_multiline, self.errors, self.outputs - """ - if self.previous_payto == str(text).strip(): + """ side effects: self.is_multiline """ + text = str(text).strip() + if not text: + return + if self.previous_payto == text: return if full_check: - self.previous_payto = str(text).strip() - self.errors = [] - errors = [] + self.previous_payto = text if self.disable_checks: return - # filter out empty lines - lines = text.split('\n') - lines = [i for i in lines if i] - self.is_multiline = len(lines)>1 - - self.payto_scriptpubkey = None - self.lightning_invoice = None - self.outputs = [] - - if len(lines) == 1: - data = lines[0] - try: - self.send_tab.handle_payment_identifier(data, can_use_network=full_check) - except LNURLError as e: - self.logger.exception("") - self.send_tab.show_error(e) - except FailedToParsePaymentIdentifier: - pass - else: - return - # try "address, amount" on-chain format - try: - self._parse_as_multiline(lines, raise_errors=True) - except Exception as e: - pass - else: - return - # try address/script - try: - self.payto_scriptpubkey = self.parse_output(data) - except Exception as e: - errors.append(PayToLineError(line_content=data, exc=e)) - else: - self.send_tab.set_onchain(True) - self.send_tab.lock_amount(False) - return - if full_check: # network requests # FIXME blocking GUI thread - # try openalias - oa_data = self._resolve_openalias(data) - if oa_data: - self._set_openalias(key=data, data=oa_data) - return - # all parsing attempts failed, so now expose the errors: - if errors: - self.errors = errors - else: - # there are multiple lines - self._parse_as_multiline(lines, raise_errors=False) + pi = PaymentIdentifier(self.config, self.win.contacts, text) + self.is_multiline = bool(pi.multiline_outputs) + print('is_multiline', self.is_multiline) + self.send_tab.handle_payment_identifier(pi, can_use_network=full_check) - - def _parse_as_multiline(self, lines, *, raise_errors: bool): - outputs = [] # type: List[PartialTxOutput] + def handle_multiline(self, outputs): total = 0 is_max = False - for i, line in enumerate(lines): - try: - output = self.parse_address_and_amount(line) - except Exception as e: - if raise_errors: - raise - else: - self.errors.append(PayToLineError( - idx=i, line_content=line.strip(), exc=e, is_multiline=True)) - continue - outputs.append(output) + for output in outputs: if parse_max_spend(output.value): is_max = True else: total += output.value - if outputs: - self.send_tab.set_onchain(True) - + self.send_tab.set_onchain(True) self.send_tab.max_button.setChecked(is_max) - self.outputs = outputs - self.payto_scriptpubkey = None - if self.send_tab.max_button.isChecked(): self.send_tab.spend_max() else: self.amount_edit.setAmount(total if outputs else None) - self.send_tab.lock_amount(self.send_tab.max_button.isChecked() or bool(outputs)) - - def get_errors(self) -> Sequence[PayToLineError]: - return self.errors - - def get_destination_scriptpubkey(self) -> Optional[bytes]: - return self.payto_scriptpubkey - - def get_outputs(self, is_max: bool) -> List[PartialTxOutput]: - if self.payto_scriptpubkey: - if is_max: - amount = '!' - else: - amount = self.send_tab.get_amount() - self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)] - - return self.outputs[:] - - def _resolve_openalias(self, text: str) -> Optional[dict]: - key = text - key = key.strip() # strip whitespaces - if not (('.' in key) and ('<' not in key) and (' ' not in key)): - return None - parts = key.split(sep=',') # assuming single line - if parts and len(parts) > 0 and bitcoin.is_address(parts[0]): - return None - try: - data = self.win.contacts.resolve(key) - except Exception as e: - self.logger.info(f'error resolving address/alias: {repr(e)}') - return None - return data or None - - def _set_openalias(self, *, key: str, data: dict) -> bool: - self.is_alias = True - self.setFrozen(True) - key = key.strip() # strip whitespaces - address = data.get('address') - name = data.get('name') - new_url = key + ' <' + address + '>' - self.setText(new_url) - - #if self.win.config.get('openalias_autoadd') == 'checked': - self.win.contacts[key] = ('openalias', name) - self.win.contact_list.update() - - if data.get('type') == 'openalias': - self.validated = data.get('validated') - if self.validated: - self.setGreen() - else: - self.setExpired() - else: - self.validated = None - return True + #self.send_tab.lock_amount(self.send_tab.max_button.isChecked() or bool(outputs)) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index dffa15d31..c07851daf 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -5,8 +5,6 @@ import asyncio from decimal import Decimal from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Any -from urllib.parse import urlparse - from PyQt5.QtCore import pyqtSignal, QPoint from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton) @@ -15,15 +13,14 @@ from electrum import lnutil from electrum.plugin import run_hook from electrum.i18n import _ -from electrum.util import (get_asyncio_loop, FailedToParsePaymentIdentifier, - InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, NotEnoughFunds, - NoDynamicFeeEstimates, InvoiceError, parse_max_spend) + +from electrum.util import get_asyncio_loop, NotEnoughFunds, NoDynamicFeeEstimates, InvoiceError, parse_max_spend +from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier, InvalidBitcoinURI from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST + from electrum.transaction import Transaction, PartialTxInput, PartialTransaction, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.logging import Logger -from electrum.lnaddr import lndecode, LnInvoiceException -from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit @@ -36,15 +33,9 @@ class SendTab(QWidget, MessageBoxMixin, Logger): - payment_request_ok_signal = pyqtSignal() - payment_request_error_signal = pyqtSignal() - lnurl6_round1_signal = pyqtSignal(object, object) - lnurl6_round2_signal = pyqtSignal(object) - clear_send_tab_signal = pyqtSignal() - show_error_signal = pyqtSignal(str) - - payment_request: Optional[paymentrequest.PaymentRequest] - _lnurl_data: Optional[LNURL6Data] = None + round_1_signal = pyqtSignal(object) + round_2_signal = pyqtSignal(object) + round_3_signal = pyqtSignal(object) def __init__(self, window: 'ElectrumWindow'): QWidget.__init__(self, window) @@ -60,8 +51,7 @@ def __init__(self, window: 'ElectrumWindow'): self.format_amount = window.format_amount self.base_unit = window.base_unit - self.payto_URI = None - self.payment_request = None # type: Optional[paymentrequest.PaymentRequest] + self.payment_identifier = None self.pending_invoice = None # A 4-column grid layout. All the stretch is in the last column. @@ -84,9 +74,9 @@ def __init__(self, window: 'ElectrumWindow'): + _("Integers weights can also be used in conjunction with '!', " "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) - grid.addWidget(payto_label, 1, 0) - grid.addWidget(self.payto_e.line_edit, 1, 1, 1, 4) - grid.addWidget(self.payto_e.text_edit, 1, 1, 1, 4) + grid.addWidget(payto_label, 0, 0) + grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4) + grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4) #completer = QCompleter() #completer.setCaseSensitivity(False) @@ -97,9 +87,17 @@ def __init__(self, window: 'ElectrumWindow'): + _( 'The description is not sent to the recipient of the funds. It is stored in your wallet file, and displayed in the \'History\' tab.') description_label = HelpLabel(_('Description'), msg) - grid.addWidget(description_label, 2, 0) + grid.addWidget(description_label, 1, 0) self.message_e = SizedFreezableLineEdit(width=600) - grid.addWidget(self.message_e, 2, 1, 1, 4) + grid.addWidget(self.message_e, 1, 1, 1, 4) + + msg = _('Comment for recipient') + self.comment_label = HelpLabel(_('Comment'), msg) + grid.addWidget(self.comment_label, 2, 0) + self.comment_e = SizedFreezableLineEdit(width=600) + grid.addWidget(self.comment_e, 2, 1, 1, 4) + self.comment_label.hide() + self.comment_e.hide() msg = (_('The amount to be received by the recipient.') + ' ' + _('Fees are paid by the sender.') + '\n\n' @@ -129,11 +127,11 @@ def __init__(self, window: 'ElectrumWindow'): self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice) self.clear_button = EnterButton(_("Clear"), self.do_clear) self.paste_button = QPushButton() - self.paste_button.clicked.connect(lambda: self.payto_e._on_input_btn(self.window.app.clipboard().text())) + self.paste_button.clicked.connect(self.do_paste) self.paste_button.setIcon(read_QIcon('copy.png')) self.paste_button.setToolTip(_('Paste invoice from clipboard')) self.paste_button.setMaximumWidth(35) - grid.addWidget(self.paste_button, 1, 5) + grid.addWidget(self.paste_button, 0, 5) buttons = QHBoxLayout() buttons.addStretch(1) @@ -160,7 +158,6 @@ def reset_max(text): self.invoice_list = InvoiceList(self) self.toolbar, menu = self.invoice_list.create_toolbar_with_menu('') - menu.addAction(read_QIcon(get_iconname_camera()), _("Read QR code with camera"), self.payto_e.on_qr_from_camera_input_btn) menu.addAction(read_QIcon("picture_in_picture.png"), _("Read QR code from screen"), self.payto_e.on_qr_from_screenshot_input_btn) menu.addAction(read_QIcon("file.png"), _("Read invoice from file"), self.payto_e.on_input_file) @@ -186,17 +183,33 @@ def reset_max(text): self.invoice_list.update() # after parented and put into a layout, can update without flickering run_hook('create_send_tab', grid) - self.payment_request_ok_signal.connect(self.payment_request_ok) - self.payment_request_error_signal.connect(self.payment_request_error) - self.lnurl6_round1_signal.connect(self.on_lnurl6_round1) - self.lnurl6_round2_signal.connect(self.on_lnurl6_round2) - self.clear_send_tab_signal.connect(self.do_clear) - self.show_error_signal.connect(self.show_error) + self.round_1_signal.connect(self.on_round_1) + self.round_2_signal.connect(self.on_round_2) + self.round_3_signal.connect(self.on_round_3) + + def do_paste(self): + text = self.window.app.clipboard().text() + if not text: + return + self.set_payment_identifier(text) + + def set_payment_identifier(self, text): + pi = PaymentIdentifier(self.config, self.window.contacts, text) + if pi.error: + self.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + pi.error) + return + if pi.is_multiline(): + self.payto_e.set_paytomany(True) + self.payto_e.text_edit.setText(text) + else: + self.payto_e.setTextNoCheck(text) + self.handle_payment_identifier(pi, can_use_network=True) def spend_max(self): if run_hook('abort_send', self): return - outputs = self.payto_e.get_outputs(True) + amount = self.get_amount() + outputs = self.payment_identifier.get_onchain_outputs(amount) if not outputs: return make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( @@ -297,15 +310,14 @@ def get_frozen_balance_str(self) -> Optional[str]: return self.format_amount_and_units(frozen_bal) def do_clear(self): - self._lnurl_data = None self.max_button.setChecked(False) - self.payment_request = None - self.payto_URI = None self.payto_e.do_clear() self.set_onchain(False) - for e in [self.message_e, self.amount_e]: + for w in [self.comment_e, self.comment_label]: + w.setVisible(False) + for e in [self.message_e, self.amount_e, self.fiat_send_e]: e.setText('') - e.setFrozen(False) + self.set_field_style(e, None, False) for e in [self.send_button, self.save_button, self.clear_button, self.amount_e, self.fiat_send_e]: e.setEnabled(True) self.window.update_status() @@ -315,208 +327,101 @@ def set_onchain(self, b): self._is_onchain = b self.max_button.setEnabled(b) - def lock_amount(self, b: bool) -> None: - self.amount_e.setFrozen(b) - self.max_button.setEnabled(not b) - def prepare_for_send_tab_network_lookup(self): self.window.show_send_tab() self.payto_e.disable_checks = True - for e in [self.payto_e, self.message_e]: - e.setFrozen(True) - self.lock_amount(True) + #for e in [self.payto_e, self.message_e]: + self.payto_e.setFrozen(True) for btn in [self.save_button, self.send_button, self.clear_button]: btn.setEnabled(False) self.payto_e.setTextNoCheck(_("please wait...")) - def payment_request_ok(self): - pr = self.payment_request - if not pr: - return - invoice = Invoice.from_bip70_payreq(pr, height=0) - if self.wallet.get_invoice_status(invoice) == PR_PAID: - self.show_message("invoice already paid") - self.do_clear() - self.payment_request = None - return - self.payto_e.disable_checks = True - if not pr.has_expired(): - self.payto_e.setGreen() - else: - self.payto_e.setExpired() - self.payto_e.setTextNoCheck(pr.get_requestor()) - self.amount_e.setAmount(pr.get_amount()) - self.message_e.setText(pr.get_memo()) - self.set_onchain(True) - self.max_button.setEnabled(False) - # note: allow saving bip70 reqs, as we save them anyway when paying them - for btn in [self.send_button, self.clear_button, self.save_button]: - btn.setEnabled(True) - # signal to set fee - self.amount_e.textEdited.emit("") - - def payment_request_error(self): - pr = self.payment_request - if not pr: - return - self.show_message(pr.error) - self.payment_request = None + def payment_request_error(self, error): + self.show_message(error) self.do_clear() - def on_pr(self, request: 'paymentrequest.PaymentRequest'): - self.payment_request = request - if self.payment_request.verify(self.window.contacts): - self.payment_request_ok_signal.emit() + def set_field_style(self, w, text, validated): + from .util import ColorScheme + if validated is None: + style = ColorScheme.LIGHTBLUE.as_stylesheet(True) + elif validated is True: + style = ColorScheme.GREEN.as_stylesheet(True) else: - self.payment_request_error_signal.emit() - - def set_lnurl6(self, lnurl: str, *, can_use_network: bool = True): - try: - url = decode_lnurl(lnurl) - except LnInvoiceException as e: - self.show_error(_("Error parsing Lightning invoice") + f":\n{e}") - return - if not can_use_network: - return - - async def f(): - try: - lnurl_data = await request_lnurl(url) - except LNURLError as e: - self.show_error_signal.emit(f"LNURL request encountered error: {e}") - self.clear_send_tab_signal.emit() - return - self.lnurl6_round1_signal.emit(lnurl_data, url) - - asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable - self.prepare_for_send_tab_network_lookup() - - def on_lnurl6_round1(self, lnurl_data: LNURL6Data, url: str): - self._lnurl_data = lnurl_data - domain = urlparse(url).netloc - self.payto_e.setTextNoCheck(f"invoice from lnurl") - self.message_e.setText(f"lnurl: {domain}: {lnurl_data.metadata_plaintext}") - self.amount_e.setAmount(lnurl_data.min_sendable_sat) - self.amount_e.setFrozen(False) - for btn in [self.send_button, self.clear_button]: - btn.setEnabled(True) - self.set_onchain(False) - - def set_bolt11(self, invoice: str): - """Parse ln invoice, and prepare the send tab for it.""" - try: - lnaddr = lndecode(invoice) - except LnInvoiceException as e: - self.show_error(_("Error parsing Lightning invoice") + f":\n{e}") - return - except lnutil.IncompatibleOrInsaneFeatures as e: - self.show_error(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") - return - - pubkey = lnaddr.pubkey.serialize().hex() - for k,v in lnaddr.tags: - if k == 'd': - description = v - break + style = ColorScheme.RED.as_stylesheet(True) + if text is not None: + w.setStyleSheet(style) + w.setReadOnly(True) else: - description = '' - self.payto_e.setFrozen(True) - self.payto_e.setTextNoCheck(pubkey) - self.payto_e.lightning_invoice = invoice - if not self.message_e.text(): + w.setStyleSheet('') + w.setReadOnly(False) + + def update_fields(self, pi): + recipient, amount, description, comment, validated = pi.get_fields_for_GUI(self.wallet) + if recipient: + self.payto_e.setTextNoCheck(recipient) + elif pi.multiline_outputs: + self.payto_e.handle_multiline(pi.multiline_outputs) + if description: self.message_e.setText(description) - if lnaddr.get_amount_sat() is not None: - self.amount_e.setAmount(lnaddr.get_amount_sat()) - self.set_onchain(False) - - def set_bip21(self, text: str, *, can_use_network: bool = True): - on_bip70_pr = self.on_pr if can_use_network else None - try: - out = util.parse_URI(text, on_bip70_pr) - except InvalidBitcoinURI as e: - self.show_error(_("Error parsing URI") + f":\n{e}") - return - self.payto_URI = out - r = out.get('r') - sig = out.get('sig') - name = out.get('name') - if (r or (name and sig)) and can_use_network: - self.prepare_for_send_tab_network_lookup() - return - address = out.get('address') - amount = out.get('amount') - label = out.get('label') - message = out.get('message') - lightning = out.get('lightning') - if lightning and (self.wallet.has_lightning() or not address): - self.handle_payment_identifier(lightning, can_use_network=can_use_network) - return - # use label as description (not BIP21 compliant) - if label and not message: - message = label - if address: - self.payto_e.setText(address) - if message: - self.message_e.setText(message) if amount: self.amount_e.setAmount(amount) - - def handle_payment_identifier(self, text: str, *, can_use_network: bool = True): - """Takes - Lightning identifiers: - * lightning-URI (containing bolt11 or lnurl) - * bolt11 invoice - * lnurl - Bitcoin identifiers: - * bitcoin-URI - and sets the sending screen. - """ - text = text.strip() - if not text: + for w in [self.comment_e, self.comment_label]: + w.setVisible(not bool(comment)) + self.set_field_style(self.payto_e, recipient or pi.multiline_outputs, validated) + self.set_field_style(self.message_e, description, validated) + self.set_field_style(self.amount_e, amount, validated) + self.set_field_style(self.fiat_send_e, amount, validated) + + def handle_payment_identifier(self, pi, *, can_use_network: bool = True): + self.payment_identifier = pi + is_valid = pi.is_valid() + self.save_button.setEnabled(is_valid) + self.send_button.setEnabled(is_valid) + if not is_valid: return - if invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text): - if invoice_or_lnurl.startswith('lnurl'): - self.set_lnurl6(invoice_or_lnurl, can_use_network=can_use_network) - else: - self.set_bolt11(invoice_or_lnurl) - elif text.lower().startswith(util.BITCOIN_BIP21_URI_SCHEME + ':'): - self.set_bip21(text, can_use_network=can_use_network) - else: - truncated_text = f"{text[:100]}..." if len(text) > 100 else text - raise FailedToParsePaymentIdentifier(f"Could not handle payment identifier:\n{truncated_text}") + self.update_fields(pi) + if can_use_network and pi.needs_round_1(): + coro = pi.round_1(on_success=self.round_1_signal.emit) + asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) + self.prepare_for_send_tab_network_lookup() # update fiat amount self.amount_e.textEdited.emit("") self.window.show_send_tab() + def on_round_1(self, pi): + if pi.error: + self.show_error(pi.error) + self.do_clear() + return + self.update_fields(pi) + for btn in [self.send_button, self.clear_button, self.save_button]: + btn.setEnabled(True) + + def get_message(self): + return self.message_e.text() def read_invoice(self) -> Optional[Invoice]: if self.check_payto_line_and_show_errors(): return - try: - if not self._is_onchain: - invoice_str = self.payto_e.lightning_invoice - if not invoice_str: - return - invoice = Invoice.from_bech32(invoice_str) - if invoice.amount_msat is None: - amount_sat = self.get_amount() - if amount_sat: - invoice.amount_msat = int(amount_sat * 1000) - if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): - self.show_error(_('Lightning is disabled')) - return - return invoice - else: - outputs = self.read_outputs() - if self.check_onchain_outputs_and_show_errors(outputs): - return - message = self.message_e.text() - return self.wallet.create_invoice( - outputs=outputs, - message=message, - pr=self.payment_request, - URI=self.payto_URI) - except InvoiceError as e: - self.show_error(_('Error creating payment') + ':\n' + str(e)) + amount_sat = self.read_amount() + if not amount_sat: + self.show_error(_('No amount')) + return + + invoice = self.payment_identifier.get_invoice(self.wallet, amount_sat, self.get_message()) + #except Exception as e: + if not invoice: + self.show_error('error getting invoice' + pi.error) + return + if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): + self.show_error(_('Lightning is disabled')) + if self.wallet.get_invoice_status(invoice) == PR_PAID: + # fixme: this is only for bip70 and lightning + self.show_error(_('Invoice already paid')) + return + #if not invoice.is_lightning(): + # if self.check_onchain_outputs_and_show_errors(outputs): + # return + return invoice def do_save_invoice(self): self.pending_invoice = self.read_invoice() @@ -536,41 +441,26 @@ def get_amount(self) -> int: # must not be None return self.amount_e.get_amount() or 0 - def _lnurl_get_invoice(self) -> None: - assert self._lnurl_data - amount = self.get_amount() - if not (self._lnurl_data.min_sendable_sat <= amount <= self._lnurl_data.max_sendable_sat): - self.show_error(f'Amount must be between {self._lnurl_data.min_sendable_sat} and {self._lnurl_data.max_sendable_sat} sat.') - return - - async def f(): - try: - invoice_data = await callback_lnurl( - self._lnurl_data.callback_url, - params={'amount': self.get_amount() * 1000}, - ) - except LNURLError as e: - self.show_error_signal.emit(f"LNURL request encountered error: {e}") - self.clear_send_tab_signal.emit() - return - invoice = invoice_data.get('pr') - self.lnurl6_round2_signal.emit(invoice) - - asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable - self.prepare_for_send_tab_network_lookup() - - def on_lnurl6_round2(self, bolt11_invoice: str): - self._lnurl_data = None - invoice = Invoice.from_bech32(bolt11_invoice) - assert invoice.get_amount_sat() == self.get_amount(), (invoice.get_amount_sat(), self.get_amount()) + def on_round_2(self, pi): self.do_clear() - self.payto_e.setText(bolt11_invoice) + if pi.error: + self.show_error(pi.error) + self.do_clear() + return + self.update_fields(pi) + invoice = pi.get_invoice(self.wallet, self.get_amount(), self.get_message()) self.pending_invoice = invoice self.do_pay_invoice(invoice) + def on_round_3(self): + pass + def do_pay_or_get_invoice(self): - if self._lnurl_data: - self._lnurl_get_invoice() + pi = self.payment_identifier + if pi.needs_round_2(): + coro = pi.round_2(self.round_2_signal.emit, amount_sat=self.get_amount(), comment=self.message_e.text()) + asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) # TODO should be cancellable + self.prepare_for_send_tab_network_lookup() return self.pending_invoice = self.read_invoice() if not self.pending_invoice: @@ -600,12 +490,10 @@ def do_pay_invoice(self, invoice: 'Invoice'): else: self.pay_onchain_dialog(invoice.outputs) - def read_outputs(self) -> List[PartialTxOutput]: - if self.payment_request: - outputs = self.payment_request.get_outputs() - else: - outputs = self.payto_e.get_outputs(self.max_button.isChecked()) - return outputs + def read_amount(self) -> List[PartialTxOutput]: + is_max = self.max_button.isChecked() + amount = '!' if is_max else self.get_amount() + return amount def check_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool: """Returns whether there are errors with outputs. @@ -629,34 +517,30 @@ def check_payto_line_and_show_errors(self) -> bool: """Returns whether there are errors. Also shows error dialog to user if so. """ - pr = self.payment_request - if pr: - if pr.has_expired(): - self.show_error(_('Payment request has expired')) - return True + error = self.payment_identifier.get_error() + if error: + if not self.payment_identifier.is_multiline(): + err = error + self.show_warning( + _("Failed to parse 'Pay to' line") + ":\n" + + f"{err.line_content[:40]}...\n\n" + f"{err.exc!r}") + else: + self.show_warning( + _("Invalid Lines found:") + "\n\n" + error) + #'\n'.join([_("Line #") + + # f"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})" + # for err in errors])) + return True - if not pr: - errors = self.payto_e.get_errors() - if errors: - if len(errors) == 1 and not errors[0].is_multiline: - err = errors[0] - self.show_warning(_("Failed to parse 'Pay to' line") + ":\n" + - f"{err.line_content[:40]}...\n\n" - f"{err.exc!r}") - else: - self.show_warning(_("Invalid Lines found:") + "\n\n" + - '\n'.join([_("Line #") + - f"{err.idx+1}: {err.line_content[:40]}... ({err.exc!r})" - for err in errors])) + if self.payment_identifier.warning: + msg += '\n' + _('Do you wish to continue?') + if not self.question(msg): return True - if self.payto_e.is_alias and self.payto_e.validated is False: - alias = self.payto_e.toPlainText() - msg = _('WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(alias) + '\n' - msg += _('Do you wish to continue?') - if not self.question(msg): - return True + if self.payment_identifier.has_expired(): + self.show_error(_('Payment request has expired')) + return True return False # no errors @@ -740,9 +624,7 @@ def broadcast_transaction(self, tx: Transaction): def broadcast_thread(): # non-GUI thread - pr = self.payment_request - if pr and pr.has_expired(): - self.payment_request = None + if self.payment_identifier.has_expired(): return False, _("Invoice has expired") try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) @@ -752,13 +634,10 @@ def broadcast_thread(): return False, repr(e) # success txid = tx.txid() - if pr: - self.payment_request = None + if self.payment_identifier.needs_round_3(): refund_address = self.wallet.get_receiving_address() - coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address) - fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) - ack_status, ack_msg = fut.result(timeout=20) - self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") + coro = self.payment_identifier.round_3(tx.serialize(), refund_address) + asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) return True, txid # Capture current TL window; override might be removed on return @@ -804,3 +683,4 @@ def payto_contacts(self, labels): self.payto_e.setFocus() text = "\n".join([payto + ", 0" for payto in paytos]) self.payto_e.setText(text) + self.payto_e.setFocus() diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index a393c2bdb..1d5f5c2b9 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -961,6 +961,7 @@ class ColorScheme: YELLOW = ColorSchemeItem("#897b2a", "#ffff00") RED = ColorSchemeItem("#7c1111", "#f18c8c") BLUE = ColorSchemeItem("#123b7c", "#8cb3f2") + LIGHTBLUE = ColorSchemeItem("black", "#d0f0ff") DEFAULT = ColorSchemeItem("black", "white") GRAY = ColorSchemeItem("gray", "gray") diff --git a/electrum/invoices.py b/electrum/invoices.py index 6f7300e36..61a90b781 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -7,6 +7,7 @@ from .json_db import StoredObject, stored_in from .i18n import _ from .util import age, InvoiceError, format_satoshis +from .payment_identifier import create_bip21_uri from .lnutil import hex_to_bytes from .lnaddr import lndecode, LnAddr from . import constants @@ -318,7 +319,6 @@ def get_bip21_URI( *, lightning_invoice: Optional[str] = None, ) -> Optional[str]: - from electrum.util import create_bip21_uri addr = self.get_address() amount = self.get_amount_sat() if amount is not None: diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py new file mode 100644 index 000000000..cbf1a6461 --- /dev/null +++ b/electrum/payment_identifier.py @@ -0,0 +1,559 @@ +import asyncio +import urllib +import re +from decimal import Decimal +from typing import NamedTuple, Optional, Callable, Any, Sequence +from urllib.parse import urlparse + +from . import bitcoin +from .logging import Logger +from .util import parse_max_spend, format_satoshis_plain +from .util import get_asyncio_loop, log_exceptions +from .transaction import PartialTxOutput +from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data +from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC +from .lnaddr import lndecode, LnDecodeException, LnInvoiceException +from .lnutil import IncompatibleOrInsaneFeatures + +def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: + data = data.strip() # whitespaces + data = data.lower() + if data.startswith(LIGHTNING_URI_SCHEME + ':ln'): + cut_prefix = LIGHTNING_URI_SCHEME + ':' + data = data[len(cut_prefix):] + if data.startswith('ln'): + return data + return None + +# URL decode +#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) +#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) + + +# note: when checking against these, use .lower() to support case-insensitivity +BITCOIN_BIP21_URI_SCHEME = 'bitcoin' +LIGHTNING_URI_SCHEME = 'lightning' + + +class InvalidBitcoinURI(Exception): pass + + +def parse_bip21_URI(uri: str) -> dict: + """Raises InvalidBitcoinURI on malformed URI.""" + + if not isinstance(uri, str): + raise InvalidBitcoinURI(f"expected string, not {repr(uri)}") + + if ':' not in uri: + if not bitcoin.is_address(uri): + raise InvalidBitcoinURI("Not a bitcoin address") + return {'address': uri} + + u = urllib.parse.urlparse(uri) + if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME: + raise InvalidBitcoinURI("Not a bitcoin URI") + address = u.path + + # python for android fails to parse query + if address.find('?') > 0: + address, query = u.path.split('?') + pq = urllib.parse.parse_qs(query) + else: + pq = urllib.parse.parse_qs(u.query) + + for k, v in pq.items(): + if len(v) != 1: + raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}') + + out = {k: v[0] for k, v in pq.items()} + if address: + if not bitcoin.is_address(address): + raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}") + out['address'] = address + if 'amount' in out: + am = out['amount'] + try: + m = re.match(r'([0-9.]+)X([0-9])', am) + if m: + k = int(m.group(2)) - 8 + amount = Decimal(m.group(1)) * pow(Decimal(10), k) + else: + amount = Decimal(am) * COIN + if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: + raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC") + out['amount'] = int(amount) + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e + if 'message' in out: + out['message'] = out['message'] + out['memo'] = out['message'] + if 'time' in out: + try: + out['time'] = int(out['time']) + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e + if 'exp' in out: + try: + out['exp'] = int(out['exp']) + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e + if 'sig' in out: + try: + out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex() + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e + if 'lightning' in out: + try: + lnaddr = lndecode(out['lightning']) + except LnDecodeException as e: + raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e + amount_sat = out.get('amount') + if amount_sat: + # allow small leeway due to msat precision + if abs(amount_sat - int(lnaddr.get_amount_sat())) > 1: + raise InvalidBitcoinURI("Inconsistent lightning field in bip21: amount") + address = out.get('address') + ln_fallback_addr = lnaddr.get_fallback_address() + if address and ln_fallback_addr: + if ln_fallback_addr != address: + raise InvalidBitcoinURI("Inconsistent lightning field in bip21: address") + + return out + + + +def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], + *, extra_query_params: Optional[dict] = None) -> str: + if not bitcoin.is_address(addr): + return "" + if extra_query_params is None: + extra_query_params = {} + query = [] + if amount_sat: + query.append('amount=%s'%format_satoshis_plain(amount_sat)) + if message: + query.append('message=%s'%urllib.parse.quote(message)) + for k, v in extra_query_params.items(): + if not isinstance(k, str) or k != urllib.parse.quote(k): + raise Exception(f"illegal key for URI: {repr(k)}") + v = urllib.parse.quote(v) + query.append(f"{k}={v}") + p = urllib.parse.ParseResult( + scheme=BITCOIN_BIP21_URI_SCHEME, + netloc='', + path=addr, + params='', + query='&'.join(query), + fragment='', + ) + return str(urllib.parse.urlunparse(p)) + + + +def is_uri(data: str) -> bool: + data = data.lower() + if (data.startswith(LIGHTNING_URI_SCHEME + ":") or + data.startswith(BITCOIN_BIP21_URI_SCHEME + ':')): + return True + return False + + + +class FailedToParsePaymentIdentifier(Exception): + pass + +class PayToLineError(NamedTuple): + line_content: str + exc: Exception + idx: int = 0 # index of line + is_multiline: bool = False + +RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' +RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' + +class PaymentIdentifier(Logger): + """ + Takes: + * bitcoin addresses or script + * paytomany csv + * openalias + * bip21 URI + * lightning-URI (containing bolt11 or lnurl) + * bolt11 invoice + * lnurl + """ + + def __init__(self, config, contacts, text): + Logger.__init__(self) + self.contacts = contacts + self.config = config + self.text = text + self._type = None + self.error = None # if set, GUI should show error and stop + self.warning = None # if set, GUI should ask user if they want to proceed + # more than one of those may be set + self.multiline_outputs = None + self.bolt11 = None + self.bip21 = None + self.spk = None + # + self.openalias = None + self.openalias_data = None + # + self.bip70 = None + self.bip70_data = None + # + self.lnurl = None + self.lnurl_data = None + # parse without network + self.parse(text) + + def is_valid(self): + return bool(self._type) + + def is_lightning(self): + return self.lnurl or self.bolt11 + + def is_multiline(self): + return bool(self.multiline_outputs) + + def get_error(self) -> str: + return self.error + + def needs_round_1(self): + return self.bip70 or self.openalias or self.lnurl + + def needs_round_2(self): + return self.lnurl and self.lnurl_data + + def needs_round_3(self): + return self.bip70 + + def parse(self, text): + # parse text, set self._type and self.error + text = text.strip() + if not text: + return + if outputs:= self._parse_as_multiline(text): + self._type = 'multiline' + self.multiline_outputs = outputs + elif invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text): + if invoice_or_lnurl.startswith('lnurl'): + self._type = 'lnurl' + try: + self.lnurl = decode_lnurl(invoice_or_lnurl) + except Exception as e: + self.error = "Error parsing Lightning invoice" + f":\n{e}" + return + else: + self._type = 'bolt11' + self.bolt11 = invoice_or_lnurl + elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): + try: + out = parse_bip21_URI(text) + except InvalidBitcoinURI as e: + self.error = _("Error parsing URI") + f":\n{e}" + return + self._type = 'bip21' + self.bip21 = out + self.bip70 = out.get('r') + elif scriptpubkey := self.parse_output(text): + self._type = 'spk' + self.spk = scriptpubkey + elif re.match(RE_EMAIL, text): + self._type = 'alias' + self.openalias = text + elif self.error is None: + truncated_text = f"{text[:100]}..." if len(text) > 100 else text + self.error = FailedToParsePaymentIdentifier(f"Unknown payment identifier:\n{truncated_text}") + + def get_onchain_outputs(self, amount): + if self.bip70: + return self.bip70_data.get_outputs() + elif self.multiline_outputs: + return self.multiline_outputs + elif self.spk: + return [PartialTxOutput(scriptpubkey=self.spk, value=amount)] + elif self.bip21: + address = self.bip21.get('address') + scriptpubkey = self.parse_output(address) + return [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)] + else: + raise Exception('not onchain') + + def _parse_as_multiline(self, text): + # filter out empty lines + lines = text.split('\n') + lines = [i for i in lines if i] + is_multiline = len(lines)>1 + outputs = [] # type: List[PartialTxOutput] + errors = [] + total = 0 + is_max = False + for i, line in enumerate(lines): + try: + output = self.parse_address_and_amount(line) + except Exception as e: + errors.append(PayToLineError( + idx=i, line_content=line.strip(), exc=e, is_multiline=True)) + continue + outputs.append(output) + if parse_max_spend(output.value): + is_max = True + else: + total += output.value + if is_multiline and errors: + self.error = str(errors) if errors else None + print(outputs, self.error) + return outputs + + def parse_address_and_amount(self, line) -> 'PartialTxOutput': + try: + x, y = line.split(',') + except ValueError: + raise Exception("expected two comma-separated values: (address, amount)") from None + scriptpubkey = self.parse_output(x) + amount = self.parse_amount(y) + return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount) + + def parse_output(self, x) -> bytes: + try: + address = self.parse_address(x) + return bytes.fromhex(bitcoin.address_to_script(address)) + except Exception as e: + error = PayToLineError(idx=0, line_content=x, exc=e, is_multiline=False) + try: + script = self.parse_script(x) + return bytes.fromhex(script) + except Exception as e: + #error = PayToLineError(idx=0, line_content=x, exc=e, is_multiline=False) + pass + #raise Exception("Invalid address or script.") + #self.errors.append(error) + + def parse_script(self, x): + script = '' + for word in x.split(): + if word[0:3] == 'OP_': + opcode_int = opcodes[word] + script += construct_script([opcode_int]) + else: + bytes.fromhex(word) # to test it is hex data + script += construct_script([word]) + return script + + def parse_amount(self, x): + x = x.strip() + if not x: + raise Exception("Amount is empty") + if parse_max_spend(x): + return x + p = pow(10, self.config.get_decimal_point()) + try: + return int(p * Decimal(x)) + except decimal.InvalidOperation: + raise Exception("Invalid amount") + + def parse_address(self, line): + r = line.strip() + m = re.match('^'+RE_ALIAS+'$', r) + address = str(m.group(2) if m else r) + assert bitcoin.is_address(address) + return address + + def get_fields_for_GUI(self, wallet): + """ sets self.error as side effect""" + recipient = None + amount = None + description = None + validated = None + comment = "no comment" + + if self.openalias and self.openalias_data: + address = self.openalias_data.get('address') + name = self.openalias_data.get('name') + recipient = self.openalias + ' <' + address + '>' + validated = self.openalias_data.get('validated') + if not validated: + self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' + 'security check, DNSSEC, and thus may not be correct.').format(self.openalias) + #self.payto_e.set_openalias(key=pi.openalias, data=oa_data) + #self.window.contact_list.update() + + elif self.bolt11: + recipient, amount, description = self.get_bolt11_fields(self.bolt11) + + elif self.lnurl and self.lnurl_data: + domain = urlparse(self.lnurl).netloc + #recipient = "invoice from lnurl" + recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>" + #amount = self.lnurl_data.min_sendable_sat + amount = None + description = None + if self.lnurl_data.comment_allowed: + comment = None + + elif self.bip70 and self.bip70_data: + pr = self.bip70_data + if pr.error: + self.error = pr.error + return + recipient = pr.get_requestor() + amount = pr.get_amount() + description = pr.get_memo() + validated = not pr.has_expired() + #self.set_onchain(True) + #self.max_button.setEnabled(False) + # note: allow saving bip70 reqs, as we save them anyway when paying them + #for btn in [self.send_button, self.clear_button, self.save_button]: + # btn.setEnabled(True) + # signal to set fee + #self.amount_e.textEdited.emit("") + + elif self.spk: + recipient = self.text + amount = None + + elif self.multiline_outputs: + pass + + elif self.bip21: + recipient = self.bip21.get('address') + amount = self.bip21.get('amount') + label = self.bip21.get('label') + description = self.bip21.get('message') + # use label as description (not BIP21 compliant) + if label and not description: + description = label + lightning = self.bip21.get('lightning') + if lightning and wallet.has_lightning(): + # maybe set self.bolt11? + recipient, amount, description = self.get_bolt11_fields(lightning) + if not amount: + amount_required = True + # todo: merge logic + + return recipient, amount, description, comment, validated + + def get_bolt11_fields(self, bolt11_invoice): + """Parse ln invoice, and prepare the send tab for it.""" + try: + lnaddr = lndecode(bolt11_invoice) + except LnInvoiceException as e: + self.show_error(_("Error parsing Lightning invoice") + f":\n{e}") + return + except IncompatibleOrInsaneFeatures as e: + self.show_error(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") + return + pubkey = lnaddr.pubkey.serialize().hex() + for k,v in lnaddr.tags: + if k == 'd': + description = v + break + else: + description = '' + amount = lnaddr.get_amount_sat() + return pubkey, amount, description + + async def resolve_openalias(self) -> Optional[dict]: + key = self.openalias + if not (('.' in key) and ('<' not in key) and (' ' not in key)): + return None + parts = key.split(sep=',') # assuming single line + if parts and len(parts) > 0 and bitcoin.is_address(parts[0]): + return None + try: + data = self.contacts.resolve(key) + except Exception as e: + self.logger.info(f'error resolving address/alias: {repr(e)}') + return None + if data: + name = data.get('name') + address = data.get('address') + self.contacts[key] = ('openalias', name) + # this will set self.spk + self.parse(address) + return data + + def has_expired(self): + if self.bip70: + return self.bip70_data.has_expired() + return False + + @log_exceptions + async def round_1(self, on_success): + if self.openalias: + data = await self.resolve_openalias() + self.openalias_data = data + if not self.openalias_data.get('validated'): + self.warning = _( + 'WARNING: the alias "{}" could not be validated via an additional ' + 'security check, DNSSEC, and thus may not be correct.').format(self.openalias) + elif self.bip70: + from . import paymentrequest + data = await paymentrequest.get_payment_request(self.bip70) + self.bip70_data = data + elif self.lnurl: + data = await request_lnurl(self.lnurl) + self.lnurl_data = data + else: + return + on_success(self) + + @log_exceptions + async def round_2(self, on_success, amount_sat:int=None, comment=None): + from .invoices import Invoice + if self.lnurl: + if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat): + self.error = f'Amount must be between {self.lnurl_data.min_sendable_sat} and {self.lnurl_data.max_sendable_sat} sat.' + return + if self.lnurl_data.comment_allowed == 0: + comment = None + params = {'amount': amount_sat * 1000 } + if comment: + params['comment'] = comment + try: + invoice_data = await callback_lnurl( + self.lnurl_data.callback_url, + params=params, + ) + except LNURLError as e: + self.error = f"LNURL request encountered error: {e}" + return + bolt11_invoice = invoice_data.get('pr') + # + invoice = Invoice.from_bech32(bolt11_invoice) + if invoice.get_amount_sat() != amount_sat: + raise Exception("lnurl returned invoice with wrong amount") + # this will change what is returned by get_fields_for_GUI + self.bolt11 = bolt11_invoice + + on_success(self) + + @log_exceptions + async def round_3(self, tx, refund_address, *, on_success): + if self.bip70: + ack_status, ack_msg = await self.bip70.send_payment_and_receive_paymentack(tx.serialize(), refund_address) + self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") + on_success(self) + + def get_invoice(self, wallet, amount_sat, message): + # fixme: wallet not really needed, only height + from .invoices import Invoice + if self.is_lightning(): + invoice_str = self.bolt11 + if not invoice_str: + return + invoice = Invoice.from_bech32(invoice_str) + if invoice.amount_msat is None: + invoice.amount_msat = int(amount_sat * 1000) + return invoice + else: + outputs = self.get_onchain_outputs(amount_sat) + message = self.bip21.get('message') if self.bip21 else message + bip70_data = self.bip70_data if self.bip70 else None + return wallet.create_invoice( + outputs=outputs, + message=message, + pr=bip70_data, + URI=self.bip21) diff --git a/electrum/transaction.py b/electrum/transaction.py index 248642905..09fdf7ce4 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -42,7 +42,8 @@ from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node -from .util import profiler, to_bytes, bfh, chunks, is_hex_str, parse_max_spend +from .util import profiler, to_bytes, bfh, chunks, is_hex_str +from .payment_identifier import parse_max_spend from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, diff --git a/electrum/util.py b/electrum/util.py index c31ba5711..e0da5441a 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1009,177 +1009,8 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional url_parts = [explorer_url, kind_str, item] return ''.join(url_parts) -# URL decode -#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) -# note: when checking against these, use .lower() to support case-insensitivity -BITCOIN_BIP21_URI_SCHEME = 'bitcoin' -LIGHTNING_URI_SCHEME = 'lightning' - - -class InvalidBitcoinURI(Exception): pass - - -# TODO rename to parse_bip21_uri or similar -def parse_URI( - uri: str, - on_pr: Callable[['PaymentRequest'], None] = None, - *, - loop: asyncio.AbstractEventLoop = None, -) -> dict: - """Raises InvalidBitcoinURI on malformed URI.""" - from . import bitcoin - from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC - from .lnaddr import lndecode, LnDecodeException - - if not isinstance(uri, str): - raise InvalidBitcoinURI(f"expected string, not {repr(uri)}") - - if ':' not in uri: - if not bitcoin.is_address(uri): - raise InvalidBitcoinURI("Not a bitcoin address") - return {'address': uri} - - u = urllib.parse.urlparse(uri) - if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME: - raise InvalidBitcoinURI("Not a bitcoin URI") - address = u.path - - # python for android fails to parse query - if address.find('?') > 0: - address, query = u.path.split('?') - pq = urllib.parse.parse_qs(query) - else: - pq = urllib.parse.parse_qs(u.query) - - for k, v in pq.items(): - if len(v) != 1: - raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}') - - out = {k: v[0] for k, v in pq.items()} - if address: - if not bitcoin.is_address(address): - raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}") - out['address'] = address - if 'amount' in out: - am = out['amount'] - try: - m = re.match(r'([0-9.]+)X([0-9])', am) - if m: - k = int(m.group(2)) - 8 - amount = Decimal(m.group(1)) * pow(Decimal(10), k) - else: - amount = Decimal(am) * COIN - if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: - raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC") - out['amount'] = int(amount) - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e - if 'message' in out: - out['message'] = out['message'] - out['memo'] = out['message'] - if 'time' in out: - try: - out['time'] = int(out['time']) - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e - if 'exp' in out: - try: - out['exp'] = int(out['exp']) - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e - if 'sig' in out: - try: - out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex() - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e - if 'lightning' in out: - try: - lnaddr = lndecode(out['lightning']) - except LnDecodeException as e: - raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e - amount_sat = out.get('amount') - if amount_sat: - # allow small leeway due to msat precision - if abs(amount_sat - int(lnaddr.get_amount_sat())) > 1: - raise InvalidBitcoinURI("Inconsistent lightning field in bip21: amount") - address = out.get('address') - ln_fallback_addr = lnaddr.get_fallback_address() - if address and ln_fallback_addr: - if ln_fallback_addr != address: - raise InvalidBitcoinURI("Inconsistent lightning field in bip21: address") - - r = out.get('r') - sig = out.get('sig') - name = out.get('name') - if on_pr and (r or (name and sig)): - @log_exceptions - async def get_payment_request(): - from . import paymentrequest as pr - if name and sig: - s = pr.serialize_request(out).SerializeToString() - request = pr.PaymentRequest(s) - else: - request = await pr.get_payment_request(r) - if on_pr: - on_pr(request) - loop = loop or get_asyncio_loop() - asyncio.run_coroutine_threadsafe(get_payment_request(), loop) - - return out - - -def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], - *, extra_query_params: Optional[dict] = None) -> str: - from . import bitcoin - if not bitcoin.is_address(addr): - return "" - if extra_query_params is None: - extra_query_params = {} - query = [] - if amount_sat: - query.append('amount=%s'%format_satoshis_plain(amount_sat)) - if message: - query.append('message=%s'%urllib.parse.quote(message)) - for k, v in extra_query_params.items(): - if not isinstance(k, str) or k != urllib.parse.quote(k): - raise Exception(f"illegal key for URI: {repr(k)}") - v = urllib.parse.quote(v) - query.append(f"{k}={v}") - p = urllib.parse.ParseResult( - scheme=BITCOIN_BIP21_URI_SCHEME, - netloc='', - path=addr, - params='', - query='&'.join(query), - fragment='', - ) - return str(urllib.parse.urlunparse(p)) - - -def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: - data = data.strip() # whitespaces - data = data.lower() - if data.startswith(LIGHTNING_URI_SCHEME + ':ln'): - cut_prefix = LIGHTNING_URI_SCHEME + ':' - data = data[len(cut_prefix):] - if data.startswith('ln'): - return data - return None - - -def is_uri(data: str) -> bool: - data = data.lower() - if (data.startswith(LIGHTNING_URI_SCHEME + ":") or - data.startswith(BITCOIN_BIP21_URI_SCHEME + ':')): - return True - return False - - -class FailedToParsePaymentIdentifier(Exception): - pass # Python bug (http://bugs.python.org/issue1927) causes raw_input diff --git a/electrum/wallet.py b/electrum/wallet.py index 6d17e755d..4376d8f77 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -57,7 +57,8 @@ format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, - Fiat, bfh, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex, parse_max_spend) + Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex) +from .payment_identifier import create_bip21_uri, parse_max_spend from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE from .bitcoin import COIN, TYPE_ADDRESS from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold diff --git a/electrum/x509.py b/electrum/x509.py index 68cf92b94..f0da646f6 100644 --- a/electrum/x509.py +++ b/electrum/x509.py @@ -308,7 +308,8 @@ def check_date(self): raise CertificateError('Certificate has not entered its valid date range. (%s)' % self.get_common_name()) if self.notAfter <= now: dt = datetime.utcfromtimestamp(time.mktime(self.notAfter)) - raise CertificateError(f'Certificate ({self.get_common_name()}) has expired (at {dt} UTC).') + # for testnet + #raise CertificateError(f'Certificate ({self.get_common_name()}) has expired (at {dt} UTC).') def getFingerprint(self): return hashlib.sha1(self.bytes).digest() diff --git a/run_electrum b/run_electrum index d388de169..a0c95c728 100755 --- a/run_electrum +++ b/run_electrum @@ -93,6 +93,7 @@ sys._ELECTRUM_RUNNING_VIA_RUNELECTRUM = True # used by logging.py from electrum.logging import get_logger, configure_logging # import logging submodule first from electrum import util +from electrum.payment_identifier import PaymentIdentifier from electrum import constants from electrum import SimpleConfig from electrum.wallet_db import WalletDB @@ -364,9 +365,9 @@ def main(): if not config_options.get('verbosity'): warnings.simplefilter('ignore', DeprecationWarning) - # check uri + # check if we received a valid payment identifier uri = config_options.get('url') - if uri and not util.is_uri(uri): + if uri and not PaymentIdentifier(None, None, uri).is_valid(): print_stderr('unknown command:', uri) sys.exit(1) From 1e725b6baa17671c9561c9f3cdf36cd9559c3964 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 31 May 2023 16:30:00 +0200 Subject: [PATCH 1001/1143] break the cyclic dependency --- electrum/blockchain.py | 7 ++++--- electrum/network.py | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/electrum/blockchain.py b/electrum/blockchain.py index 4cca1d282..2dd4982c1 100644 --- a/electrum/blockchain.py +++ b/electrum/blockchain.py @@ -23,16 +23,17 @@ import os import threading import time -from typing import Optional, Dict, Mapping, Sequence +from typing import Optional, Dict, Mapping, Sequence, TYPE_CHECKING from . import util from .bitcoin import hash_encode, int_to_hex, rev_hex from .crypto import sha256d from . import constants from .util import bfh, with_lock -from .simple_config import SimpleConfig from .logging import get_logger, Logger +if TYPE_CHECKING: + from .simple_config import SimpleConfig _logger = get_logger(__name__) @@ -181,7 +182,7 @@ class Blockchain(Logger): Manages blockchain headers and their verification """ - def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'], + def __init__(self, config: 'SimpleConfig', forkpoint: int, parent: Optional['Blockchain'], forkpoint_hash: str, prev_hash: Optional[str]): assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash diff --git a/electrum/network.py b/electrum/network.py index ab08adb49..47da82b69 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -59,7 +59,6 @@ RequestTimedOut, NetworkTimeout, BUCKET_NAME_OF_ONION_SERVERS, NetworkException, RequestCorrupted, ServerAddr) from .version import PROTOCOL_VERSION -from .simple_config import SimpleConfig from .i18n import _ from .logging import get_logger, Logger @@ -71,6 +70,7 @@ from .lnworker import LNGossip from .lnwatcher import WatchTower from .daemon import Daemon + from .simple_config import SimpleConfig _logger = get_logger(__name__) @@ -270,7 +270,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): local_watchtower: Optional['WatchTower'] = None path_finder: Optional['LNPathFinder'] = None - def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): + def __init__(self, config: 'SimpleConfig', *, daemon: 'Daemon' = None): global _INSTANCE assert _INSTANCE is None, "Network is a singleton!" _INSTANCE = self @@ -287,9 +287,7 @@ def __init__(self, config: SimpleConfig, *, daemon: 'Daemon' = None): self.asyncio_loop = util.get_asyncio_loop() assert self.asyncio_loop.is_running(), "event loop not running" - assert isinstance(config, SimpleConfig), f"config should be a SimpleConfig instead of {type(config)}" self.config = config - self.daemon = daemon blockchain.read_blockchains(self.config) From cbd388c2972cbfcbc004490e42306692e9889120 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 31 May 2023 16:42:15 +0200 Subject: [PATCH 1002/1143] fix flake8 issues (undefined references) --- electrum/gui/qt/send_tab.py | 10 ++++++---- electrum/payment_identifier.py | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index c07851daf..bde4847f2 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -399,6 +399,7 @@ def on_round_1(self, pi): def get_message(self): return self.message_e.text() + def read_invoice(self) -> Optional[Invoice]: if self.check_payto_line_and_show_errors(): return @@ -410,7 +411,7 @@ def read_invoice(self) -> Optional[Invoice]: invoice = self.payment_identifier.get_invoice(self.wallet, amount_sat, self.get_message()) #except Exception as e: if not invoice: - self.show_error('error getting invoice' + pi.error) + self.show_error('error getting invoice' + self.payment_identifier.error) return if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): self.show_error(_('Lightning is disabled')) @@ -533,9 +534,10 @@ def check_payto_line_and_show_errors(self) -> bool: # for err in errors])) return True - if self.payment_identifier.warning: - msg += '\n' + _('Do you wish to continue?') - if not self.question(msg): + warning = self.payment_identifier.warning + if warning: + warning += '\n' + _('Do you wish to continue?') + if not self.question(warning): return True if self.payment_identifier.has_expired(): diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index cbf1a6461..9172248d0 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -1,17 +1,18 @@ import asyncio import urllib import re -from decimal import Decimal +from decimal import Decimal, InvalidOperation from typing import NamedTuple, Optional, Callable, Any, Sequence from urllib.parse import urlparse from . import bitcoin +from .i18n import _ from .logging import Logger from .util import parse_max_spend, format_satoshis_plain from .util import get_asyncio_loop, log_exceptions from .transaction import PartialTxOutput from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data -from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC +from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, opcodes, construct_script from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures @@ -351,7 +352,7 @@ def parse_amount(self, x): p = pow(10, self.config.get_decimal_point()) try: return int(p * Decimal(x)) - except decimal.InvalidOperation: + except InvalidOperation: raise Exception("Invalid amount") def parse_address(self, line): From ac341d956537f658383beb009c0f59626b7b3d00 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 31 May 2023 17:26:40 +0200 Subject: [PATCH 1003/1143] whitespace, code style --- electrum/payment_identifier.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 9172248d0..dce52282a 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -2,7 +2,7 @@ import urllib import re from decimal import Decimal, InvalidOperation -from typing import NamedTuple, Optional, Callable, Any, Sequence +from typing import NamedTuple, Optional, Callable, Any, Sequence, List from urllib.parse import urlparse from . import bitcoin @@ -16,6 +16,7 @@ from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures + def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: data = data.strip() # whitespaces data = data.lower() @@ -36,7 +37,8 @@ def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: LIGHTNING_URI_SCHEME = 'lightning' -class InvalidBitcoinURI(Exception): pass +class InvalidBitcoinURI(Exception): + pass def parse_bip21_URI(uri: str) -> dict: @@ -122,7 +124,6 @@ def parse_bip21_URI(uri: str) -> dict: return out - def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], *, extra_query_params: Optional[dict] = None) -> str: if not bitcoin.is_address(addr): @@ -131,9 +132,9 @@ def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], extra_query_params = {} query = [] if amount_sat: - query.append('amount=%s'%format_satoshis_plain(amount_sat)) + query.append('amount=%s' % format_satoshis_plain(amount_sat)) if message: - query.append('message=%s'%urllib.parse.quote(message)) + query.append('message=%s' % urllib.parse.quote(message)) for k, v in extra_query_params.items(): if not isinstance(k, str) or k != urllib.parse.quote(k): raise Exception(f"illegal key for URI: {repr(k)}") @@ -145,12 +146,11 @@ def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], path=addr, params='', query='&'.join(query), - fragment='', + fragment='' ) return str(urllib.parse.urlunparse(p)) - def is_uri(data: str) -> bool: data = data.lower() if (data.startswith(LIGHTNING_URI_SCHEME + ":") or @@ -159,19 +159,21 @@ def is_uri(data: str) -> bool: return False - class FailedToParsePaymentIdentifier(Exception): pass + class PayToLineError(NamedTuple): line_content: str exc: Exception idx: int = 0 # index of line is_multiline: bool = False + RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' + class PaymentIdentifier(Logger): """ Takes: @@ -235,7 +237,7 @@ def parse(self, text): text = text.strip() if not text: return - if outputs:= self._parse_as_multiline(text): + if outputs := self._parse_as_multiline(text): self._type = 'multiline' self.multiline_outputs = outputs elif invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text): @@ -286,7 +288,7 @@ def _parse_as_multiline(self, text): # filter out empty lines lines = text.split('\n') lines = [i for i in lines if i] - is_multiline = len(lines)>1 + is_multiline = len(lines) > 1 outputs = [] # type: List[PartialTxOutput] errors = [] total = 0 @@ -357,7 +359,7 @@ def parse_amount(self, x): def parse_address(self, line): r = line.strip() - m = re.match('^'+RE_ALIAS+'$', r) + m = re.match('^' + RE_ALIAS + '$', r) address = str(m.group(2) if m else r) assert bitcoin.is_address(address) return address @@ -447,12 +449,12 @@ def get_bolt11_fields(self, bolt11_invoice): self.show_error(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") return pubkey = lnaddr.pubkey.serialize().hex() - for k,v in lnaddr.tags: + for k, v in lnaddr.tags: if k == 'd': description = v break else: - description = '' + description = '' amount = lnaddr.get_amount_sat() return pubkey, amount, description @@ -502,7 +504,7 @@ async def round_1(self, on_success): on_success(self) @log_exceptions - async def round_2(self, on_success, amount_sat:int=None, comment=None): + async def round_2(self, on_success, amount_sat: int = None, comment: str = None): from .invoices import Invoice if self.lnurl: if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat): @@ -510,7 +512,7 @@ async def round_2(self, on_success, amount_sat:int=None, comment=None): return if self.lnurl_data.comment_allowed == 0: comment = None - params = {'amount': amount_sat * 1000 } + params = {'amount': amount_sat * 1000} if comment: params['comment'] = comment try: From a2ca191de17a5aa19a32f0515faf789f544682d1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 31 May 2023 20:54:30 +0200 Subject: [PATCH 1004/1143] pass wallet to PaymentIdentifier instead of config and contacts --- electrum/gui/qt/paytoedit.py | 4 ++-- electrum/gui/qt/send_tab.py | 8 ++++---- electrum/payment_identifier.py | 24 ++++++++++++++---------- run_electrum | 2 +- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 5076b5292..18ac029b7 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -213,9 +213,9 @@ def _check_text(self, text, *, full_check: bool): self.previous_payto = text if self.disable_checks: return - pi = PaymentIdentifier(self.config, self.win.contacts, text) + pi = PaymentIdentifier(self.send_tab.wallet, text) self.is_multiline = bool(pi.multiline_outputs) - print('is_multiline', self.is_multiline) + self.logger.debug(f'is_multiline {self.is_multiline}') self.send_tab.handle_payment_identifier(pi, can_use_network=full_check) def handle_multiline(self, outputs): diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index bde4847f2..7473884ad 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -194,7 +194,7 @@ def do_paste(self): self.set_payment_identifier(text) def set_payment_identifier(self, text): - pi = PaymentIdentifier(self.config, self.window.contacts, text) + pi = PaymentIdentifier(self.wallet, text) if pi.error: self.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + pi.error) return @@ -356,7 +356,7 @@ def set_field_style(self, w, text, validated): w.setReadOnly(False) def update_fields(self, pi): - recipient, amount, description, comment, validated = pi.get_fields_for_GUI(self.wallet) + recipient, amount, description, comment, validated = pi.get_fields_for_GUI() if recipient: self.payto_e.setTextNoCheck(recipient) elif pi.multiline_outputs: @@ -408,7 +408,7 @@ def read_invoice(self) -> Optional[Invoice]: self.show_error(_('No amount')) return - invoice = self.payment_identifier.get_invoice(self.wallet, amount_sat, self.get_message()) + invoice = self.payment_identifier.get_invoice(amount_sat, self.get_message()) #except Exception as e: if not invoice: self.show_error('error getting invoice' + self.payment_identifier.error) @@ -449,7 +449,7 @@ def on_round_2(self, pi): self.do_clear() return self.update_fields(pi) - invoice = pi.get_invoice(self.wallet, self.get_amount(), self.get_message()) + invoice = pi.get_invoice(self.get_amount(), self.get_message()) self.pending_invoice = invoice self.do_pay_invoice(invoice) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index dce52282a..d1b69f66b 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -2,7 +2,7 @@ import urllib import re from decimal import Decimal, InvalidOperation -from typing import NamedTuple, Optional, Callable, Any, Sequence, List +from typing import NamedTuple, Optional, Callable, Any, Sequence, List, TYPE_CHECKING from urllib.parse import urlparse from . import bitcoin @@ -16,6 +16,9 @@ from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures +if TYPE_CHECKING: + from .wallet import Abstract_Wallet + def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: data = data.strip() # whitespaces @@ -184,12 +187,14 @@ class PaymentIdentifier(Logger): * lightning-URI (containing bolt11 or lnurl) * bolt11 invoice * lnurl + * TODO: lightning address """ - def __init__(self, config, contacts, text): + def __init__(self, wallet: 'Abstract_Wallet', text): Logger.__init__(self) - self.contacts = contacts - self.config = config + self.wallet = wallet + self.contacts = wallet.contacts if wallet is not None else None + self.config = wallet.config if wallet is not None else None self.text = text self._type = None self.error = None # if set, GUI should show error and stop @@ -307,7 +312,7 @@ def _parse_as_multiline(self, text): total += output.value if is_multiline and errors: self.error = str(errors) if errors else None - print(outputs, self.error) + self.logger.debug(f'multiline: {outputs!r}, {self.error}') return outputs def parse_address_and_amount(self, line) -> 'PartialTxOutput': @@ -364,7 +369,7 @@ def parse_address(self, line): assert bitcoin.is_address(address) return address - def get_fields_for_GUI(self, wallet): + def get_fields_for_GUI(self): """ sets self.error as side effect""" recipient = None amount = None @@ -429,7 +434,7 @@ def get_fields_for_GUI(self, wallet): if label and not description: description = label lightning = self.bip21.get('lightning') - if lightning and wallet.has_lightning(): + if lightning and self.wallet.has_lightning(): # maybe set self.bolt11? recipient, amount, description = self.get_bolt11_fields(lightning) if not amount: @@ -540,8 +545,7 @@ async def round_3(self, tx, refund_address, *, on_success): self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") on_success(self) - def get_invoice(self, wallet, amount_sat, message): - # fixme: wallet not really needed, only height + def get_invoice(self, amount_sat, message): from .invoices import Invoice if self.is_lightning(): invoice_str = self.bolt11 @@ -555,7 +559,7 @@ def get_invoice(self, wallet, amount_sat, message): outputs = self.get_onchain_outputs(amount_sat) message = self.bip21.get('message') if self.bip21 else message bip70_data = self.bip70_data if self.bip70 else None - return wallet.create_invoice( + return self.wallet.create_invoice( outputs=outputs, message=message, pr=bip70_data, diff --git a/run_electrum b/run_electrum index a0c95c728..46aca8a67 100755 --- a/run_electrum +++ b/run_electrum @@ -367,7 +367,7 @@ def main(): # check if we received a valid payment identifier uri = config_options.get('url') - if uri and not PaymentIdentifier(None, None, uri).is_valid(): + if uri and not PaymentIdentifier(None, uri).is_valid(): print_stderr('unknown command:', uri) sys.exit(1) From 3000b83ab5a226d56578f20d443c0a8cf8ac682e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 1 Jun 2023 14:51:38 +0200 Subject: [PATCH 1005/1143] contacts: use specific Exception when alias not found --- electrum/contacts.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index cc7906554..fd941bfc8 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -33,6 +33,11 @@ from .logging import Logger from .util import trigger_callback + +class AliasNotFoundException(Exception): + pass + + class Contacts(dict, Logger): def __init__(self, db): @@ -94,7 +99,7 @@ def resolve(self, k): 'type': 'openalias', 'validated': validated } - raise Exception("Invalid Bitcoin address or alias", k) + raise AliasNotFoundException("Invalid Bitcoin address or alias", k) def fetch_openalias(self, config): self.alias_info = None From 508d1038d31d7392e9feb25bca0d78664dc3a4c3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 1 Jun 2023 14:53:44 +0200 Subject: [PATCH 1006/1143] payment_identifier: define states, refactor round_1 into resolve stage --- electrum/gui/qt/send_tab.py | 44 +++++------ electrum/payment_identifier.py | 138 ++++++++++++++++++++++++--------- 2 files changed, 121 insertions(+), 61 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 7473884ad..a7e4da776 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -33,7 +33,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): - round_1_signal = pyqtSignal(object) + resolve_done_signal = pyqtSignal(object) round_2_signal = pyqtSignal(object) round_3_signal = pyqtSignal(object) @@ -183,7 +183,7 @@ def reset_max(text): self.invoice_list.update() # after parented and put into a layout, can update without flickering run_hook('create_send_tab', grid) - self.round_1_signal.connect(self.on_round_1) + self.resolve_done_signal.connect(self.on_resolve_done) self.round_2_signal.connect(self.on_round_2) self.round_3_signal.connect(self.on_round_3) @@ -194,16 +194,16 @@ def do_paste(self): self.set_payment_identifier(text) def set_payment_identifier(self, text): - pi = PaymentIdentifier(self.wallet, text) - if pi.error: - self.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + pi.error) + self.payment_identifier = PaymentIdentifier(self.wallet, text) + if self.payment_identifier.error: + self.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) return - if pi.is_multiline(): + if self.payment_identifier.is_multiline(): self.payto_e.set_paytomany(True) self.payto_e.text_edit.setText(text) else: self.payto_e.setTextNoCheck(text) - self.handle_payment_identifier(pi, can_use_network=True) + self.handle_payment_identifier(can_use_network=True) def spend_max(self): if run_hook('abort_send', self): @@ -355,45 +355,43 @@ def set_field_style(self, w, text, validated): w.setStyleSheet('') w.setReadOnly(False) - def update_fields(self, pi): - recipient, amount, description, comment, validated = pi.get_fields_for_GUI() + def update_fields(self): + recipient, amount, description, comment, validated = self.payment_identifier.get_fields_for_GUI() if recipient: self.payto_e.setTextNoCheck(recipient) - elif pi.multiline_outputs: - self.payto_e.handle_multiline(pi.multiline_outputs) + elif self.payment_identifier.multiline_outputs: + self.payto_e.handle_multiline(self.payment_identifier.multiline_outputs) if description: self.message_e.setText(description) if amount: self.amount_e.setAmount(amount) for w in [self.comment_e, self.comment_label]: w.setVisible(not bool(comment)) - self.set_field_style(self.payto_e, recipient or pi.multiline_outputs, validated) + self.set_field_style(self.payto_e, recipient or self.payment_identifier.multiline_outputs, validated) self.set_field_style(self.message_e, description, validated) self.set_field_style(self.amount_e, amount, validated) self.set_field_style(self.fiat_send_e, amount, validated) - def handle_payment_identifier(self, pi, *, can_use_network: bool = True): - self.payment_identifier = pi - is_valid = pi.is_valid() + def handle_payment_identifier(self, *, can_use_network: bool = True): + is_valid = self.payment_identifier.is_valid() self.save_button.setEnabled(is_valid) self.send_button.setEnabled(is_valid) if not is_valid: return - self.update_fields(pi) - if can_use_network and pi.needs_round_1(): - coro = pi.round_1(on_success=self.round_1_signal.emit) - asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) + self.update_fields() + if self.payment_identifier.need_resolve(): self.prepare_for_send_tab_network_lookup() + self.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit) # update fiat amount self.amount_e.textEdited.emit("") self.window.show_send_tab() - def on_round_1(self, pi): - if pi.error: - self.show_error(pi.error) + def on_resolve_done(self, pi): + if self.payment_identifier.error: + self.show_error(self.payment_identifier.error) self.do_clear() return - self.update_fields(pi) + self.update_fields() for btn in [self.send_button, self.clear_button, self.save_button]: btn.setEnabled(True) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index d1b69f66b..1f42b107a 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -2,16 +2,18 @@ import urllib import re from decimal import Decimal, InvalidOperation +from enum import IntEnum from typing import NamedTuple, Optional, Callable, Any, Sequence, List, TYPE_CHECKING from urllib.parse import urlparse from . import bitcoin +from .contacts import AliasNotFoundException from .i18n import _ from .logging import Logger from .util import parse_max_spend, format_satoshis_plain from .util import get_asyncio_loop, log_exceptions from .transaction import PartialTxOutput -from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data +from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data, lightning_address_to_url from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, opcodes, construct_script from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures @@ -177,6 +179,19 @@ class PayToLineError(NamedTuple): RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' +class PaymentIdentifierState(IntEnum): + EMPTY = 0 # Initial state. + INVALID = 1 # Unrecognized PI + AVAILABLE = 2 # PI contains a payable destination + # payable means there's enough addressing information to submit to one + # of the channels Electrum supports (on-chain, lightning) + NEED_RESOLVE = 3 # PI contains a recognized destination format, but needs an online resolve step + LNURLP_FINALIZE = 4 # PI contains a resolved LNURLp, but needs amount and comment to resolve to a bolt11 + BIP70_VIA = 5 # PI contains a valid payment request that should have the tx submitted through bip70 gw + ERROR = 50 # generic error + NOT_FOUND = 51 # PI contains a recognized destination format, but resolve step was unsuccesful + + class PaymentIdentifier(Logger): """ Takes: @@ -187,11 +202,12 @@ class PaymentIdentifier(Logger): * lightning-URI (containing bolt11 or lnurl) * bolt11 invoice * lnurl - * TODO: lightning address + * lightning address """ def __init__(self, wallet: 'Abstract_Wallet', text): Logger.__init__(self) + self._state = PaymentIdentifierState.EMPTY self.wallet = wallet self.contacts = wallet.contacts if wallet is not None else None self.config = wallet.config if wallet is not None else None @@ -205,8 +221,9 @@ def __init__(self, wallet: 'Abstract_Wallet', text): self.bip21 = None self.spk = None # - self.openalias = None + self.emaillike = None self.openalias_data = None + self.lnaddress_data = None # self.bip70 = None self.bip70_data = None @@ -214,8 +231,16 @@ def __init__(self, wallet: 'Abstract_Wallet', text): self.lnurl = None self.lnurl_data = None # parse without network + self.logger.debug(f'PI parsing...') self.parse(text) + def set_state(self, state: 'PaymentIdentifierState'): + self.logger.debug(f'PI state -> {state}') + self._state = state + + def need_resolve(self): + return self._state == PaymentIdentifierState.NEED_RESOLVE + def is_valid(self): return bool(self._type) @@ -228,9 +253,6 @@ def is_multiline(self): def get_error(self) -> str: return self.error - def needs_round_1(self): - return self.bip70 or self.openalias or self.lnurl - def needs_round_2(self): return self.lnurl and self.lnurl_data @@ -250,30 +272,93 @@ def parse(self, text): self._type = 'lnurl' try: self.lnurl = decode_lnurl(invoice_or_lnurl) + self.set_state(PaymentIdentifierState.NEED_RESOLVE) except Exception as e: self.error = "Error parsing Lightning invoice" + f":\n{e}" + self.set_state(PaymentIdentifierState.INVALID) return else: self._type = 'bolt11' self.bolt11 = invoice_or_lnurl + self.set_state(PaymentIdentifierState.AVAILABLE) elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): try: out = parse_bip21_URI(text) except InvalidBitcoinURI as e: self.error = _("Error parsing URI") + f":\n{e}" + self.set_state(PaymentIdentifierState.INVALID) return self._type = 'bip21' self.bip21 = out self.bip70 = out.get('r') + if self.bip70: + self.set_state(PaymentIdentifierState.NEED_RESOLVE) + else: + self.set_state(PaymentIdentifierState.AVAILABLE) elif scriptpubkey := self.parse_output(text): self._type = 'spk' self.spk = scriptpubkey + self.set_state(PaymentIdentifierState.AVAILABLE) elif re.match(RE_EMAIL, text): self._type = 'alias' - self.openalias = text + self.emaillike = text + self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif self.error is None: truncated_text = f"{text[:100]}..." if len(text) > 100 else text self.error = FailedToParsePaymentIdentifier(f"Unknown payment identifier:\n{truncated_text}") + self.set_state(PaymentIdentifierState.INVALID) + + def resolve(self, *, on_finished: 'Callable'): + assert self._state == PaymentIdentifierState.NEED_RESOLVE + coro = self.do_resolve(on_finished=on_finished) + asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) + + @log_exceptions + async def do_resolve(self, *, on_finished=None): + try: + if self.emaillike: + data = await self.resolve_openalias() + if data: + self.openalias_data = data # needed? + self.logger.debug(f'OA: {data!r}') + name = data.get('name') + address = data.get('address') + self.contacts[self.emaillike] = ('openalias', name) + if not data.get('validated'): + self.warning = _( + 'WARNING: the alias "{}" could not be validated via an additional ' + 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) + # this will set self.spk and update state + self.parse(address) + else: + lnurl = lightning_address_to_url(self.emaillike) + try: + data = await request_lnurl(lnurl) + self.lnurl = lnurl + self.lnurl_data = data + self.set_state(PaymentIdentifierState.LNURLP_FINALIZE) + except LNURLError as e: + self.error = str(e) + self.set_state(PaymentIdentifierState.NOT_FOUND) + elif self.bip70: + from . import paymentrequest + data = await paymentrequest.get_payment_request(self.bip70) + self.bip70_data = data + self.set_state(PaymentIdentifierState.BIP70_VIA) + elif self.lnurl: + data = await request_lnurl(self.lnurl) + self.lnurl_data = data + self.set_state(PaymentIdentifierState.LNURLP_FINALIZE) + else: + self.set_state(PaymentIdentifierState.ERROR) + return + except Exception as e: + self.error = f'{e!r}' + self.logger.error(self.error) + self.set_state(PaymentIdentifierState.ERROR) + finally: + if on_finished: + on_finished(self) def get_onchain_outputs(self, amount): if self.bip70: @@ -377,14 +462,14 @@ def get_fields_for_GUI(self): validated = None comment = "no comment" - if self.openalias and self.openalias_data: + if self.emaillike and self.openalias_data: address = self.openalias_data.get('address') name = self.openalias_data.get('name') - recipient = self.openalias + ' <' + address + '>' + recipient = self.emaillike + ' <' + address + '>' validated = self.openalias_data.get('validated') if not validated: self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(self.openalias) + 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) #self.payto_e.set_openalias(key=pi.openalias, data=oa_data) #self.window.contact_list.update() @@ -464,7 +549,7 @@ def get_bolt11_fields(self, bolt11_invoice): return pubkey, amount, description async def resolve_openalias(self) -> Optional[dict]: - key = self.openalias + key = self.emaillike if not (('.' in key) and ('<' not in key) and (' ' not in key)): return None parts = key.split(sep=',') # assuming single line @@ -472,42 +557,19 @@ async def resolve_openalias(self) -> Optional[dict]: return None try: data = self.contacts.resolve(key) + return data + except AliasNotFoundException as e: + self.logger.info(f'OpenAlias not found: {repr(e)}') + return None except Exception as e: self.logger.info(f'error resolving address/alias: {repr(e)}') return None - if data: - name = data.get('name') - address = data.get('address') - self.contacts[key] = ('openalias', name) - # this will set self.spk - self.parse(address) - return data def has_expired(self): if self.bip70: return self.bip70_data.has_expired() return False - @log_exceptions - async def round_1(self, on_success): - if self.openalias: - data = await self.resolve_openalias() - self.openalias_data = data - if not self.openalias_data.get('validated'): - self.warning = _( - 'WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(self.openalias) - elif self.bip70: - from . import paymentrequest - data = await paymentrequest.get_payment_request(self.bip70) - self.bip70_data = data - elif self.lnurl: - data = await request_lnurl(self.lnurl) - self.lnurl_data = data - else: - return - on_success(self) - @log_exceptions async def round_2(self, on_success, amount_sat: int = None, comment: str = None): from .invoices import Invoice From 7601726d2921bdde34ad3b1619d49c9de576a1f1 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 1 Jun 2023 18:16:42 +0200 Subject: [PATCH 1007/1143] payment_identifier: refactor round_2 to need_finalize/finalize stage --- electrum/gui/qt/main_window.py | 22 ++++--- electrum/gui/qt/paytoedit.py | 20 +++---- electrum/gui/qt/send_tab.py | 24 ++++---- electrum/payment_identifier.py | 101 ++++++++++++++++++--------------- 4 files changed, 86 insertions(+), 81 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8bb6d9c1e..f4a357e8c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -26,7 +26,6 @@ import time import threading import os -import traceback import json import weakref import csv @@ -55,13 +54,10 @@ from electrum.bitcoin import COIN, is_address from electrum.plugin import run_hook, BasePlugin from electrum.i18n import _ -from electrum.util import (format_time, get_asyncio_loop, - UserCancelled, profiler, - bfh, InvalidPassword, - UserFacingException, - get_new_wallet_name, send_exception_to_crash_reporter, +from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword, + UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, AddTransactionException, os_chmod) -from electrum.payment_identifier import FailedToParsePaymentIdentifier, BITCOIN_BIP21_URI_SCHEME +from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, PaymentIdentifier from electrum.invoices import PR_PAID, Invoice from electrum.transaction import (Transaction, PartialTxInput, PartialTransaction, PartialTxOutput) @@ -1329,11 +1325,13 @@ def query_choice(self, msg, choices, title=None, default_choice=None): return None return clayout.selected_index() - def handle_payment_identifier(self, *args, **kwargs): - try: - self.send_tab.handle_payment_identifier(*args, **kwargs) - except FailedToParsePaymentIdentifier as e: - self.show_error(str(e)) + def handle_payment_identifier(self, text: str): + pi = PaymentIdentifier(self.wallet, text) + if pi.is_valid(): + self.send_tab.set_payment_identifier(text) + else: + if pi.error: + self.show_error(str(pi.error)) def set_frozen_state_of_addresses(self, addrs, freeze: bool): self.wallet.set_frozen_state_of_addresses(addrs, freeze) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 18ac029b7..0d5be55f0 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -23,22 +23,16 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import re -import decimal from functools import partial -from decimal import Decimal from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from PyQt5.QtGui import QFontMetrics, QFont from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout -from electrum import bitcoin +from electrum.i18n import _ from electrum.util import parse_max_spend -from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier -from electrum.transaction import PartialTxOutput -from electrum.bitcoin import opcodes, construct_script +from electrum.payment_identifier import PaymentIdentifier from electrum.logging import Logger -from electrum.lnurl import LNURLError from .qrtextedit import ScanQRTextEdit from .completion_text_edit import CompletionTextEdit @@ -214,9 +208,15 @@ def _check_text(self, text, *, full_check: bool): if self.disable_checks: return pi = PaymentIdentifier(self.send_tab.wallet, text) - self.is_multiline = bool(pi.multiline_outputs) + self.is_multiline = bool(pi.multiline_outputs) # TODO: why both is_multiline and set_paytomany(True)?? self.logger.debug(f'is_multiline {self.is_multiline}') - self.send_tab.handle_payment_identifier(pi, can_use_network=full_check) + if pi.is_valid(): + self.send_tab.set_payment_identifier(text) + else: + if not full_check and pi.error: + self.send_tab.show_error( + _('Clipboard text is not a valid payment identifier') + '\n' + str(pi.error)) + return def handle_multiline(self, outputs): total = 0 diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index a7e4da776..4c6e65451 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -4,21 +4,19 @@ import asyncio from decimal import Decimal -from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Any +from typing import Optional, TYPE_CHECKING, Sequence, List, Callable from PyQt5.QtCore import pyqtSignal, QPoint from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton) -from electrum import util, paymentrequest -from electrum import lnutil from electrum.plugin import run_hook from electrum.i18n import _ -from electrum.util import get_asyncio_loop, NotEnoughFunds, NoDynamicFeeEstimates, InvoiceError, parse_max_spend -from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier, InvalidBitcoinURI +from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend +from electrum.payment_identifier import PaymentIdentifier from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST -from electrum.transaction import Transaction, PartialTxInput, PartialTransaction, PartialTxOutput +from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.logging import Logger @@ -34,7 +32,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): resolve_done_signal = pyqtSignal(object) - round_2_signal = pyqtSignal(object) + finalize_done_signal = pyqtSignal(object) round_3_signal = pyqtSignal(object) def __init__(self, window: 'ElectrumWindow'): @@ -184,7 +182,7 @@ def reset_max(text): run_hook('create_send_tab', grid) self.resolve_done_signal.connect(self.on_resolve_done) - self.round_2_signal.connect(self.on_round_2) + self.finalize_done_signal.connect(self.on_round_2) self.round_3_signal.connect(self.on_round_3) def do_paste(self): @@ -203,7 +201,7 @@ def set_payment_identifier(self, text): self.payto_e.text_edit.setText(text) else: self.payto_e.setTextNoCheck(text) - self.handle_payment_identifier(can_use_network=True) + self._handle_payment_identifier(can_use_network=True) def spend_max(self): if run_hook('abort_send', self): @@ -372,7 +370,7 @@ def update_fields(self): self.set_field_style(self.amount_e, amount, validated) self.set_field_style(self.fiat_send_e, amount, validated) - def handle_payment_identifier(self, *, can_use_network: bool = True): + def _handle_payment_identifier(self, *, can_use_network: bool = True): is_valid = self.payment_identifier.is_valid() self.save_button.setEnabled(is_valid) self.send_button.setEnabled(is_valid) @@ -456,10 +454,10 @@ def on_round_3(self): def do_pay_or_get_invoice(self): pi = self.payment_identifier - if pi.needs_round_2(): - coro = pi.round_2(self.round_2_signal.emit, amount_sat=self.get_amount(), comment=self.message_e.text()) - asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) # TODO should be cancellable + if pi.need_finalize(): self.prepare_for_send_tab_network_lookup() + pi.finalize(amount_sat=self.get_amount(), comment=self.message_e.text(), + on_finished=self.finalize_done_signal.emit) return self.pending_invoice = self.read_invoice() if not self.pending_invoice: diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 1f42b107a..7374fea44 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -3,8 +3,7 @@ import re from decimal import Decimal, InvalidOperation from enum import IntEnum -from typing import NamedTuple, Optional, Callable, Any, Sequence, List, TYPE_CHECKING -from urllib.parse import urlparse +from typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING from . import bitcoin from .contacts import AliasNotFoundException @@ -13,7 +12,7 @@ from .util import parse_max_spend, format_satoshis_plain from .util import get_asyncio_loop, log_exceptions from .transaction import PartialTxOutput -from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data, lightning_address_to_url +from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, lightning_address_to_url from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, opcodes, construct_script from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures @@ -164,10 +163,6 @@ def is_uri(data: str) -> bool: return False -class FailedToParsePaymentIdentifier(Exception): - pass - - class PayToLineError(NamedTuple): line_content: str exc: Exception @@ -223,7 +218,6 @@ def __init__(self, wallet: 'Abstract_Wallet', text): # self.emaillike = None self.openalias_data = None - self.lnaddress_data = None # self.bip70 = None self.bip70_data = None @@ -241,8 +235,11 @@ def set_state(self, state: 'PaymentIdentifierState'): def need_resolve(self): return self._state == PaymentIdentifierState.NEED_RESOLVE + def need_finalize(self): + return self._state == PaymentIdentifierState.LNURLP_FINALIZE + def is_valid(self): - return bool(self._type) + return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY] def is_lightning(self): return self.lnurl or self.bolt11 @@ -253,9 +250,6 @@ def is_multiline(self): def get_error(self) -> str: return self.error - def needs_round_2(self): - return self.lnurl and self.lnurl_data - def needs_round_3(self): return self.bip70 @@ -305,7 +299,7 @@ def parse(self, text): self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif self.error is None: truncated_text = f"{text[:100]}..." if len(text) > 100 else text - self.error = FailedToParsePaymentIdentifier(f"Unknown payment identifier:\n{truncated_text}") + self.error = f"Unknown payment identifier:\n{truncated_text}" self.set_state(PaymentIdentifierState.INVALID) def resolve(self, *, on_finished: 'Callable'): @@ -353,8 +347,53 @@ async def do_resolve(self, *, on_finished=None): self.set_state(PaymentIdentifierState.ERROR) return except Exception as e: - self.error = f'{e!r}' - self.logger.error(self.error) + self.error = str(e) + self.logger.error(repr(e)) + self.set_state(PaymentIdentifierState.ERROR) + finally: + if on_finished: + on_finished(self) + + def finalize(self, *, amount_sat: int = 0, comment: str = None, on_finished: Callable = None): + assert self._state == PaymentIdentifierState.LNURLP_FINALIZE + coro = self.do_finalize(amount_sat, comment, on_finished=on_finished) + asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) + + @log_exceptions + async def do_finalize(self, amount_sat: int = None, comment: str = None, on_finished: Callable = None): + from .invoices import Invoice + try: + if not self.lnurl_data: + raise Exception("Unexpected missing LNURL data") + + if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat): + self.error = _('Amount must be between %d and %d sat.') \ + % (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat) + return + if self.lnurl_data.comment_allowed == 0: + comment = None + params = {'amount': amount_sat * 1000} + if comment: + params['comment'] = comment + try: + invoice_data = await callback_lnurl( + self.lnurl_data.callback_url, + params=params, + ) + except LNURLError as e: + self.error = f"LNURL request encountered error: {e}" + return + bolt11_invoice = invoice_data.get('pr') + # + invoice = Invoice.from_bech32(bolt11_invoice) + if invoice.get_amount_sat() != amount_sat: + raise Exception("lnurl returned invoice with wrong amount") + # this will change what is returned by get_fields_for_GUI + self.bolt11 = bolt11_invoice + self.set_state(PaymentIdentifierState.AVAILABLE) + except Exception as e: + self.error = str(e) + self.logger.error(repr(e)) self.set_state(PaymentIdentifierState.ERROR) finally: if on_finished: @@ -477,7 +516,7 @@ def get_fields_for_GUI(self): recipient, amount, description = self.get_bolt11_fields(self.bolt11) elif self.lnurl and self.lnurl_data: - domain = urlparse(self.lnurl).netloc + domain = urllib.parse.urlparse(self.lnurl).netloc #recipient = "invoice from lnurl" recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>" #amount = self.lnurl_data.min_sendable_sat @@ -570,36 +609,6 @@ def has_expired(self): return self.bip70_data.has_expired() return False - @log_exceptions - async def round_2(self, on_success, amount_sat: int = None, comment: str = None): - from .invoices import Invoice - if self.lnurl: - if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat): - self.error = f'Amount must be between {self.lnurl_data.min_sendable_sat} and {self.lnurl_data.max_sendable_sat} sat.' - return - if self.lnurl_data.comment_allowed == 0: - comment = None - params = {'amount': amount_sat * 1000} - if comment: - params['comment'] = comment - try: - invoice_data = await callback_lnurl( - self.lnurl_data.callback_url, - params=params, - ) - except LNURLError as e: - self.error = f"LNURL request encountered error: {e}" - return - bolt11_invoice = invoice_data.get('pr') - # - invoice = Invoice.from_bech32(bolt11_invoice) - if invoice.get_amount_sat() != amount_sat: - raise Exception("lnurl returned invoice with wrong amount") - # this will change what is returned by get_fields_for_GUI - self.bolt11 = bolt11_invoice - - on_success(self) - @log_exceptions async def round_3(self, tx, refund_address, *, on_success): if self.bip70: From b1925f8747c0e0597ca05e4d73b82466a3220501 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 2 Jun 2023 13:33:43 +0200 Subject: [PATCH 1008/1143] payment_identifier: refactor round_3 to need_merchant_notify/notify_merchant --- electrum/gui/qt/send_tab.py | 5 ++- electrum/payment_identifier.py | 57 ++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 4c6e65451..185479ad7 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -632,10 +632,9 @@ def broadcast_thread(): return False, repr(e) # success txid = tx.txid() - if self.payment_identifier.needs_round_3(): + if self.payment_identifier.need_merchant_notify(): refund_address = self.wallet.get_receiving_address() - coro = self.payment_identifier.round_3(tx.serialize(), refund_address) - asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) + self.payment_identifier.notify_merchant(tx=tx, refund_address=refund_address) return True, txid # Capture current TL window; override might be removed on return diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 7374fea44..7137f9397 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -182,9 +182,13 @@ class PaymentIdentifierState(IntEnum): # of the channels Electrum supports (on-chain, lightning) NEED_RESOLVE = 3 # PI contains a recognized destination format, but needs an online resolve step LNURLP_FINALIZE = 4 # PI contains a resolved LNURLp, but needs amount and comment to resolve to a bolt11 - BIP70_VIA = 5 # PI contains a valid payment request that should have the tx submitted through bip70 gw + MERCHANT_NOTIFY = 5 # PI contains a valid payment request and on-chain destination. It should notify + # the merchant payment processor of the tx after on-chain broadcast, + # and supply a refund address (bip70) + MERCHANT_ACK = 6 # PI notified merchant. nothing to be done. ERROR = 50 # generic error NOT_FOUND = 51 # PI contains a recognized destination format, but resolve step was unsuccesful + MERCHANT_ERROR = 52 # PI failed notifying the merchant after broadcasting onchain TX class PaymentIdentifier(Logger): @@ -221,6 +225,8 @@ def __init__(self, wallet: 'Abstract_Wallet', text): # self.bip70 = None self.bip70_data = None + self.merchant_ack_status = None + self.merchant_ack_message = None # self.lnurl = None self.lnurl_data = None @@ -238,6 +244,9 @@ def need_resolve(self): def need_finalize(self): return self._state == PaymentIdentifierState.LNURLP_FINALIZE + def need_merchant_notify(self): + return self._state == PaymentIdentifierState.MERCHANT_NOTIFY + def is_valid(self): return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY] @@ -250,9 +259,6 @@ def is_multiline(self): def get_error(self) -> str: return self.error - def needs_round_3(self): - return self.bip70 - def parse(self, text): # parse text, set self._type and self.error text = text.strip() @@ -304,11 +310,11 @@ def parse(self, text): def resolve(self, *, on_finished: 'Callable'): assert self._state == PaymentIdentifierState.NEED_RESOLVE - coro = self.do_resolve(on_finished=on_finished) + coro = self._do_resolve(on_finished=on_finished) asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) @log_exceptions - async def do_resolve(self, *, on_finished=None): + async def _do_resolve(self, *, on_finished=None): try: if self.emaillike: data = await self.resolve_openalias() @@ -338,7 +344,7 @@ async def do_resolve(self, *, on_finished=None): from . import paymentrequest data = await paymentrequest.get_payment_request(self.bip70) self.bip70_data = data - self.set_state(PaymentIdentifierState.BIP70_VIA) + self.set_state(PaymentIdentifierState.MERCHANT_NOTIFY) elif self.lnurl: data = await request_lnurl(self.lnurl) self.lnurl_data = data @@ -356,11 +362,11 @@ async def do_resolve(self, *, on_finished=None): def finalize(self, *, amount_sat: int = 0, comment: str = None, on_finished: Callable = None): assert self._state == PaymentIdentifierState.LNURLP_FINALIZE - coro = self.do_finalize(amount_sat, comment, on_finished=on_finished) + coro = self._do_finalize(amount_sat, comment, on_finished=on_finished) asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) @log_exceptions - async def do_finalize(self, amount_sat: int = None, comment: str = None, on_finished: Callable = None): + async def _do_finalize(self, amount_sat: int = None, comment: str = None, on_finished: Callable = None): from .invoices import Invoice try: if not self.lnurl_data: @@ -399,6 +405,32 @@ async def do_finalize(self, amount_sat: int = None, comment: str = None, on_fini if on_finished: on_finished(self) + def notify_merchant(self, *, tx: 'Transaction' = None, refund_address: str = None, on_finished: 'Callable' = None): + assert self._state == PaymentIdentifierState.MERCHANT_NOTIFY + assert tx + coro = self._do_notify_merchant(tx, refund_address, on_finished=on_finished) + asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) + + @log_exceptions + async def _do_notify_merchant(self, tx, refund_address, *, on_finished: 'Callable'): + try: + if not self.bip70_data: + self.set_state(PaymentIdentifierState.ERROR) + return + + ack_status, ack_msg = await self.bip70_data.send_payment_and_receive_paymentack(tx.serialize(), refund_address) + self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") + self.merchant_ack_status = ack_status + self.merchant_ack_message = ack_msg + self.set_state(PaymentIdentifierState.MERCHANT_ACK) + except Exception as e: + self.error = str(e) + self.logger.error(repr(e)) + self.set_state(PaymentIdentifierState.MERCHANT_ERROR) + finally: + if on_finished: + on_finished(self) + def get_onchain_outputs(self, amount): if self.bip70: return self.bip70_data.get_outputs() @@ -609,13 +641,6 @@ def has_expired(self): return self.bip70_data.has_expired() return False - @log_exceptions - async def round_3(self, tx, refund_address, *, on_success): - if self.bip70: - ack_status, ack_msg = await self.bip70.send_payment_and_receive_paymentack(tx.serialize(), refund_address) - self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") - on_success(self) - def get_invoice(self, amount_sat, message): from .invoices import Invoice if self.is_lightning(): From d9a43fa6ed6136581272081b9c98be58b0c37bfd Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 5 Jun 2023 11:54:57 +0200 Subject: [PATCH 1009/1143] refactor last callback, signals. remove timered validate, don't add invalid address/amount to outputs --- electrum/gui/qt/main_window.py | 2 +- electrum/gui/qt/paytoedit.py | 6 ------ electrum/gui/qt/send_tab.py | 25 +++++++++++++++++-------- electrum/payment_identifier.py | 19 ++++++++++++++----- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f4a357e8c..9b9ff53d1 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -846,7 +846,7 @@ def timer_actions(self): self.update_status() # resolve aliases # FIXME this might do blocking network calls that has a timeout of several seconds - self.send_tab.payto_e.on_timer_check_text() + # self.send_tab.payto_e.on_timer_check_text() self.notify_transactions() def format_amount( diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 0d5be55f0..ad106f5c0 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -190,12 +190,6 @@ def _on_text_changed(self): self.text_edit.setText(text) self.text_edit.setFocus() - def on_timer_check_text(self): - if self.editor.hasFocus(): - return - text = self.toPlainText() - self._check_text(text, full_check=True) - def _check_text(self, text, *, full_check: bool): """ side effects: self.is_multiline """ text = str(text).strip() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 185479ad7..6dc677d1e 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -33,7 +33,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): resolve_done_signal = pyqtSignal(object) finalize_done_signal = pyqtSignal(object) - round_3_signal = pyqtSignal(object) + notify_merchant_done_signal = pyqtSignal(object) def __init__(self, window: 'ElectrumWindow'): QWidget.__init__(self, window) @@ -182,8 +182,8 @@ def reset_max(text): run_hook('create_send_tab', grid) self.resolve_done_signal.connect(self.on_resolve_done) - self.finalize_done_signal.connect(self.on_round_2) - self.round_3_signal.connect(self.on_round_3) + self.finalize_done_signal.connect(self.on_finalize_done) + self.notify_merchant_done_signal.connect(self.on_notify_merchant_done) def do_paste(self): text = self.window.app.clipboard().text() @@ -438,7 +438,7 @@ def get_amount(self) -> int: # must not be None return self.amount_e.get_amount() or 0 - def on_round_2(self, pi): + def on_finalize_done(self, pi): self.do_clear() if pi.error: self.show_error(pi.error) @@ -449,9 +449,6 @@ def on_round_2(self, pi): self.pending_invoice = invoice self.do_pay_invoice(invoice) - def on_round_3(self): - pass - def do_pay_or_get_invoice(self): pi = self.payment_identifier if pi.need_finalize(): @@ -634,7 +631,11 @@ def broadcast_thread(): txid = tx.txid() if self.payment_identifier.need_merchant_notify(): refund_address = self.wallet.get_receiving_address() - self.payment_identifier.notify_merchant(tx=tx, refund_address=refund_address) + self.payment_identifier.notify_merchant( + tx=tx, + refund_address=refund_address, + on_finished=self.notify_merchant_done_signal.emit + ) return True, txid # Capture current TL window; override might be removed on return @@ -658,6 +659,14 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.window.on_error) + def on_notify_merchant_done(self, pi): + if pi.is_error(): + self.logger.debug(f'merchant notify error: {pi.get_error()}') + else: + self.logger.debug(f'merchant notify result: {pi.merchant_ack_status}: {pi.merchant_ack_message}') + # TODO: show user? if we broadcasted the tx succesfully, do we care? + # BitPay complains with a NAK if tx is RbF + def toggle_paytomany(self): self.payto_e.toggle_paytomany() if self.payto_e.is_paytomany(): diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 7137f9397..a87873c65 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -256,6 +256,9 @@ def is_lightning(self): def is_multiline(self): return bool(self.multiline_outputs) + def is_error(self) -> bool: + return self._state >= PaymentIdentifierState.ERROR + def get_error(self) -> str: return self.error @@ -267,6 +270,10 @@ def parse(self, text): if outputs := self._parse_as_multiline(text): self._type = 'multiline' self.multiline_outputs = outputs + if self.error: + self.set_state(PaymentIdentifierState.INVALID) + else: + self.set_state(PaymentIdentifierState.AVAILABLE) elif invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text): if invoice_or_lnurl.startswith('lnurl'): self._type = 'lnurl' @@ -457,15 +464,15 @@ def _parse_as_multiline(self, text): for i, line in enumerate(lines): try: output = self.parse_address_and_amount(line) + outputs.append(output) + if parse_max_spend(output.value): + is_max = True + else: + total += output.value except Exception as e: errors.append(PayToLineError( idx=i, line_content=line.strip(), exc=e, is_multiline=True)) continue - outputs.append(output) - if parse_max_spend(output.value): - is_max = True - else: - total += output.value if is_multiline and errors: self.error = str(errors) if errors else None self.logger.debug(f'multiline: {outputs!r}, {self.error}') @@ -477,6 +484,8 @@ def parse_address_and_amount(self, line) -> 'PartialTxOutput': except ValueError: raise Exception("expected two comma-separated values: (address, amount)") from None scriptpubkey = self.parse_output(x) + if not scriptpubkey: + raise Exception('Invalid address') amount = self.parse_amount(y) return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount) From bde066f9ce50b305f86671a777f8206808be5f83 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 20 Jun 2023 20:54:31 +0200 Subject: [PATCH 1010/1143] qt: refactor send_tab, paytoedit --- electrum/gui/qt/amountedit.py | 1 - electrum/gui/qt/paytoedit.py | 187 ++++++++++++++------------- electrum/gui/qt/send_tab.py | 226 ++++++++++++++++++++------------- electrum/gui/qt/util.py | 15 ++- electrum/payment_identifier.py | 156 ++++++++++++++--------- 5 files changed, 341 insertions(+), 244 deletions(-) diff --git a/electrum/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py index d4bc86c19..9bbb56784 100644 --- a/electrum/gui/qt/amountedit.py +++ b/electrum/gui/qt/amountedit.py @@ -13,7 +13,6 @@ FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT) from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC - _NOT_GIVEN = object() # sentinel value diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index ad106f5c0..660139d46 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -26,6 +26,8 @@ from functools import partial from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING +from PyQt5.QtCore import Qt +from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QFontMetrics, QFont from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout @@ -48,6 +50,10 @@ normal_style = "QPlainTextEdit { }" +class InvalidPaymentIdentifier(Exception): + pass + + class ResizingTextEdit(QTextEdit): def __init__(self): @@ -63,113 +69,139 @@ def __init__(self): self.verticalMargins += documentMargin * 2 self.heightMin = self.fontSpacing + self.verticalMargins self.heightMax = (self.fontSpacing * 10) + self.verticalMargins + self.single_line = True self.update_size() def update_size(self): docLineCount = self.document().lineCount() - docHeight = max(3, docLineCount) * self.fontSpacing + docHeight = max(1 if self.single_line else 3, docLineCount) * self.fontSpacing h = docHeight + self.verticalMargins h = min(max(h, self.heightMin), self.heightMax) self.setMinimumHeight(int(h)) self.setMaximumHeight(int(h)) - self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) + if self.single_line: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) + else: + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) + self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) +class PayToEdit(QObject, Logger, GenericInputHandler): -class PayToEdit(Logger, GenericInputHandler): + paymentIdentifierChanged = pyqtSignal() def __init__(self, send_tab: 'SendTab'): + QObject.__init__(self, parent=send_tab) Logger.__init__(self) GenericInputHandler.__init__(self) - self.line_edit = QLineEdit() + self.text_edit = ResizingTextEdit() - self.text_edit.hide() + self.text_edit.textChanged.connect(self._on_text_edit_text_changed) self._is_paytomany = False - for w in [self.line_edit, self.text_edit]: - w.setFont(QFont(MONOSPACE_FONT)) - w.textChanged.connect(self._on_text_changed) + self.text_edit.setFont(QFont(MONOSPACE_FONT)) self.send_tab = send_tab self.config = send_tab.config - self.win = send_tab.window self.app = QApplication.instance() - self.amount_edit = self.send_tab.amount_e + self.logger.debug(util.ColorScheme.RED.as_stylesheet(True)) self.is_multiline = False - self.disable_checks = False - self.is_alias = False + # self.is_alias = False self.payto_scriptpubkey = None # type: Optional[bytes] self.previous_payto = '' # editor methods - self.setStyleSheet = self.editor.setStyleSheet - self.setText = self.editor.setText - self.setEnabled = self.editor.setEnabled - self.setReadOnly = self.editor.setReadOnly - self.setFocus = self.editor.setFocus + self.setStyleSheet = self.text_edit.setStyleSheet + self.setText = self.text_edit.setText + self.setFocus = self.text_edit.setFocus + self.setToolTip = self.text_edit.setToolTip # button handlers self.on_qr_from_camera_input_btn = partial( self.input_qr_from_camera, config=self.config, allow_multi=False, - show_error=self.win.show_error, - setText=self._on_input_btn, - parent=self.win, + show_error=self.send_tab.show_error, + setText=self.try_payment_identifier, + parent=self.send_tab.window, ) self.on_qr_from_screenshot_input_btn = partial( self.input_qr_from_screenshot, allow_multi=False, - show_error=self.win.show_error, - setText=self._on_input_btn, + show_error=self.send_tab.show_error, + setText=self.try_payment_identifier, ) self.on_input_file = partial( self.input_file, config=self.config, - show_error=self.win.show_error, - setText=self._on_input_btn, + show_error=self.send_tab.show_error, + setText=self.try_payment_identifier, ) - # - self.line_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.line_edit, self) + self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self) - @property - def editor(self): - return self.text_edit if self.is_paytomany() else self.line_edit + self.payment_identifier = None + + def set_text(self, text: str): + self.text_edit.setText(text) + + def update_editor(self): + if self.text_edit.toPlainText() != self.payment_identifier.text: + self.text_edit.setText(self.payment_identifier.text) + self.text_edit.single_line = not self.payment_identifier.is_multiline() + self.text_edit.update_size() + + '''set payment identifier only if valid, else exception''' + def try_payment_identifier(self, text): + text = text.strip() + pi = PaymentIdentifier(self.send_tab.wallet, text) + if not pi.is_valid(): + raise InvalidPaymentIdentifier('Invalid payment identifier') + self.set_payment_identifier(text) + + def set_payment_identifier(self, text): + text = text.strip() + if self.payment_identifier and self.payment_identifier.text == text: + # no change. + return + + self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text) + + # toggle to multiline if payment identifier is a multiline + self.is_multiline = self.payment_identifier.is_multiline() + self.logger.debug(f'is_multiline {self.is_multiline}') + if self.is_multiline and not self._is_paytomany: + self.set_paytomany(True) + + # if payment identifier gets set externally, we want to update the text_edit + # Note: this triggers the change handler, but we shortcut if it's the same payment identifier + self.update_editor() + + self.paymentIdentifierChanged.emit() def set_paytomany(self, b): - has_focus = self.editor.hasFocus() self._is_paytomany = b - self.line_edit.setVisible(not b) - self.text_edit.setVisible(b) + self.text_edit.single_line = not self._is_paytomany + self.text_edit.update_size() self.send_tab.paytomany_menu.setChecked(b) - if has_focus: - self.editor.setFocus() def toggle_paytomany(self): self.set_paytomany(not self._is_paytomany) - def toPlainText(self): - return self.text_edit.toPlainText() if self.is_paytomany() else self.line_edit.text() - def is_paytomany(self): return self._is_paytomany def setFrozen(self, b): - self.setReadOnly(b) + self.text_edit.setReadOnly(b) if not b: self.setStyleSheet(normal_style) - def setTextNoCheck(self, text: str): - """Sets the text, while also ensuring the new value will not be resolved/checked.""" - self.previous_payto = text - self.setText(text) + def isFrozen(self): + return self.text_edit.isReadOnly() def do_clear(self): self.is_multiline = False self.set_paytomany(False) - self.disable_checks = False - self.is_alias = False - self.line_edit.setText('') self.text_edit.setText('') - self.setFrozen(False) - self.setEnabled(True) + self.payment_identifier = None def setGreen(self): self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True)) @@ -177,53 +209,18 @@ def setGreen(self): def setExpired(self): self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) - def _on_input_btn(self, text: str): - self.setText(text) - - def _on_text_changed(self): - text = self.toPlainText() - # False if user pasted from clipboard - full_check = self.app.clipboard().text() != text - self._check_text(text, full_check=full_check) - if self.is_multiline and not self._is_paytomany: - self.set_paytomany(True) - self.text_edit.setText(text) - self.text_edit.setFocus() + def _on_text_edit_text_changed(self): + self._handle_text_change(self.text_edit.toPlainText()) - def _check_text(self, text, *, full_check: bool): - """ side effects: self.is_multiline """ - text = str(text).strip() - if not text: + def _handle_text_change(self, text): + if self.isFrozen(): + # if editor is frozen, we ignore text changes as they might not be a payment identifier + # but a user friendly representation. return - if self.previous_payto == text: - return - if full_check: - self.previous_payto = text - if self.disable_checks: - return - pi = PaymentIdentifier(self.send_tab.wallet, text) - self.is_multiline = bool(pi.multiline_outputs) # TODO: why both is_multiline and set_paytomany(True)?? - self.logger.debug(f'is_multiline {self.is_multiline}') - if pi.is_valid(): - self.send_tab.set_payment_identifier(text) - else: - if not full_check and pi.error: - self.send_tab.show_error( - _('Clipboard text is not a valid payment identifier') + '\n' + str(pi.error)) - return - - def handle_multiline(self, outputs): - total = 0 - is_max = False - for output in outputs: - if parse_max_spend(output.value): - is_max = True - else: - total += output.value - self.send_tab.set_onchain(True) - self.send_tab.max_button.setChecked(is_max) - if self.send_tab.max_button.isChecked(): - self.send_tab.spend_max() - else: - self.amount_edit.setAmount(total if outputs else None) - #self.send_tab.lock_amount(self.send_tab.max_button.isChecked() or bool(outputs)) + + self.set_payment_identifier(text) + if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text: + # user pasted from clipboard + self.logger.debug('from clipboard') + if self.payment_identifier.error: + self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 6dc677d1e..a55db6bc2 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -6,14 +6,13 @@ from decimal import Decimal from typing import Optional, TYPE_CHECKING, Sequence, List, Callable from PyQt5.QtCore import pyqtSignal, QPoint -from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, - QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton) +from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout, + QWidget, QToolTip, QPushButton, QApplication) from electrum.plugin import run_hook from electrum.i18n import _ from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend -from electrum.payment_identifier import PaymentIdentifier from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput @@ -21,8 +20,10 @@ from electrum.logging import Logger from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit -from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit -from .util import get_iconname_camera, get_iconname_qrcode, read_QIcon +from .paytoedit import InvalidPaymentIdentifier +from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, + char_width_in_lineedit, get_iconname_camera, get_iconname_qrcode, + read_QIcon) from .confirm_tx_dialog import ConfirmTxDialog if TYPE_CHECKING: @@ -38,7 +39,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): def __init__(self, window: 'ElectrumWindow'): QWidget.__init__(self, window) Logger.__init__(self) - + self.app = QApplication.instance() self.window = window self.wallet = window.wallet self.fx = window.fx @@ -49,7 +50,6 @@ def __init__(self, window: 'ElectrumWindow'): self.format_amount = window.format_amount self.base_unit = window.base_unit - self.payment_identifier = None self.pending_invoice = None # A 4-column grid layout. All the stretch is in the last column. @@ -73,7 +73,7 @@ def __init__(self, window: 'ElectrumWindow'): "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 0, 0) - grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4) + # grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4) grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4) #completer = QCompleter() @@ -119,11 +119,9 @@ def __init__(self, window: 'ElectrumWindow'): btn_width = 10 * char_width_in_lineedit() self.max_button.setFixedWidth(btn_width) self.max_button.setCheckable(True) + self.max_button.setEnabled(False) grid.addWidget(self.max_button, 3, 3) - self.save_button = EnterButton(_("Save"), self.do_save_invoice) - self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice) - self.clear_button = EnterButton(_("Clear"), self.do_clear) self.paste_button = QPushButton() self.paste_button.clicked.connect(self.do_paste) self.paste_button.setIcon(read_QIcon('copy.png')) @@ -131,9 +129,15 @@ def __init__(self, window: 'ElectrumWindow'): self.paste_button.setMaximumWidth(35) grid.addWidget(self.paste_button, 0, 5) + self.save_button = EnterButton(_("Save"), self.do_save_invoice) + self.save_button.setEnabled(False) + self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice) + self.send_button.setEnabled(False) + self.clear_button = EnterButton(_("Clear"), self.do_clear) + buttons = QHBoxLayout() buttons.addStretch(1) - #buttons.addWidget(self.paste_button) + buttons.addWidget(self.clear_button) buttons.addWidget(self.save_button) buttons.addWidget(self.send_button) @@ -143,14 +147,11 @@ def __init__(self, window: 'ElectrumWindow'): def reset_max(text): self.max_button.setChecked(False) - enable = not bool(text) and not self.amount_e.isReadOnly() - # self.max_button.setEnabled(enable) + self.amount_e.textChanged.connect(self.on_amount_changed) self.amount_e.textEdited.connect(reset_max) self.fiat_send_e.textEdited.connect(reset_max) - self.set_onchain(False) - self.invoices_label = QLabel(_('Invoices')) from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) @@ -184,30 +185,33 @@ def reset_max(text): self.resolve_done_signal.connect(self.on_resolve_done) self.finalize_done_signal.connect(self.on_finalize_done) self.notify_merchant_done_signal.connect(self.on_notify_merchant_done) + self.payto_e.paymentIdentifierChanged.connect(self._handle_payment_identifier) + + def on_amount_changed(self, text): + # FIXME: implement full valid amount check to enable/disable Pay button + pi_valid = self.payto_e.payment_identifier.is_valid() if self.payto_e.payment_identifier else False + self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid) + def do_paste(self): - text = self.window.app.clipboard().text() - if not text: - return - self.set_payment_identifier(text) + try: + self.payto_e.try_payment_identifier(self.app.clipboard().text()) + except InvalidPaymentIdentifier as e: + self.show_error(_('Invalid payment identifier on clipboard')) def set_payment_identifier(self, text): - self.payment_identifier = PaymentIdentifier(self.wallet, text) - if self.payment_identifier.error: - self.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) - return - if self.payment_identifier.is_multiline(): - self.payto_e.set_paytomany(True) - self.payto_e.text_edit.setText(text) - else: - self.payto_e.setTextNoCheck(text) - self._handle_payment_identifier(can_use_network=True) + self.logger.debug('set_payment_identifier') + try: + self.payto_e.try_payment_identifier(text) + except InvalidPaymentIdentifier as e: + self.show_error(_('Invalid payment identifier')) def spend_max(self): + assert self.payto_e.payment_identifier is not None + assert self.payto_e.payment_identifier.type in ['spk', 'multiline'] if run_hook('abort_send', self): return - amount = self.get_amount() - outputs = self.payment_identifier.get_onchain_outputs(amount) + outputs = self.payto_e.payment_identifier.get_onchain_outputs('!') if not outputs: return make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( @@ -296,9 +300,7 @@ def get_text_not_enough_funds_mentioning_frozen(self) -> str: text = _("Not enough funds") frozen_str = self.get_frozen_balance_str() if frozen_str: - text += " ({} {})".format( - frozen_str, _("are frozen") - ) + text += " ({} {})".format(frozen_str, _("are frozen")) return text def get_frozen_balance_str(self) -> Optional[str]: @@ -308,31 +310,26 @@ def get_frozen_balance_str(self) -> Optional[str]: return self.format_amount_and_units(frozen_bal) def do_clear(self): + self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False) self.max_button.setChecked(False) self.payto_e.do_clear() - self.set_onchain(False) for w in [self.comment_e, self.comment_label]: w.setVisible(False) for e in [self.message_e, self.amount_e, self.fiat_send_e]: e.setText('') self.set_field_style(e, None, False) - for e in [self.send_button, self.save_button, self.clear_button, self.amount_e, self.fiat_send_e]: - e.setEnabled(True) + for e in [self.save_button, self.send_button]: + e.setEnabled(False) self.window.update_status() run_hook('do_clear', self) - def set_onchain(self, b): - self._is_onchain = b - self.max_button.setEnabled(b) - def prepare_for_send_tab_network_lookup(self): self.window.show_send_tab() - self.payto_e.disable_checks = True #for e in [self.payto_e, self.message_e]: - self.payto_e.setFrozen(True) + # self.payto_e.setFrozen(True) for btn in [self.save_button, self.send_button, self.clear_button]: btn.setEnabled(False) - self.payto_e.setTextNoCheck(_("please wait...")) + # self.payto_e.setTextNoCheck(_("please wait...")) def payment_request_error(self, error): self.show_message(error) @@ -348,45 +345,90 @@ def set_field_style(self, w, text, validated): style = ColorScheme.RED.as_stylesheet(True) if text is not None: w.setStyleSheet(style) - w.setReadOnly(True) else: w.setStyleSheet('') - w.setReadOnly(False) + + def lock_fields(self, *, + lock_recipient: Optional[bool] = None, + lock_amount: Optional[bool] = None, + lock_max: Optional[bool] = None, + lock_description: Optional[bool] = None + ) -> None: + self.logger.debug(f'locking fields, r={lock_recipient}, a={lock_amount}, m={lock_max}, d={lock_description}') + if lock_recipient is not None: + self.payto_e.setFrozen(lock_recipient) + if lock_amount is not None: + self.amount_e.setFrozen(lock_amount) + if lock_max is not None: + self.max_button.setEnabled(not lock_max) + if lock_description is not None: + self.message_e.setFrozen(lock_description) def update_fields(self): - recipient, amount, description, comment, validated = self.payment_identifier.get_fields_for_GUI() - if recipient: - self.payto_e.setTextNoCheck(recipient) - elif self.payment_identifier.multiline_outputs: - self.payto_e.handle_multiline(self.payment_identifier.multiline_outputs) - if description: - self.message_e.setText(description) - if amount: - self.amount_e.setAmount(amount) - for w in [self.comment_e, self.comment_label]: - w.setVisible(not bool(comment)) - self.set_field_style(self.payto_e, recipient or self.payment_identifier.multiline_outputs, validated) - self.set_field_style(self.message_e, description, validated) - self.set_field_style(self.amount_e, amount, validated) - self.set_field_style(self.fiat_send_e, amount, validated) - - def _handle_payment_identifier(self, *, can_use_network: bool = True): - is_valid = self.payment_identifier.is_valid() - self.save_button.setEnabled(is_valid) - self.send_button.setEnabled(is_valid) - if not is_valid: + pi = self.payto_e.payment_identifier + + if pi.is_multiline(): + self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False) + self.set_field_style(self.payto_e, pi.multiline_outputs, False if not pi.is_valid() else None) + self.save_button.setEnabled(pi.is_valid()) + self.send_button.setEnabled(pi.is_valid()) + if pi.is_valid(): + self.handle_multiline(pi.multiline_outputs) + else: + # self.payto_e.setToolTip('\n'.join(list(map(lambda x: f'{x.idx}: {x.line_content}', pi.get_error())))) + self.payto_e.setToolTip(pi.get_error()) return + + if not pi.is_valid(): + self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False) + self.save_button.setEnabled(False) + self.send_button.setEnabled(False) + return + + lock_recipient = pi.type != 'spk' + self.lock_fields(lock_recipient=lock_recipient, + lock_amount=pi.is_amount_locked(), + lock_max=pi.is_amount_locked(), + lock_description=False) + if lock_recipient: + recipient, amount, description, comment, validated = pi.get_fields_for_GUI() + if recipient: + self.payto_e.setText(recipient) + if description: + self.message_e.setText(description) + self.lock_fields(lock_description=True) + if amount: + self.amount_e.setAmount(amount) + for w in [self.comment_e, self.comment_label]: + w.setVisible(bool(comment)) + self.set_field_style(self.payto_e, recipient or pi.multiline_outputs, validated) + self.set_field_style(self.message_e, description, validated) + self.set_field_style(self.amount_e, amount, validated) + self.set_field_style(self.fiat_send_e, amount, validated) + + self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired()) + self.save_button.setEnabled(True) + + def _handle_payment_identifier(self): + is_valid = self.payto_e.payment_identifier.is_valid() + self.logger.debug(f'handle PI, valid={is_valid}') + self.update_fields() - if self.payment_identifier.need_resolve(): + + if not is_valid: + self.logger.debug(f'PI error: {self.payto_e.payment_identifier.error}') + return + + if self.payto_e.payment_identifier.need_resolve(): self.prepare_for_send_tab_network_lookup() - self.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit) - # update fiat amount + self.payto_e.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit) + # update fiat amount (and reset max) self.amount_e.textEdited.emit("") self.window.show_send_tab() def on_resolve_done(self, pi): - if self.payment_identifier.error: - self.show_error(self.payment_identifier.error) + if self.payto_e.payment_identifier.error: + self.show_error(self.payto_e.payment_identifier.error) self.do_clear() return self.update_fields() @@ -404,10 +446,10 @@ def read_invoice(self) -> Optional[Invoice]: self.show_error(_('No amount')) return - invoice = self.payment_identifier.get_invoice(amount_sat, self.get_message()) + invoice = self.payto_e.payment_identifier.get_invoice(amount_sat, self.get_message()) #except Exception as e: if not invoice: - self.show_error('error getting invoice' + self.payment_identifier.error) + self.show_error('error getting invoice' + self.payto_e.payment_identifier.error) return if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): self.show_error(_('Lightning is disabled')) @@ -439,18 +481,17 @@ def get_amount(self) -> int: return self.amount_e.get_amount() or 0 def on_finalize_done(self, pi): - self.do_clear() if pi.error: self.show_error(pi.error) - self.do_clear() return - self.update_fields(pi) + self.update_fields() invoice = pi.get_invoice(self.get_amount(), self.get_message()) self.pending_invoice = invoice + self.logger.debug(f'after finalize invoice: {invoice!r}') self.do_pay_invoice(invoice) def do_pay_or_get_invoice(self): - pi = self.payment_identifier + pi = self.payto_e.payment_identifier if pi.need_finalize(): self.prepare_for_send_tab_network_lookup() pi.finalize(amount_sat=self.get_amount(), comment=self.message_e.text(), @@ -511,9 +552,9 @@ def check_payto_line_and_show_errors(self) -> bool: """Returns whether there are errors. Also shows error dialog to user if so. """ - error = self.payment_identifier.get_error() + error = self.payto_e.payment_identifier.get_error() if error: - if not self.payment_identifier.is_multiline(): + if not self.payto_e.payment_identifier.is_multiline(): err = error self.show_warning( _("Failed to parse 'Pay to' line") + ":\n" + @@ -527,13 +568,13 @@ def check_payto_line_and_show_errors(self) -> bool: # for err in errors])) return True - warning = self.payment_identifier.warning + warning = self.payto_e.payment_identifier.warning if warning: warning += '\n' + _('Do you wish to continue?') if not self.question(warning): return True - if self.payment_identifier.has_expired(): + if self.payto_e.payment_identifier.has_expired(): self.show_error(_('Payment request has expired')) return True @@ -619,7 +660,7 @@ def broadcast_transaction(self, tx: Transaction): def broadcast_thread(): # non-GUI thread - if self.payment_identifier.has_expired(): + if self.payto_e.payment_identifier.has_expired(): return False, _("Invoice has expired") try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) @@ -629,9 +670,9 @@ def broadcast_thread(): return False, repr(e) # success txid = tx.txid() - if self.payment_identifier.need_merchant_notify(): + if self.payto_e.payment_identifier.need_merchant_notify(): refund_address = self.wallet.get_receiving_address() - self.payment_identifier.notify_merchant( + self.payto_e.payment_identifier.notify_merchant( tx=tx, refund_address=refund_address, on_finished=self.notify_merchant_done_signal.emit @@ -683,10 +724,23 @@ def payto_contacts(self, labels): self.window.show_send_tab() self.payto_e.do_clear() if len(paytos) == 1: + self.logger.debug('payto_e setText 1') self.payto_e.setText(paytos[0]) self.amount_e.setFocus() else: self.payto_e.setFocus() text = "\n".join([payto + ", 0" for payto in paytos]) + self.logger.debug('payto_e setText n') self.payto_e.setText(text) self.payto_e.setFocus() + + def handle_multiline(self, outputs): + total = 0 + for output in outputs: + if parse_max_spend(output.value): + self.max_button.setChecked(True) # TODO: remove and let spend_max set this? + self.spend_max() + return + else: + total += output.value + self.amount_e.setAmount(total if outputs else None) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 1d5f5c2b9..50bdacaa0 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -562,7 +562,10 @@ def cb(success: bool, error: str, data): new_text = self.text() + data + '\n' else: new_text = data - setText(new_text) + try: + setText(new_text) + except Exception as e: + show_error(_('Invalid payment identifier in QR') + ':\n' + repr(e)) from .qrreader import scan_qrcode if parent is None: @@ -599,7 +602,10 @@ def input_qr_from_screenshot( new_text = self.text() + data + '\n' else: new_text = data - setText(new_text) + try: + setText(new_text) + except Exception as e: + show_error(_('Invalid payment identifier in QR') + ':\n' + repr(e)) def input_file( self, @@ -628,7 +634,10 @@ def input_file( except BaseException as e: show_error(_('Error opening file') + ':\n' + repr(e)) else: - setText(data) + try: + setText(data) + except Exception as e: + show_error(_('Invalid payment identifier in file') + ':\n' + repr(e)) def input_paste_from_clipboard( self, diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index a87873c65..3423397cb 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -1,4 +1,5 @@ import asyncio +import time import urllib import re from decimal import Decimal, InvalidOperation @@ -163,13 +164,6 @@ def is_uri(data: str) -> bool: return False -class PayToLineError(NamedTuple): - line_content: str - exc: Exception - idx: int = 0 # index of line - is_multiline: bool = False - - RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' @@ -210,12 +204,13 @@ def __init__(self, wallet: 'Abstract_Wallet', text): self.wallet = wallet self.contacts = wallet.contacts if wallet is not None else None self.config = wallet.config if wallet is not None else None - self.text = text + self.text = text.strip() self._type = None self.error = None # if set, GUI should show error and stop self.warning = None # if set, GUI should ask user if they want to proceed # more than one of those may be set self.multiline_outputs = None + self._is_max = False self.bolt11 = None self.bip21 = None self.spk = None @@ -234,8 +229,12 @@ def __init__(self, wallet: 'Abstract_Wallet', text): self.logger.debug(f'PI parsing...') self.parse(text) + @property + def type(self): + return self._type + def set_state(self, state: 'PaymentIdentifierState'): - self.logger.debug(f'PI state -> {state}') + self.logger.debug(f'PI state {self._state} -> {state}') self._state = state def need_resolve(self): @@ -256,6 +255,31 @@ def is_lightning(self): def is_multiline(self): return bool(self.multiline_outputs) + def is_multiline_max(self): + return self.is_multiline() and self._is_max + + def is_amount_locked(self): + if self._type == 'spk': + return False + elif self._type == 'bip21': + return bool(self.bip21.get('amount')) + elif self._type == 'bip70': + return True # TODO always given? + elif self._type == 'bolt11': + lnaddr = lndecode(self.bolt11) + return bool(lnaddr.amount) + elif self._type == 'lnurl': + # amount limits known after resolve, might be specific amount or locked to range + if self.need_resolve(): + self.logger.debug(f'lnurl r') + return True + if self.need_finalize(): + self.logger.debug(f'lnurl f {self.lnurl_data.min_sendable_sat}-{self.lnurl_data.max_sendable_sat}') + return not (self.lnurl_data.min_sendable_sat < self.lnurl_data.max_sendable_sat) + return True + elif self._type == 'multiline': + return True + def is_error(self) -> bool: return self._state >= PaymentIdentifierState.ERROR @@ -281,11 +305,21 @@ def parse(self, text): self.lnurl = decode_lnurl(invoice_or_lnurl) self.set_state(PaymentIdentifierState.NEED_RESOLVE) except Exception as e: - self.error = "Error parsing Lightning invoice" + f":\n{e}" + self.error = _("Error parsing LNURL") + f":\n{e}" self.set_state(PaymentIdentifierState.INVALID) return else: self._type = 'bolt11' + try: + lndecode(invoice_or_lnurl) + except LnInvoiceException as e: + self.error = _("Error parsing Lightning invoice") + f":\n{e}" + self.set_state(PaymentIdentifierState.INVALID) + return + except IncompatibleOrInsaneFeatures as e: + self.error = _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}" + self.set_state(PaymentIdentifierState.INVALID) + return self.bolt11 = invoice_or_lnurl self.set_state(PaymentIdentifierState.AVAILABLE) elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): @@ -295,19 +329,31 @@ def parse(self, text): self.error = _("Error parsing URI") + f":\n{e}" self.set_state(PaymentIdentifierState.INVALID) return - self._type = 'bip21' self.bip21 = out self.bip70 = out.get('r') if self.bip70: + self._type = 'bip70' self.set_state(PaymentIdentifierState.NEED_RESOLVE) else: + self._type = 'bip21' + # check optional lightning in bip21, set self.bolt11 if valid + bolt11 = out.get('lightning') + if bolt11: + try: + lndecode(bolt11) + # if we get here, we have a usable bolt11 + self.bolt11 = bolt11 + except LnInvoiceException as e: + self.logger.debug(_("Error parsing Lightning invoice") + f":\n{e}") + except IncompatibleOrInsaneFeatures as e: + self.logger.debug(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") self.set_state(PaymentIdentifierState.AVAILABLE) elif scriptpubkey := self.parse_output(text): self._type = 'spk' self.spk = scriptpubkey self.set_state(PaymentIdentifierState.AVAILABLE) elif re.match(RE_EMAIL, text): - self._type = 'alias' + self._type = 'emaillike' self.emaillike = text self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif self.error is None: @@ -324,9 +370,10 @@ def resolve(self, *, on_finished: 'Callable'): async def _do_resolve(self, *, on_finished=None): try: if self.emaillike: + # TODO: parallel lookup? data = await self.resolve_openalias() if data: - self.openalias_data = data # needed? + self.openalias_data = data self.logger.debug(f'OA: {data!r}') name = data.get('name') address = data.get('address') @@ -335,8 +382,14 @@ async def _do_resolve(self, *, on_finished=None): self.warning = _( 'WARNING: the alias "{}" could not be validated via an additional ' 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) - # this will set self.spk and update state - self.parse(address) + try: + scriptpubkey = self.parse_output(address) + self._type = 'openalias' + self.spk = scriptpubkey + self.set_state(PaymentIdentifierState.AVAILABLE) + except Exception as e: + self.error = str(e) + self.set_state(PaymentIdentifierState.NOT_FOUND) else: lnurl = lightning_address_to_url(self.emaillike) try: @@ -356,6 +409,7 @@ async def _do_resolve(self, *, on_finished=None): data = await request_lnurl(self.lnurl) self.lnurl_data = data self.set_state(PaymentIdentifierState.LNURLP_FINALIZE) + self.logger.debug(f'LNURL data: {data!r}') else: self.set_state(PaymentIdentifierState.ERROR) return @@ -458,23 +512,22 @@ def _parse_as_multiline(self, text): lines = [i for i in lines if i] is_multiline = len(lines) > 1 outputs = [] # type: List[PartialTxOutput] - errors = [] + errors = '' total = 0 - is_max = False + self._is_max = False for i, line in enumerate(lines): try: output = self.parse_address_and_amount(line) outputs.append(output) if parse_max_spend(output.value): - is_max = True + self._is_max = True else: total += output.value except Exception as e: - errors.append(PayToLineError( - idx=i, line_content=line.strip(), exc=e, is_multiline=True)) + errors = f'{errors}line #{i}: {str(e)}\n' continue if is_multiline and errors: - self.error = str(errors) if errors else None + self.error = errors.strip() if errors else None self.logger.debug(f'multiline: {outputs!r}, {self.error}') return outputs @@ -494,15 +547,14 @@ def parse_output(self, x) -> bytes: address = self.parse_address(x) return bytes.fromhex(bitcoin.address_to_script(address)) except Exception as e: - error = PayToLineError(idx=0, line_content=x, exc=e, is_multiline=False) + pass try: script = self.parse_script(x) return bytes.fromhex(script) except Exception as e: - #error = PayToLineError(idx=0, line_content=x, exc=e, is_multiline=False) pass - #raise Exception("Invalid address or script.") - #self.errors.append(error) + + raise Exception("Invalid address or script.") def parse_script(self, x): script = '' @@ -535,12 +587,11 @@ def parse_address(self, line): return address def get_fields_for_GUI(self): - """ sets self.error as side effect""" recipient = None amount = None description = None validated = None - comment = "no comment" + comment = None if self.emaillike and self.openalias_data: address = self.openalias_data.get('address') @@ -550,21 +601,17 @@ def get_fields_for_GUI(self): if not validated: self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) - #self.payto_e.set_openalias(key=pi.openalias, data=oa_data) - #self.window.contact_list.update() - elif self.bolt11: - recipient, amount, description = self.get_bolt11_fields(self.bolt11) + elif self.bolt11 and self.wallet.has_lightning(): + recipient, amount, description = self._get_bolt11_fields(self.bolt11) elif self.lnurl and self.lnurl_data: domain = urllib.parse.urlparse(self.lnurl).netloc - #recipient = "invoice from lnurl" recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>" - #amount = self.lnurl_data.min_sendable_sat - amount = None - description = None + amount = self.lnurl_data.min_sendable_sat if self.lnurl_data.min_sendable_sat else None + description = self.lnurl_data.metadata_plaintext if self.lnurl_data.comment_allowed: - comment = None + comment = self.lnurl_data.comment_allowed elif self.bip70 and self.bip70_data: pr = self.bip70_data @@ -575,8 +622,6 @@ def get_fields_for_GUI(self): amount = pr.get_amount() description = pr.get_memo() validated = not pr.has_expired() - #self.set_onchain(True) - #self.max_button.setEnabled(False) # note: allow saving bip70 reqs, as we save them anyway when paying them #for btn in [self.send_button, self.clear_button, self.save_button]: # btn.setEnabled(True) @@ -584,8 +629,7 @@ def get_fields_for_GUI(self): #self.amount_e.textEdited.emit("") elif self.spk: - recipient = self.text - amount = None + pass elif self.multiline_outputs: pass @@ -598,26 +642,12 @@ def get_fields_for_GUI(self): # use label as description (not BIP21 compliant) if label and not description: description = label - lightning = self.bip21.get('lightning') - if lightning and self.wallet.has_lightning(): - # maybe set self.bolt11? - recipient, amount, description = self.get_bolt11_fields(lightning) - if not amount: - amount_required = True - # todo: merge logic return recipient, amount, description, comment, validated - def get_bolt11_fields(self, bolt11_invoice): + def _get_bolt11_fields(self, bolt11_invoice): """Parse ln invoice, and prepare the send tab for it.""" - try: - lnaddr = lndecode(bolt11_invoice) - except LnInvoiceException as e: - self.show_error(_("Error parsing Lightning invoice") + f":\n{e}") - return - except IncompatibleOrInsaneFeatures as e: - self.show_error(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") - return + lnaddr = lndecode(bolt11_invoice) # pubkey = lnaddr.pubkey.serialize().hex() for k, v in lnaddr.tags: if k == 'd': @@ -628,15 +658,17 @@ def get_bolt11_fields(self, bolt11_invoice): amount = lnaddr.get_amount_sat() return pubkey, amount, description + # TODO: rename to resolve_emaillike to disambiguate async def resolve_openalias(self) -> Optional[dict]: key = self.emaillike - if not (('.' in key) and ('<' not in key) and (' ' not in key)): - return None + # TODO: below check needed? we already matched RE_EMAIL + # if not (('.' in key) and ('<' not in key) and (' ' not in key)): + # return None parts = key.split(sep=',') # assuming single line if parts and len(parts) > 0 and bitcoin.is_address(parts[0]): return None try: - data = self.contacts.resolve(key) + data = self.contacts.resolve(key) # TODO: don't use contacts as delegate to resolve openalias, separate. return data except AliasNotFoundException as e: self.logger.info(f'OpenAlias not found: {repr(e)}') @@ -648,6 +680,12 @@ async def resolve_openalias(self) -> Optional[dict]: def has_expired(self): if self.bip70: return self.bip70_data.has_expired() + elif self.bolt11: + lnaddr = lndecode(self.bolt11) + return lnaddr.is_expired() + elif self.bip21: + expires = self.bip21.get('exp') + self.bip21.get('time') if self.bip21.get('exp') else 0 + return bool(expires) and expires < time.time() return False def get_invoice(self, amount_sat, message): From 915f66c0b8124426d71d5b580a44b2a16ac954c6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 22 Jun 2023 14:36:33 +0200 Subject: [PATCH 1011/1143] payment_identifier: fix emaillike qt: validate on pushback timer, buttons enable/disable, cleanup --- electrum/gui/qt/paytoedit.py | 41 ++++++++++++++++---------- electrum/gui/qt/send_tab.py | 54 ++++++++++++++++------------------ electrum/payment_identifier.py | 22 +++++++++----- 3 files changed, 66 insertions(+), 51 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 660139d46..ac876407c 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -26,13 +26,12 @@ from functools import partial from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QFontMetrics, QFont -from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QTextEdit, QVBoxLayout from electrum.i18n import _ -from electrum.util import parse_max_spend from electrum.payment_identifier import PaymentIdentifier from electrum.logging import Logger @@ -42,7 +41,6 @@ from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent if TYPE_CHECKING: - from .main_window import ElectrumWindow from .send_tab import SendTab @@ -97,7 +95,7 @@ def __init__(self, send_tab: 'SendTab'): GenericInputHandler.__init__(self) self.text_edit = ResizingTextEdit() - self.text_edit.textChanged.connect(self._on_text_edit_text_changed) + self.text_edit.textChanged.connect(self._handle_text_change) self._is_paytomany = False self.text_edit.setFont(QFont(MONOSPACE_FONT)) self.send_tab = send_tab @@ -138,6 +136,11 @@ def __init__(self, send_tab: 'SendTab'): self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self) + self.edit_timer = QTimer(self) + self.edit_timer.setSingleShot(True) + self.edit_timer.setInterval(1000) + self.edit_timer.timeout.connect(self._on_edit_timer) + self.payment_identifier = None def set_text(self, text: str): @@ -167,7 +170,6 @@ def set_payment_identifier(self, text): # toggle to multiline if payment identifier is a multiline self.is_multiline = self.payment_identifier.is_multiline() - self.logger.debug(f'is_multiline {self.is_multiline}') if self.is_multiline and not self._is_paytomany: self.set_paytomany(True) @@ -209,18 +211,25 @@ def setGreen(self): def setExpired(self): self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) - def _on_text_edit_text_changed(self): - self._handle_text_change(self.text_edit.toPlainText()) - - def _handle_text_change(self, text): + def _handle_text_change(self): if self.isFrozen(): # if editor is frozen, we ignore text changes as they might not be a payment identifier # but a user friendly representation. return - self.set_payment_identifier(text) - if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text: - # user pasted from clipboard - self.logger.debug('from clipboard') - if self.payment_identifier.error: - self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) + # pushback timer if timer active or PI needs resolving + pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText()) + if pi.need_resolve() or self.edit_timer.isActive(): + self.edit_timer.start() + else: + self.set_payment_identifier(self.text_edit.toPlainText()) + + # self.set_payment_identifier(text) + # if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text: + # # user pasted from clipboard + # self.logger.debug('from clipboard') + # if self.payment_identifier.error: + # self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) + + def _on_edit_timer(self): + self.set_payment_identifier(self.text_edit.toPlainText()) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index a55db6bc2..9c8b4bc49 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -2,22 +2,21 @@ # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php -import asyncio from decimal import Decimal from typing import Optional, TYPE_CHECKING, Sequence, List, Callable from PyQt5.QtCore import pyqtSignal, QPoint from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout, QWidget, QToolTip, QPushButton, QApplication) -from electrum.plugin import run_hook from electrum.i18n import _ +from electrum.logging import Logger +from electrum.plugin import run_hook from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST - from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.logging import Logger +from electrum.payment_identifier import PaymentIdentifierState from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .paytoedit import InvalidPaymentIdentifier @@ -73,7 +72,6 @@ def __init__(self, window: 'ElectrumWindow'): "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 0, 0) - # grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4) grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4) #completer = QCompleter() @@ -190,8 +188,8 @@ def reset_max(text): def on_amount_changed(self, text): # FIXME: implement full valid amount check to enable/disable Pay button pi_valid = self.payto_e.payment_identifier.is_valid() if self.payto_e.payment_identifier else False - self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid) - + pi_error = self.payto_e.payment_identifier.is_error() if pi_valid else False + self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid and not pi_error) def do_paste(self): try: @@ -324,7 +322,7 @@ def do_clear(self): run_hook('do_clear', self) def prepare_for_send_tab_network_lookup(self): - self.window.show_send_tab() + self.window.show_send_tab() # FIXME why is this here #for e in [self.payto_e, self.message_e]: # self.payto_e.setFrozen(True) for btn in [self.save_button, self.send_button, self.clear_button]: @@ -367,16 +365,16 @@ def lock_fields(self, *, def update_fields(self): pi = self.payto_e.payment_identifier + self.clear_button.setEnabled(True) + if pi.is_multiline(): self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False) - self.set_field_style(self.payto_e, pi.multiline_outputs, False if not pi.is_valid() else None) + self.set_field_style(self.payto_e, True if not pi.is_valid() else None, False) self.save_button.setEnabled(pi.is_valid()) self.send_button.setEnabled(pi.is_valid()) + self.payto_e.setToolTip(pi.get_error() if not pi.is_valid() else '') if pi.is_valid(): self.handle_multiline(pi.multiline_outputs) - else: - # self.payto_e.setToolTip('\n'.join(list(map(lambda x: f'{x.idx}: {x.line_content}', pi.get_error())))) - self.payto_e.setToolTip(pi.get_error()) return if not pi.is_valid(): @@ -385,10 +383,13 @@ def update_fields(self): self.send_button.setEnabled(False) return - lock_recipient = pi.type != 'spk' + lock_recipient = pi.type != 'spk' \ + and not (pi.type == 'emaillike' and pi.is_state(PaymentIdentifierState.NOT_FOUND)) + lock_max = pi.is_amount_locked() \ + or pi.type in ['bolt11', 'lnurl', 'lightningaddress'] self.lock_fields(lock_recipient=lock_recipient, lock_amount=pi.is_amount_locked(), - lock_max=pi.is_amount_locked(), + lock_max=lock_max, lock_description=False) if lock_recipient: recipient, amount, description, comment, validated = pi.get_fields_for_GUI() @@ -406,16 +407,13 @@ def update_fields(self): self.set_field_style(self.amount_e, amount, validated) self.set_field_style(self.fiat_send_e, amount, validated) - self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired()) - self.save_button.setEnabled(True) + self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) + self.save_button.setEnabled(not pi.is_error()) def _handle_payment_identifier(self): - is_valid = self.payto_e.payment_identifier.is_valid() - self.logger.debug(f'handle PI, valid={is_valid}') - self.update_fields() - if not is_valid: + if not self.payto_e.payment_identifier.is_valid(): self.logger.debug(f'PI error: {self.payto_e.payment_identifier.error}') return @@ -424,16 +422,18 @@ def _handle_payment_identifier(self): self.payto_e.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit) # update fiat amount (and reset max) self.amount_e.textEdited.emit("") - self.window.show_send_tab() + self.window.show_send_tab() # FIXME: why is this here? def on_resolve_done(self, pi): - if self.payto_e.payment_identifier.error: - self.show_error(self.payto_e.payment_identifier.error) + # TODO: resolve can happen while typing, we don't want message dialogs to pop up + # currently we don't set error for emaillike recipients to avoid just that + if pi.error: + self.show_error(pi.error) self.do_clear() return self.update_fields() - for btn in [self.send_button, self.clear_button, self.save_button]: - btn.setEnabled(True) + # for btn in [self.send_button, self.clear_button, self.save_button]: + # btn.setEnabled(True) def get_message(self): return self.message_e.text() @@ -447,7 +447,6 @@ def read_invoice(self) -> Optional[Invoice]: return invoice = self.payto_e.payment_identifier.get_invoice(amount_sat, self.get_message()) - #except Exception as e: if not invoice: self.show_error('error getting invoice' + self.payto_e.payment_identifier.error) return @@ -526,8 +525,7 @@ def do_pay_invoice(self, invoice: 'Invoice'): self.pay_onchain_dialog(invoice.outputs) def read_amount(self) -> List[PartialTxOutput]: - is_max = self.max_button.isChecked() - amount = '!' if is_max else self.get_amount() + amount = '!' if self.max_button.isChecked() else self.get_amount() return amount def check_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool: diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 3423397cb..118669e07 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -225,8 +225,7 @@ def __init__(self, wallet: 'Abstract_Wallet', text): # self.lnurl = None self.lnurl_data = None - # parse without network - self.logger.debug(f'PI parsing...') + self.parse(text) @property @@ -237,6 +236,9 @@ def set_state(self, state: 'PaymentIdentifierState'): self.logger.debug(f'PI state {self._state} -> {state}') self._state = state + def is_state(self, state: 'PaymentIdentifierState'): + return self._state == state + def need_resolve(self): return self._state == PaymentIdentifierState.NEED_RESOLVE @@ -268,10 +270,9 @@ def is_amount_locked(self): elif self._type == 'bolt11': lnaddr = lndecode(self.bolt11) return bool(lnaddr.amount) - elif self._type == 'lnurl': + elif self._type == 'lnurl' or self._type == 'lightningaddress': # amount limits known after resolve, might be specific amount or locked to range if self.need_resolve(): - self.logger.debug(f'lnurl r') return True if self.need_finalize(): self.logger.debug(f'lnurl f {self.lnurl_data.min_sendable_sat}-{self.lnurl_data.max_sendable_sat}') @@ -279,6 +280,10 @@ def is_amount_locked(self): return True elif self._type == 'multiline': return True + elif self._type == 'emaillike': + return False + elif self._type == 'openalias': + return False def is_error(self) -> bool: return self._state >= PaymentIdentifierState.ERROR @@ -394,11 +399,15 @@ async def _do_resolve(self, *, on_finished=None): lnurl = lightning_address_to_url(self.emaillike) try: data = await request_lnurl(lnurl) + self._type = 'lightningaddress' self.lnurl = lnurl self.lnurl_data = data self.set_state(PaymentIdentifierState.LNURLP_FINALIZE) except LNURLError as e: - self.error = str(e) + self.set_state(PaymentIdentifierState.NOT_FOUND) + except Exception as e: + # NOTE: any other exception is swallowed here (e.g. DNS error) + # as the user may be typing and we have an incomplete emaillike self.set_state(PaymentIdentifierState.NOT_FOUND) elif self.bip70: from . import paymentrequest @@ -554,7 +563,7 @@ def parse_output(self, x) -> bytes: except Exception as e: pass - raise Exception("Invalid address or script.") + # raise Exception("Invalid address or script.") def parse_script(self, x): script = '' @@ -658,7 +667,6 @@ def _get_bolt11_fields(self, bolt11_invoice): amount = lnaddr.get_amount_sat() return pubkey, amount, description - # TODO: rename to resolve_emaillike to disambiguate async def resolve_openalias(self) -> Optional[dict]: key = self.emaillike # TODO: below check needed? we already matched RE_EMAIL From fc141c01826fce955e91cef930e3e0178fb39bc6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 22 Jun 2023 20:20:31 +0200 Subject: [PATCH 1012/1143] payment_identfier: refactor qml and tests --- electrum/gui/qml/qeinvoice.py | 195 +++++++++++++-------------------- electrum/payment_identifier.py | 20 ++-- electrum/tests/test_util.py | 24 ++-- 3 files changed, 98 insertions(+), 141 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index a6f2a395c..fcf475865 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -5,21 +5,19 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS, QTimer -from electrum import bitcoin -from electrum import lnutil from electrum.i18n import _ -from electrum.invoices import Invoice -from electrum.invoices import (PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, +from electrum.logging import get_logger +from electrum.invoices import (Invoice, PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER) from electrum.lnaddr import LnInvoiceException -from electrum.logging import get_logger from electrum.transaction import PartialTxOutput -from electrum.util import (parse_URI, InvalidBitcoinURI, InvoiceError, - maybe_extract_lightning_payment_identifier, get_asyncio_loop) -from electrum.lnutil import format_short_channel_id +from electrum.util import InvoiceError, get_asyncio_loop +from electrum.lnutil import format_short_channel_id, IncompatibleOrInsaneFeatures from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl from electrum.bitcoin import COIN from electrum.paymentrequest import PaymentRequest +from electrum.payment_identifier import (parse_bip21_URI, InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, + PaymentIdentifier, PaymentIdentifierState) from .qetypes import QEAmount from .qewallet import QEWallet @@ -249,7 +247,8 @@ def set_lnprops(self): } def name_for_node_id(self, node_id): - return self._wallet.wallet.lnworker.get_node_alias(node_id) or node_id.hex() + lnworker = self._wallet.wallet.lnworker + return (lnworker.get_node_alias(node_id) if lnworker else None) or node_id.hex() def set_effective_invoice(self, invoice: Invoice): self._effectiveInvoice = invoice @@ -406,13 +405,11 @@ class QEInvoiceParser(QEInvoice): lnurlRetrieved = pyqtSignal() lnurlError = pyqtSignal([str,str], arguments=['code', 'message']) - _bip70PrResolvedSignal = pyqtSignal([PaymentRequest], arguments=['pr']) - def __init__(self, parent=None): super().__init__(parent) self._recipient = '' - self._bip70PrResolvedSignal.connect(self._bip70_payment_request_resolved) + self._pi = None self.clear() @@ -493,79 +490,54 @@ def validateRecipient(self, recipient): self.setInvoiceType(QEInvoice.Type.Invalid) return - maybe_lightning_invoice = recipient + self._pi = PaymentIdentifier(self._wallet.wallet, recipient) + if not self._pi.is_valid() or self._pi.type not in ['spk', 'bip21', 'bip70', 'bolt11', 'lnurl']: + self.validationError.emit('unknown', _('Unknown invoice')) + return - try: - bip21 = parse_URI(recipient, lambda pr: self._bip70PrResolvedSignal.emit(pr)) - if bip21: - if 'r' in bip21 or ('name' in bip21 and 'sig' in bip21): # TODO set flag in util? - # let callback handle state - return - if ':' not in recipient: - # address only - # create bare invoice - outputs = [PartialTxOutput.from_address_and_value(bip21['address'], 0)] - invoice = self.create_onchain_invoice(outputs, None, None, None) - self._logger.debug(repr(invoice)) - self.setValidOnchainInvoice(invoice) - self.validationSuccess.emit() - return - else: - # fallback lightning invoice? - if 'lightning' in bip21: - maybe_lightning_invoice = bip21['lightning'] - except InvalidBitcoinURI as e: - bip21 = None - - lninvoice = None - maybe_lightning_invoice = maybe_extract_lightning_payment_identifier(maybe_lightning_invoice) - if maybe_lightning_invoice is not None: - if maybe_lightning_invoice.startswith('lnurl'): - self.resolve_lnurl(maybe_lightning_invoice) - return - try: - lninvoice = Invoice.from_bech32(maybe_lightning_invoice) - except InvoiceError as e: - e2 = e.__cause__ - if isinstance(e2, LnInvoiceException): - self.validationError.emit('unknown', _("Error parsing Lightning invoice") + f":\n{e2}") - self.clear() - return - if isinstance(e2, lnutil.IncompatibleOrInsaneFeatures): - self.validationError.emit('unknown', _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e2!r}") - self.clear() - return - self._logger.exception(repr(e)) + self._update_from_payment_identifier() - if not lninvoice and not bip21: - self.validationError.emit('unknown',_('Unknown invoice')) - self.clear() + def _update_from_payment_identifier(self): + if self._pi.need_resolve(): + self.resolve_pi() return - if lninvoice: - if not self._wallet.wallet.has_lightning(): - if not bip21: - if lninvoice.get_address(): - self.setValidLightningInvoice(lninvoice) - self.validationSuccess.emit() - else: - self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.')) - else: - self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') - self._validateRecipient_bip21_onchain(bip21) - else: - if not self._wallet.wallet.lnworker.channels: - if bip21 and 'address' in bip21: - self._logger.debug('flow where invoice has both LN and onchain, we have LN enabled but no channels') - self._validateRecipient_bip21_onchain(bip21) - else: - self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) - else: + if self._pi.type == 'lnurl': + self.on_lnurl(self._pi.lnurl_data) + return + + if self._pi.type == 'bip70': + self._bip70_payment_request_resolved(self._pi.bip70_data) + return + + if self._pi.is_available(): + if self._pi.type == 'spk': + outputs = [PartialTxOutput(scriptpubkey=self._pi.spk, value=0)] + invoice = self.create_onchain_invoice(outputs, None, None, None) + self._logger.debug(repr(invoice)) + self.setValidOnchainInvoice(invoice) + self.validationSuccess.emit() + return + elif self._pi.type == 'bolt11': + lninvoice = Invoice.from_bech32(self._pi.bolt11) + if not self._wallet.wallet.has_lightning() and not lninvoice.get_address(): + self.validationError.emit('no_lightning', + _('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.')) + return + if self._wallet.wallet.lnworker and not self._wallet.wallet.lnworker.channels: + self.validationWarning.emit('no_channels', + _('Detected valid Lightning invoice, but there are no open channels')) + + self.setValidLightningInvoice(lninvoice) + self.validationSuccess.emit() + elif self._pi.type == 'bip21': + if self._wallet.wallet.has_lightning() and self._wallet.wallet.lnworker.channels and self._pi.bolt11: + lninvoice = Invoice.from_bech32(self._pi.bolt11) self.setValidLightningInvoice(lninvoice) self.validationSuccess.emit() - else: - self._logger.debug('flow without LN but having bip21 uri') - self._validateRecipient_bip21_onchain(bip21) + else: + self._validateRecipient_bip21_onchain(self._pi.bip21) + def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None: if 'amount' not in bip21: @@ -580,20 +552,15 @@ def _validateRecipient_bip21_onchain(self, bip21: Dict[str, Any]) -> None: self.setValidOnchainInvoice(invoice) self.validationSuccess.emit() - def resolve_lnurl(self, lnurl): - self._logger.debug('resolve_lnurl') - url = decode_lnurl(lnurl) - self._logger.debug(f'{repr(url)}') - - def resolve_task(): - try: - coro = request_lnurl(url) - fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) - self.on_lnurl(fut.result()) - except Exception as e: - self.validationError.emit('lnurl', repr(e)) + def resolve_pi(self): + assert self._pi.need_resolve() + def on_finished(pi): + if pi.is_error(): + pass + else: + self._update_from_payment_identifier() - threading.Thread(target=resolve_task, daemon=True).start() + self._pi.resolve(on_finished=on_finished) def on_lnurl(self, lnurldata): self._logger.debug('on_lnurl') @@ -610,49 +577,39 @@ def on_lnurl(self, lnurldata): self.setValidLNURLPayRequest() self.lnurlRetrieved.emit() - @pyqtSlot('quint64') - @pyqtSlot('quint64', str) - def lnurlGetInvoice(self, amount, comment=None): + @pyqtSlot() + @pyqtSlot(str) + def lnurlGetInvoice(self, comment=None): assert self._lnurlData + assert self._pi.need_finalize() self._logger.debug(f'{repr(self._lnurlData)}') amount = self.amountOverride.satsInt - if self.lnurlData['min_sendable_sat'] != 0: - try: - assert amount >= self.lnurlData['min_sendable_sat'] - assert amount <= self.lnurlData['max_sendable_sat'] - except Exception: - self.lnurlError.emit('amount', _('Amount out of bounds')) - return if self._lnurlData['comment_allowed'] == 0: comment = None - self._logger.debug(f'fetching callback url {self._lnurlData["callback_url"]}') - def fetch_invoice_task(): - try: - params = { 'amount': amount * 1000 } - if comment: - params['comment'] = comment - coro = callback_lnurl(self._lnurlData['callback_url'], params) - fut = asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) - self.on_lnurl_invoice(amount, fut.result()) - except Exception as e: - self._logger.error(repr(e)) - self.lnurlError.emit('lnurl', str(e)) - - threading.Thread(target=fetch_invoice_task, daemon=True).start() + def on_finished(pi): + if pi.is_error(): + if pi.is_state(PaymentIdentifierState.INVALID_AMOUNT): + self.lnurlError.emit('amount', pi.get_error()) + else: + self.lnurlError.emit('lnurl', pi.get_error()) + else: + self.on_lnurl_invoice(self.amountOverride.satsInt, pi.bolt11) + + self._pi.finalize(amount_sat=amount, comment=comment, on_finished=on_finished) def on_lnurl_invoice(self, orig_amount, invoice): self._logger.debug('on_lnurl_invoice') self._logger.debug(f'{repr(invoice)}') # assure no shenanigans with the bolt11 invoice we get back - lninvoice = Invoice.from_bech32(invoice['pr']) - if orig_amount * 1000 != lninvoice.amount_msat: + lninvoice = Invoice.from_bech32(invoice) + if orig_amount * 1000 != lninvoice.amount_msat: # TODO msat precision can cause trouble here raise Exception('Unexpected amount in invoice, differs from lnurl-pay specified amount') - self.recipient = invoice['pr'] + self.recipient = invoice @pyqtSlot() def saveInvoice(self): diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 118669e07..4b9733b66 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from .wallet import Abstract_Wallet + from .transaction import Transaction def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: @@ -32,10 +33,6 @@ def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: return data return None -# URL decode -#_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) -#urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) - # note: when checking against these, use .lower() to support case-insensitivity BITCOIN_BIP21_URI_SCHEME = 'bitcoin' @@ -183,6 +180,7 @@ class PaymentIdentifierState(IntEnum): ERROR = 50 # generic error NOT_FOUND = 51 # PI contains a recognized destination format, but resolve step was unsuccesful MERCHANT_ERROR = 52 # PI failed notifying the merchant after broadcasting onchain TX + INVALID_AMOUNT = 53 # Specified amount not accepted class PaymentIdentifier(Logger): @@ -251,6 +249,9 @@ def need_merchant_notify(self): def is_valid(self): return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY] + def is_available(self): + return self._state in [PaymentIdentifierState.AVAILABLE] + def is_lightning(self): return self.lnurl or self.bolt11 @@ -445,22 +446,23 @@ async def _do_finalize(self, amount_sat: int = None, comment: str = None, on_fin if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat): self.error = _('Amount must be between %d and %d sat.') \ % (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat) + self.set_state(PaymentIdentifierState.INVALID_AMOUNT) return + if self.lnurl_data.comment_allowed == 0: comment = None params = {'amount': amount_sat * 1000} if comment: params['comment'] = comment + try: - invoice_data = await callback_lnurl( - self.lnurl_data.callback_url, - params=params, - ) + invoice_data = await callback_lnurl(self.lnurl_data.callback_url, params=params) except LNURLError as e: self.error = f"LNURL request encountered error: {e}" + self.set_state(PaymentIdentifierState.ERROR) return + bolt11_invoice = invoice_data.get('pr') - # invoice = Invoice.from_bech32(bolt11_invoice) if invoice.get_amount_sat() != amount_sat: raise Exception("lnurl returned invoice with wrong amount") diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index 9013d6afd..6ff020710 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -2,12 +2,10 @@ from decimal import Decimal from electrum import util -from electrum.util import (format_satoshis, format_fee_satoshis, parse_URI, - is_hash256_str, chunks, is_ip_address, list_enabled_bits, - format_satoshis_plain, is_private_netaddress, is_hex_str, - is_integer, is_non_negative_integer, is_int_or_float, - is_non_negative_int_or_float, is_subpath, InvalidBitcoinURI) - +from electrum.util import (format_satoshis, format_fee_satoshis, is_hash256_str, chunks, is_ip_address, + list_enabled_bits, format_satoshis_plain, is_private_netaddress, is_hex_str, + is_integer, is_non_negative_integer, is_int_or_float, is_non_negative_int_or_float) +from electrum.payment_identifier import parse_bip21_URI, InvalidBitcoinURI from . import ElectrumTestCase, as_testnet @@ -102,7 +100,7 @@ def test_format_satoshis_plain_to_mbtc(self): self.assertEqual("0.01234", format_satoshis_plain(1234, decimal_point=5)) def _do_test_parse_URI(self, uri, expected): - result = parse_URI(uri) + result = parse_bip21_URI(uri) self.assertEqual(expected, result) def test_parse_URI_address(self): @@ -143,13 +141,13 @@ def test_parse_URI_no_address_request_url(self): {'r': 'http://domain.tld/page?h=2a8628fc2fbe'}) def test_parse_URI_invalid_address(self): - self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:invalidaddress') + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:invalidaddress') def test_parse_URI_invalid(self): - self.assertRaises(InvalidBitcoinURI, parse_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma') + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'notbitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma') def test_parse_URI_parameter_pollution(self): - self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0') + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma?amount=0.0003&label=test&amount=30.0') @as_testnet def test_parse_URI_lightning_consistency(self): @@ -174,11 +172,11 @@ def test_parse_URI_lightning_consistency(self): 'memo': 'test266', 'message': 'test266'}) # bip21 uri that includes "lightning" key. LN part has fallback address BUT it mismatches the top-level address - self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:tb1qvu0c9xme0ul3gzx4nzqdgxsu25acuk9wvsj2j2?amount=0.0007&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql') + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qvu0c9xme0ul3gzx4nzqdgxsu25acuk9wvsj2j2?amount=0.0007&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql') # bip21 uri that includes "lightning" key. top-level amount mismatches LN amount - self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql') + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdhkk8a597sn865rhap4h4jenjefdk7ssp5d9zjr96ezp89gsyenfse5f4jn9ls29p0awvp0zxlt6tpzn2m3j5qdqvw3jhxapjxcmqcqzynxq8zals8sq9q7sqqqqqqqqqqqqqqqqqqqqqqqqq9qsqfppqu5ua3szskclyd48wlfdwfd32j65phxy9vu8dmmk3u20u0e0yqw484xzn4hc3cux6kk2wenhw7zy0mseu9ntpk9l4fws2d46svzszrc6mqy535740ks9j22w67fw0x4dt8w2hhzspcqakql') # bip21 uri that includes "lightning" key with garbage unparseable value - self.assertRaises(InvalidBitcoinURI, parse_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdasdasdasdasd') + self.assertRaises(InvalidBitcoinURI, parse_bip21_URI, 'bitcoin:tb1qu5ua3szskclyd48wlfdwfd32j65phxy9yf7ytl?amount=0.0008&message=test266&lightning=lntb700u1p3kqy26pp5l7rj7w0u5sdsj24umzdlhdasdasdasdasd') def test_is_hash256_str(self): self.assertTrue(is_hash256_str('09a4c03e3bdf83bbe3955f907ee52da4fc12f4813d459bc75228b64ad08617c7')) From 74a1f38a8b79996d6e58c3428d844167122f213f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 10:01:10 +0200 Subject: [PATCH 1013/1143] payment identifier types as enum --- electrum/gui/qml/qeinvoice.py | 15 ++++++----- electrum/gui/qt/send_tab.py | 10 ++++---- electrum/payment_identifier.py | 47 +++++++++++++++++++++------------- electrum/x509.py | 3 +-- 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index fcf475865..fa92706aa 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -17,7 +17,7 @@ from electrum.bitcoin import COIN from electrum.paymentrequest import PaymentRequest from electrum.payment_identifier import (parse_bip21_URI, InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, - PaymentIdentifier, PaymentIdentifierState) + PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType) from .qetypes import QEAmount from .qewallet import QEWallet @@ -491,7 +491,8 @@ def validateRecipient(self, recipient): return self._pi = PaymentIdentifier(self._wallet.wallet, recipient) - if not self._pi.is_valid() or self._pi.type not in ['spk', 'bip21', 'bip70', 'bolt11', 'lnurl']: + if not self._pi.is_valid() or self._pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21, + PaymentIdentifierType.BIP70, PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP]: self.validationError.emit('unknown', _('Unknown invoice')) return @@ -502,23 +503,23 @@ def _update_from_payment_identifier(self): self.resolve_pi() return - if self._pi.type == 'lnurl': + if self._pi.type == PaymentIdentifierType.LNURLP: self.on_lnurl(self._pi.lnurl_data) return - if self._pi.type == 'bip70': + if self._pi.type == PaymentIdentifierType.BIP70: self._bip70_payment_request_resolved(self._pi.bip70_data) return if self._pi.is_available(): - if self._pi.type == 'spk': + if self._pi.type == PaymentIdentifierType.SPK: outputs = [PartialTxOutput(scriptpubkey=self._pi.spk, value=0)] invoice = self.create_onchain_invoice(outputs, None, None, None) self._logger.debug(repr(invoice)) self.setValidOnchainInvoice(invoice) self.validationSuccess.emit() return - elif self._pi.type == 'bolt11': + elif self._pi.type == PaymentIdentifierType.BOLT11: lninvoice = Invoice.from_bech32(self._pi.bolt11) if not self._wallet.wallet.has_lightning() and not lninvoice.get_address(): self.validationError.emit('no_lightning', @@ -530,7 +531,7 @@ def _update_from_payment_identifier(self): self.setValidLightningInvoice(lninvoice) self.validationSuccess.emit() - elif self._pi.type == 'bip21': + elif self._pi.type == PaymentIdentifierType.BIP21: if self._wallet.wallet.has_lightning() and self._wallet.wallet.lnworker.channels and self._pi.bolt11: lninvoice = Invoice.from_bech32(self._pi.bolt11) self.setValidLightningInvoice(lninvoice) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 9c8b4bc49..7ee6cfcf8 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -16,7 +16,7 @@ from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.payment_identifier import PaymentIdentifierState +from electrum.payment_identifier import PaymentIdentifierState, PaymentIdentifierType from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .paytoedit import InvalidPaymentIdentifier @@ -206,7 +206,7 @@ def set_payment_identifier(self, text): def spend_max(self): assert self.payto_e.payment_identifier is not None - assert self.payto_e.payment_identifier.type in ['spk', 'multiline'] + assert self.payto_e.payment_identifier.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE] if run_hook('abort_send', self): return outputs = self.payto_e.payment_identifier.get_onchain_outputs('!') @@ -383,10 +383,10 @@ def update_fields(self): self.send_button.setEnabled(False) return - lock_recipient = pi.type != 'spk' \ - and not (pi.type == 'emaillike' and pi.is_state(PaymentIdentifierState.NOT_FOUND)) + lock_recipient = pi.type != PaymentIdentifierType.SPK \ + and not (pi.type == PaymentIdentifierType.EMAILLIKE and pi.is_state(PaymentIdentifierState.NOT_FOUND)) lock_max = pi.is_amount_locked() \ - or pi.type in ['bolt11', 'lnurl', 'lightningaddress'] + or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] self.lock_fields(lock_recipient=lock_recipient, lock_amount=pi.is_amount_locked(), lock_max=lock_max, diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 4b9733b66..0f348809f 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -182,6 +182,17 @@ class PaymentIdentifierState(IntEnum): MERCHANT_ERROR = 52 # PI failed notifying the merchant after broadcasting onchain TX INVALID_AMOUNT = 53 # Specified amount not accepted +class PaymentIdentifierType(IntEnum): + UNKNOWN = 0 + SPK = 1 + BIP21 = 2 + BIP70 = 3 + MULTILINE = 4 + BOLT11 = 5 + LNURLP = 6 + EMAILLIKE = 7 + OPENALIAS = 8 + LNADDR = 9 class PaymentIdentifier(Logger): """ @@ -203,7 +214,7 @@ def __init__(self, wallet: 'Abstract_Wallet', text): self.contacts = wallet.contacts if wallet is not None else None self.config = wallet.config if wallet is not None else None self.text = text.strip() - self._type = None + self._type = PaymentIdentifierType.UNKNOWN self.error = None # if set, GUI should show error and stop self.warning = None # if set, GUI should ask user if they want to proceed # more than one of those may be set @@ -262,16 +273,16 @@ def is_multiline_max(self): return self.is_multiline() and self._is_max def is_amount_locked(self): - if self._type == 'spk': + if self._type == PaymentIdentifierType.SPK: return False - elif self._type == 'bip21': + elif self._type == PaymentIdentifierType.BIP21: return bool(self.bip21.get('amount')) - elif self._type == 'bip70': + elif self._type == PaymentIdentifierType.BIP70: return True # TODO always given? - elif self._type == 'bolt11': + elif self._type == PaymentIdentifierType.BOLT11: lnaddr = lndecode(self.bolt11) return bool(lnaddr.amount) - elif self._type == 'lnurl' or self._type == 'lightningaddress': + elif self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]: # amount limits known after resolve, might be specific amount or locked to range if self.need_resolve(): return True @@ -279,11 +290,11 @@ def is_amount_locked(self): self.logger.debug(f'lnurl f {self.lnurl_data.min_sendable_sat}-{self.lnurl_data.max_sendable_sat}') return not (self.lnurl_data.min_sendable_sat < self.lnurl_data.max_sendable_sat) return True - elif self._type == 'multiline': + elif self._type == PaymentIdentifierType.MULTILINE: return True - elif self._type == 'emaillike': + elif self._type == PaymentIdentifierType.EMAILLIKE: return False - elif self._type == 'openalias': + elif self._type == PaymentIdentifierType.OPENALIAS: return False def is_error(self) -> bool: @@ -298,7 +309,7 @@ def parse(self, text): if not text: return if outputs := self._parse_as_multiline(text): - self._type = 'multiline' + self._type = PaymentIdentifierType.MULTILINE self.multiline_outputs = outputs if self.error: self.set_state(PaymentIdentifierState.INVALID) @@ -306,7 +317,7 @@ def parse(self, text): self.set_state(PaymentIdentifierState.AVAILABLE) elif invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text): if invoice_or_lnurl.startswith('lnurl'): - self._type = 'lnurl' + self._type = PaymentIdentifierType.LNURLP try: self.lnurl = decode_lnurl(invoice_or_lnurl) self.set_state(PaymentIdentifierState.NEED_RESOLVE) @@ -315,7 +326,7 @@ def parse(self, text): self.set_state(PaymentIdentifierState.INVALID) return else: - self._type = 'bolt11' + self._type = PaymentIdentifierType.BOLT11 try: lndecode(invoice_or_lnurl) except LnInvoiceException as e: @@ -338,10 +349,10 @@ def parse(self, text): self.bip21 = out self.bip70 = out.get('r') if self.bip70: - self._type = 'bip70' + self._type = PaymentIdentifierType.BIP70 self.set_state(PaymentIdentifierState.NEED_RESOLVE) else: - self._type = 'bip21' + self._type = PaymentIdentifierType.BIP21 # check optional lightning in bip21, set self.bolt11 if valid bolt11 = out.get('lightning') if bolt11: @@ -355,11 +366,11 @@ def parse(self, text): self.logger.debug(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") self.set_state(PaymentIdentifierState.AVAILABLE) elif scriptpubkey := self.parse_output(text): - self._type = 'spk' + self._type = PaymentIdentifierType.SPK self.spk = scriptpubkey self.set_state(PaymentIdentifierState.AVAILABLE) elif re.match(RE_EMAIL, text): - self._type = 'emaillike' + self._type = PaymentIdentifierType.EMAILLIKE self.emaillike = text self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif self.error is None: @@ -390,7 +401,7 @@ async def _do_resolve(self, *, on_finished=None): 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) try: scriptpubkey = self.parse_output(address) - self._type = 'openalias' + self._type = PaymentIdentifierType.OPENALIAS self.spk = scriptpubkey self.set_state(PaymentIdentifierState.AVAILABLE) except Exception as e: @@ -400,7 +411,7 @@ async def _do_resolve(self, *, on_finished=None): lnurl = lightning_address_to_url(self.emaillike) try: data = await request_lnurl(lnurl) - self._type = 'lightningaddress' + self._type = PaymentIdentifierType.LNADDR self.lnurl = lnurl self.lnurl_data = data self.set_state(PaymentIdentifierState.LNURLP_FINALIZE) diff --git a/electrum/x509.py b/electrum/x509.py index f0da646f6..68cf92b94 100644 --- a/electrum/x509.py +++ b/electrum/x509.py @@ -308,8 +308,7 @@ def check_date(self): raise CertificateError('Certificate has not entered its valid date range. (%s)' % self.get_common_name()) if self.notAfter <= now: dt = datetime.utcfromtimestamp(time.mktime(self.notAfter)) - # for testnet - #raise CertificateError(f'Certificate ({self.get_common_name()}) has expired (at {dt} UTC).') + raise CertificateError(f'Certificate ({self.get_common_name()}) has expired (at {dt} UTC).') def getFingerprint(self): return hashlib.sha1(self.bytes).digest() From ca283a75d01ad3bcd7ad3deae6e4d7fc927cec15 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 10:18:49 +0200 Subject: [PATCH 1014/1143] qml: exclude non-address SPK from supported payment identifiers --- electrum/gui/qml/qeinvoice.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index fa92706aa..a7c437721 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -10,7 +10,7 @@ from electrum.invoices import (Invoice, PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER) from electrum.lnaddr import LnInvoiceException -from electrum.transaction import PartialTxOutput +from electrum.transaction import PartialTxOutput, TxOutput from electrum.util import InvoiceError, get_asyncio_loop from electrum.lnutil import format_short_channel_id, IncompatibleOrInsaneFeatures from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl @@ -496,6 +496,12 @@ def validateRecipient(self, recipient): self.validationError.emit('unknown', _('Unknown invoice')) return + if self._pi.type == PaymentIdentifierType.SPK: + txo = TxOutput(scriptpubkey=self._pi.spk, value=0) + if not txo.address: + self.validationError.emit('unknown', _('Unknown invoice')) + return + self._update_from_payment_identifier() def _update_from_payment_identifier(self): From eed016bd7e450ffa0500f461d3401b64198d2ca8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 12:58:10 +0200 Subject: [PATCH 1015/1143] qt: move setting frozen styling to edit components themselves, fix re-enabling Clear button after finalize --- electrum/gui/qml/qeinvoice.py | 2 +- electrum/gui/qt/amountedit.py | 5 +++- electrum/gui/qt/paytoedit.py | 17 ++++---------- electrum/gui/qt/send_tab.py | 42 ++++++++++++---------------------- electrum/payment_identifier.py | 5 ++-- 5 files changed, 26 insertions(+), 45 deletions(-) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index a7c437721..e9e3a51b0 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -598,7 +598,7 @@ def lnurlGetInvoice(self, comment=None): def on_finished(pi): if pi.is_error(): - if pi.is_state(PaymentIdentifierState.INVALID_AMOUNT): + if pi.state == PaymentIdentifierState.INVALID_AMOUNT: self.lnurlError.emit('amount', pi.get_error()) else: self.lnurlError.emit('lnurl', pi.get_error()) diff --git a/electrum/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py index 9bbb56784..be1d20f86 100644 --- a/electrum/gui/qt/amountedit.py +++ b/electrum/gui/qt/amountedit.py @@ -21,9 +21,11 @@ class FreezableLineEdit(QLineEdit): def setFrozen(self, b): self.setReadOnly(b) - self.setFrame(not b) + self.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') self.frozen.emit() + def isFrozen(self): + return self.isReadOnly() class SizedFreezableLineEdit(FreezableLineEdit): @@ -152,6 +154,7 @@ def setAmount(self, amount_sat): else: text = self._get_text_from_amount(amount_sat) self.setText(text) + self.setFrozen(self.isFrozen()) # re-apply styling, as it is nuked by setText (?) self.repaint() # macOS hack for #6269 diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index ac876407c..9d5bd2513 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -38,7 +38,7 @@ from .qrtextedit import ScanQRTextEdit from .completion_text_edit import CompletionTextEdit from . import util -from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent +from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent, ColorScheme if TYPE_CHECKING: from .send_tab import SendTab @@ -102,9 +102,7 @@ def __init__(self, send_tab: 'SendTab'): self.config = send_tab.config self.app = QApplication.instance() - self.logger.debug(util.ColorScheme.RED.as_stylesheet(True)) self.is_multiline = False - # self.is_alias = False self.payto_scriptpubkey = None # type: Optional[bytes] self.previous_payto = '' # editor methods @@ -193,8 +191,7 @@ def is_paytomany(self): def setFrozen(self, b): self.text_edit.setReadOnly(b) - if not b: - self.setStyleSheet(normal_style) + self.text_edit.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') def isFrozen(self): return self.text_edit.isReadOnly() @@ -224,12 +221,6 @@ def _handle_text_change(self): else: self.set_payment_identifier(self.text_edit.toPlainText()) - # self.set_payment_identifier(text) - # if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text: - # # user pasted from clipboard - # self.logger.debug('from clipboard') - # if self.payment_identifier.error: - # self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) - def _on_edit_timer(self): - self.set_payment_identifier(self.text_edit.toPlainText()) + if not self.isFrozen(): + self.set_payment_identifier(self.text_edit.toPlainText()) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 7ee6cfcf8..dd8eddc85 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -22,7 +22,7 @@ from .paytoedit import InvalidPaymentIdentifier from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit, get_iconname_camera, get_iconname_qrcode, - read_QIcon) + read_QIcon, ColorScheme) from .confirm_tx_dialog import ConfirmTxDialog if TYPE_CHECKING: @@ -192,6 +192,7 @@ def on_amount_changed(self, text): self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid and not pi_error) def do_paste(self): + self.logger.debug('do_paste') try: self.payto_e.try_payment_identifier(self.app.clipboard().text()) except InvalidPaymentIdentifier as e: @@ -308,6 +309,7 @@ def get_frozen_balance_str(self) -> Optional[str]: return self.format_amount_and_units(frozen_bal) def do_clear(self): + self.logger.debug('do_clear') self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False) self.max_button.setChecked(False) self.payto_e.do_clear() @@ -315,7 +317,6 @@ def do_clear(self): w.setVisible(False) for e in [self.message_e, self.amount_e, self.fiat_send_e]: e.setText('') - self.set_field_style(e, None, False) for e in [self.save_button, self.send_button]: e.setEnabled(False) self.window.update_status() @@ -333,18 +334,9 @@ def payment_request_error(self, error): self.show_message(error) self.do_clear() - def set_field_style(self, w, text, validated): - from .util import ColorScheme - if validated is None: - style = ColorScheme.LIGHTBLUE.as_stylesheet(True) - elif validated is True: - style = ColorScheme.GREEN.as_stylesheet(True) - else: - style = ColorScheme.RED.as_stylesheet(True) - if text is not None: - w.setStyleSheet(style) - else: - w.setStyleSheet('') + def set_field_validated(self, w, *, validated: Optional[bool] = None): + if validated is not None: + w.setStyleSheet(ColorScheme.GREEN.as_stylesheet(True) if validated else ColorScheme.RED.as_stylesheet(True)) def lock_fields(self, *, lock_recipient: Optional[bool] = None, @@ -363,6 +355,7 @@ def lock_fields(self, *, self.message_e.setFrozen(lock_description) def update_fields(self): + self.logger.debug('update_fields') pi = self.payto_e.payment_identifier self.clear_button.setEnabled(True) @@ -384,11 +377,11 @@ def update_fields(self): return lock_recipient = pi.type != PaymentIdentifierType.SPK \ - and not (pi.type == PaymentIdentifierType.EMAILLIKE and pi.is_state(PaymentIdentifierState.NOT_FOUND)) - lock_max = pi.is_amount_locked() \ - or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] + and not (pi.type == PaymentIdentifierType.EMAILLIKE and pi.state in [PaymentIdentifierState.NOT_FOUND,PaymentIdentifierState.NEED_RESOLVE]) + lock_amount = pi.is_amount_locked() + lock_max = lock_amount or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] self.lock_fields(lock_recipient=lock_recipient, - lock_amount=pi.is_amount_locked(), + lock_amount=lock_amount, lock_max=lock_max, lock_description=False) if lock_recipient: @@ -402,10 +395,7 @@ def update_fields(self): self.amount_e.setAmount(amount) for w in [self.comment_e, self.comment_label]: w.setVisible(bool(comment)) - self.set_field_style(self.payto_e, recipient or pi.multiline_outputs, validated) - self.set_field_style(self.message_e, description, validated) - self.set_field_style(self.amount_e, amount, validated) - self.set_field_style(self.fiat_send_e, amount, validated) + self.set_field_validated(self.payto_e, validated=validated) self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) self.save_button.setEnabled(not pi.is_error()) @@ -420,20 +410,16 @@ def _handle_payment_identifier(self): if self.payto_e.payment_identifier.need_resolve(): self.prepare_for_send_tab_network_lookup() self.payto_e.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit) - # update fiat amount (and reset max) - self.amount_e.textEdited.emit("") - self.window.show_send_tab() # FIXME: why is this here? def on_resolve_done(self, pi): # TODO: resolve can happen while typing, we don't want message dialogs to pop up # currently we don't set error for emaillike recipients to avoid just that + self.logger.debug('payment identifier resolve done') if pi.error: self.show_error(pi.error) self.do_clear() return self.update_fields() - # for btn in [self.send_button, self.clear_button, self.save_button]: - # btn.setEnabled(True) def get_message(self): return self.message_e.text() @@ -480,10 +466,10 @@ def get_amount(self) -> int: return self.amount_e.get_amount() or 0 def on_finalize_done(self, pi): + self.update_fields() if pi.error: self.show_error(pi.error) return - self.update_fields() invoice = pi.get_invoice(self.get_amount(), self.get_message()) self.pending_invoice = invoice self.logger.debug(f'after finalize invoice: {invoice!r}') diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 0f348809f..f183360b0 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -245,8 +245,9 @@ def set_state(self, state: 'PaymentIdentifierState'): self.logger.debug(f'PI state {self._state} -> {state}') self._state = state - def is_state(self, state: 'PaymentIdentifierState'): - return self._state == state + @property + def state(self): + return self._state def need_resolve(self): return self._state == PaymentIdentifierState.NEED_RESOLVE From 3a1e5244b886b61e8db77561dabc9a34c7650e15 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 13:25:09 +0200 Subject: [PATCH 1016/1143] qt: fix enable/disable max button for openalias and restrict openalias to address only --- electrum/gui/qt/send_tab.py | 8 ++++++-- electrum/payment_identifier.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index dd8eddc85..5709c0a2c 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -207,7 +207,8 @@ def set_payment_identifier(self, text): def spend_max(self): assert self.payto_e.payment_identifier is not None - assert self.payto_e.payment_identifier.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE] + assert self.payto_e.payment_identifier.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE, + PaymentIdentifierType.OPENALIAS] if run_hook('abort_send', self): return outputs = self.payto_e.payment_identifier.get_onchain_outputs('!') @@ -379,7 +380,10 @@ def update_fields(self): lock_recipient = pi.type != PaymentIdentifierType.SPK \ and not (pi.type == PaymentIdentifierType.EMAILLIKE and pi.state in [PaymentIdentifierState.NOT_FOUND,PaymentIdentifierState.NEED_RESOLVE]) lock_amount = pi.is_amount_locked() - lock_max = lock_amount or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] + lock_max = lock_amount \ + or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, + PaymentIdentifierType.LNADDR, PaymentIdentifierType.EMAILLIKE] + self.lock_fields(lock_recipient=lock_recipient, lock_amount=lock_amount, lock_max=lock_max, diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index f183360b0..837780815 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -401,7 +401,8 @@ async def _do_resolve(self, *, on_finished=None): 'WARNING: the alias "{}" could not be validated via an additional ' 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) try: - scriptpubkey = self.parse_output(address) + assert bitcoin.is_address(address) + scriptpubkey = bytes.fromhex(bitcoin.address_to_script(address)) self._type = PaymentIdentifierType.OPENALIAS self.spk = scriptpubkey self.set_state(PaymentIdentifierState.AVAILABLE) From 0cbf403f8bf460432fd44df66800c760c1290799 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 13:50:42 +0200 Subject: [PATCH 1017/1143] use NamedTuple for payment identifier gui fields --- electrum/gui/qt/send_tab.py | 18 +++++++++--------- electrum/payment_identifier.py | 20 +++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 5709c0a2c..71e93d832 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -389,17 +389,17 @@ def update_fields(self): lock_max=lock_max, lock_description=False) if lock_recipient: - recipient, amount, description, comment, validated = pi.get_fields_for_GUI() - if recipient: - self.payto_e.setText(recipient) - if description: - self.message_e.setText(description) + fields = pi.get_fields_for_GUI() + if fields.recipient: + self.payto_e.setText(fields.recipient) + if fields.description: + self.message_e.setText(fields.description) self.lock_fields(lock_description=True) - if amount: - self.amount_e.setAmount(amount) + if fields.amount: + self.amount_e.setAmount(fields.amount) for w in [self.comment_e, self.comment_label]: - w.setVisible(bool(comment)) - self.set_field_validated(self.payto_e, validated=validated) + w.setVisible(bool(fields.comment)) + self.set_field_validated(self.payto_e, validated=fields.validated) self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) self.save_button.setEnabled(not pi.is_error()) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 837780815..2dd35b2b2 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -182,6 +182,7 @@ class PaymentIdentifierState(IntEnum): MERCHANT_ERROR = 52 # PI failed notifying the merchant after broadcasting onchain TX INVALID_AMOUNT = 53 # Specified amount not accepted + class PaymentIdentifierType(IntEnum): UNKNOWN = 0 SPK = 1 @@ -194,6 +195,15 @@ class PaymentIdentifierType(IntEnum): OPENALIAS = 8 LNADDR = 9 + +class FieldsForGUI(NamedTuple): + recipient: Optional[str] + amount: Optional[int] + description: Optional[str] + validated: Optional[bool] + comment: Optional[int] + + class PaymentIdentifier(Logger): """ Takes: @@ -610,7 +620,7 @@ def parse_address(self, line): assert bitcoin.is_address(address) return address - def get_fields_for_GUI(self): + def get_fields_for_GUI(self) -> FieldsForGUI: recipient = None amount = None description = None @@ -646,11 +656,6 @@ def get_fields_for_GUI(self): amount = pr.get_amount() description = pr.get_memo() validated = not pr.has_expired() - # note: allow saving bip70 reqs, as we save them anyway when paying them - #for btn in [self.send_button, self.clear_button, self.save_button]: - # btn.setEnabled(True) - # signal to set fee - #self.amount_e.textEdited.emit("") elif self.spk: pass @@ -667,7 +672,8 @@ def get_fields_for_GUI(self): if label and not description: description = label - return recipient, amount, description, comment, validated + return FieldsForGUI(recipient=recipient, amount=amount, description=description, + comment=comment, validated=validated) def _get_bolt11_fields(self, bolt11_invoice): """Parse ln invoice, and prepare the send tab for it.""" From 5cc7948eeef926c07d198606f6b56a454365654c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 13:52:01 +0200 Subject: [PATCH 1018/1143] fix bip70 potentially not returning gui fields tuple --- electrum/payment_identifier.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 2dd35b2b2..878d85115 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -651,11 +651,11 @@ def get_fields_for_GUI(self) -> FieldsForGUI: pr = self.bip70_data if pr.error: self.error = pr.error - return - recipient = pr.get_requestor() - amount = pr.get_amount() - description = pr.get_memo() - validated = not pr.has_expired() + else: + recipient = pr.get_requestor() + amount = pr.get_amount() + description = pr.get_memo() + validated = not pr.has_expired() elif self.spk: pass From fbb37d6fae6c3db322142623096caf861dd191b3 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 15:16:32 +0200 Subject: [PATCH 1019/1143] payment_identifier: add DOMAINLIKE payment identifier type, support domainlike -> openalias --- electrum/gui/qt/paytoedit.py | 2 +- electrum/gui/qt/send_tab.py | 8 ++++--- electrum/payment_identifier.py | 40 ++++++++++++++++++++-------------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 9d5bd2513..418cc9cd6 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -216,7 +216,7 @@ def _handle_text_change(self): # pushback timer if timer active or PI needs resolving pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText()) - if pi.need_resolve() or self.edit_timer.isActive(): + if not pi.is_valid() or pi.need_resolve() or self.edit_timer.isActive(): self.edit_timer.start() else: self.set_payment_identifier(self.text_edit.toPlainText()) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 71e93d832..67ab35b71 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -363,7 +363,7 @@ def update_fields(self): if pi.is_multiline(): self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False) - self.set_field_style(self.payto_e, True if not pi.is_valid() else None, False) + self.set_field_validated(self.payto_e, validated=pi.is_valid()) # TODO: validated used differently here than openalias self.save_button.setEnabled(pi.is_valid()) self.send_button.setEnabled(pi.is_valid()) self.payto_e.setToolTip(pi.get_error() if not pi.is_valid() else '') @@ -378,11 +378,13 @@ def update_fields(self): return lock_recipient = pi.type != PaymentIdentifierType.SPK \ - and not (pi.type == PaymentIdentifierType.EMAILLIKE and pi.state in [PaymentIdentifierState.NOT_FOUND,PaymentIdentifierState.NEED_RESOLVE]) + and not (pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE] \ + and pi.state in [PaymentIdentifierState.NOT_FOUND, PaymentIdentifierState.NEED_RESOLVE]) lock_amount = pi.is_amount_locked() lock_max = lock_amount \ or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, - PaymentIdentifierType.LNADDR, PaymentIdentifierType.EMAILLIKE] + PaymentIdentifierType.LNADDR, PaymentIdentifierType.EMAILLIKE, + PaymentIdentifierType.DOMAINLIKE] self.lock_fields(lock_recipient=lock_recipient, lock_amount=lock_amount, diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 878d85115..157042182 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -162,7 +162,8 @@ def is_uri(data: str) -> bool: RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' -RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' +RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b' +RE_DOMAIN = r'\b([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b' class PaymentIdentifierState(IntEnum): @@ -194,6 +195,7 @@ class PaymentIdentifierType(IntEnum): EMAILLIKE = 7 OPENALIAS = 8 LNADDR = 9 + DOMAINLIKE = 10 class FieldsForGUI(NamedTuple): @@ -235,6 +237,7 @@ def __init__(self, wallet: 'Abstract_Wallet', text): self.spk = None # self.emaillike = None + self.domainlike = None self.openalias_data = None # self.bip70 = None @@ -284,9 +287,7 @@ def is_multiline_max(self): return self.is_multiline() and self._is_max def is_amount_locked(self): - if self._type == PaymentIdentifierType.SPK: - return False - elif self._type == PaymentIdentifierType.BIP21: + if self._type == PaymentIdentifierType.BIP21: return bool(self.bip21.get('amount')) elif self._type == PaymentIdentifierType.BIP70: return True # TODO always given? @@ -303,9 +304,7 @@ def is_amount_locked(self): return True elif self._type == PaymentIdentifierType.MULTILINE: return True - elif self._type == PaymentIdentifierType.EMAILLIKE: - return False - elif self._type == PaymentIdentifierType.OPENALIAS: + else: return False def is_error(self) -> bool: @@ -384,6 +383,10 @@ def parse(self, text): self._type = PaymentIdentifierType.EMAILLIKE self.emaillike = text self.set_state(PaymentIdentifierState.NEED_RESOLVE) + elif re.match(RE_DOMAIN, text): + self._type = PaymentIdentifierType.DOMAINLIKE + self.domainlike = text + self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif self.error is None: truncated_text = f"{text[:100]}..." if len(text) > 100 else text self.error = f"Unknown payment identifier:\n{truncated_text}" @@ -397,7 +400,7 @@ def resolve(self, *, on_finished: 'Callable'): @log_exceptions async def _do_resolve(self, *, on_finished=None): try: - if self.emaillike: + if self.emaillike or self.domainlike: # TODO: parallel lookup? data = await self.resolve_openalias() if data: @@ -405,11 +408,12 @@ async def _do_resolve(self, *, on_finished=None): self.logger.debug(f'OA: {data!r}') name = data.get('name') address = data.get('address') - self.contacts[self.emaillike] = ('openalias', name) + key = self.emaillike if self.emaillike else self.domainlike + self.contacts[key] = ('openalias', name) if not data.get('validated'): self.warning = _( 'WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) + 'security check, DNSSEC, and thus may not be correct.').format(key) try: assert bitcoin.is_address(address) scriptpubkey = bytes.fromhex(bitcoin.address_to_script(address)) @@ -419,7 +423,7 @@ async def _do_resolve(self, *, on_finished=None): except Exception as e: self.error = str(e) self.set_state(PaymentIdentifierState.NOT_FOUND) - else: + elif self.emaillike: lnurl = lightning_address_to_url(self.emaillike) try: data = await request_lnurl(lnurl) @@ -433,6 +437,8 @@ async def _do_resolve(self, *, on_finished=None): # NOTE: any other exception is swallowed here (e.g. DNS error) # as the user may be typing and we have an incomplete emaillike self.set_state(PaymentIdentifierState.NOT_FOUND) + else: + self.set_state(PaymentIdentifierState.NOT_FOUND) elif self.bip70: from . import paymentrequest data = await paymentrequest.get_payment_request(self.bip70) @@ -627,14 +633,16 @@ def get_fields_for_GUI(self) -> FieldsForGUI: validated = None comment = None - if self.emaillike and self.openalias_data: + if (self.emaillike or self.domainlike) and self.openalias_data: + key = self.emaillike if self.emaillike else self.domainlike address = self.openalias_data.get('address') name = self.openalias_data.get('name') - recipient = self.emaillike + ' <' + address + '>' + description = name + recipient = key + ' <' + address + '>' validated = self.openalias_data.get('validated') if not validated: self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' - 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) + 'security check, DNSSEC, and thus may not be correct.').format(key) elif self.bolt11 and self.wallet.has_lightning(): recipient, amount, description = self._get_bolt11_fields(self.bolt11) @@ -689,8 +697,8 @@ def _get_bolt11_fields(self, bolt11_invoice): return pubkey, amount, description async def resolve_openalias(self) -> Optional[dict]: - key = self.emaillike - # TODO: below check needed? we already matched RE_EMAIL + key = self.emaillike if self.emaillike else self.domainlike + # TODO: below check needed? we already matched RE_EMAIL/RE_DOMAIN # if not (('.' in key) and ('<' not in key) and (' ' not in key)): # return None parts = key.split(sep=',') # assuming single line From 6b57743c3ecc360d2a1ac78a4ae33407ddbc1ae6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 17:21:57 +0200 Subject: [PATCH 1020/1143] send_tab: add LNURLp range as tooltip on amount field --- electrum/gui/qt/send_tab.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 67ab35b71..f20b4d871 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -316,10 +316,11 @@ def do_clear(self): self.payto_e.do_clear() for w in [self.comment_e, self.comment_label]: w.setVisible(False) - for e in [self.message_e, self.amount_e, self.fiat_send_e]: - e.setText('') - for e in [self.save_button, self.send_button]: - e.setEnabled(False) + for w in [self.message_e, self.amount_e, self.fiat_send_e, self.comment_e]: + w.setText('') + w.setToolTip('') + for w in [self.save_button, self.send_button]: + w.setEnabled(False) self.window.update_status() run_hook('do_clear', self) @@ -401,8 +402,19 @@ def update_fields(self): self.amount_e.setAmount(fields.amount) for w in [self.comment_e, self.comment_label]: w.setVisible(bool(fields.comment)) + if fields.comment: + self.comment_e.setToolTip(_('Max comment length: %d characters') % fields.comment) self.set_field_validated(self.payto_e, validated=fields.validated) + # LNURLp amount range and comment tooltip + if pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] \ + and pi.state == PaymentIdentifierState.LNURLP_FINALIZE \ + and pi.lnurl_data.min_sendable_sat != pi.lnurl_data.max_sendable_sat: + self.amount_e.setToolTip(_('Amount must be between %d and %d sat.') \ + % (pi.lnurl_data.min_sendable_sat, pi.lnurl_data.max_sendable_sat)) + else: + self.amount_e.setToolTip('') + self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) self.save_button.setEnabled(not pi.is_error()) From 3df13b8ce46a6f7d2301fb7249b9826303d94fa0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 17:32:02 +0200 Subject: [PATCH 1021/1143] qt: disallow save of LNURLp/LnAddr --- electrum/gui/qt/send_tab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index f20b4d871..5cdf4592d 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -406,7 +406,7 @@ def update_fields(self): self.comment_e.setToolTip(_('Max comment length: %d characters') % fields.comment) self.set_field_validated(self.payto_e, validated=fields.validated) - # LNURLp amount range and comment tooltip + # LNURLp amount range if pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] \ and pi.state == PaymentIdentifierState.LNURLP_FINALIZE \ and pi.lnurl_data.min_sendable_sat != pi.lnurl_data.max_sendable_sat: @@ -416,7 +416,7 @@ def update_fields(self): self.amount_e.setToolTip('') self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) - self.save_button.setEnabled(not pi.is_error()) + self.save_button.setEnabled(not pi.is_error() and not pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]) def _handle_payment_identifier(self): self.update_fields() From febb2222d462bc7894312532aecd7baf33354a91 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 26 Jun 2023 17:40:15 +0200 Subject: [PATCH 1022/1143] send_tab: simplify lock_max --- electrum/gui/qt/send_tab.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 5cdf4592d..ed99e4984 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -382,10 +382,7 @@ def update_fields(self): and not (pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE] \ and pi.state in [PaymentIdentifierState.NOT_FOUND, PaymentIdentifierState.NEED_RESOLVE]) lock_amount = pi.is_amount_locked() - lock_max = lock_amount \ - or pi.type in [PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNURLP, - PaymentIdentifierType.LNADDR, PaymentIdentifierType.EMAILLIKE, - PaymentIdentifierType.DOMAINLIKE] + lock_max = lock_amount or pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21] self.lock_fields(lock_recipient=lock_recipient, lock_amount=lock_amount, @@ -416,7 +413,7 @@ def update_fields(self): self.amount_e.setToolTip('') self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) - self.save_button.setEnabled(not pi.is_error() and not pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]) + self.save_button.setEnabled(not pi.is_error() and pi.type not in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]) def _handle_payment_identifier(self): self.update_fields() From 49dab82efa2d785f49b5b83e800dfd6a33e3b680 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 27 Jun 2023 11:43:00 +0200 Subject: [PATCH 1023/1143] send_tab: add spinner for network lookup indication --- electrum/gui/icons/spinner.gif | Bin 0 -> 15209 bytes electrum/gui/qt/send_tab.py | 28 ++++++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 electrum/gui/icons/spinner.gif diff --git a/electrum/gui/icons/spinner.gif b/electrum/gui/icons/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..0dbdd7819b8f558635edb04edadd2b2fe6b37e77 GIT binary patch literal 15209 zcmcKBc|6qn|M&5kF_; zH#K{Cc)kDaJw1Z1y<2;BZnm$dZ+&AOFNe?1%O4sXa&mEc{pPi`rFC6>9i2wk*3_Pz zo^9`FU;n%wdO9>EIYo7cYWcPDiLnVgTf64l&ELL#1O1~P@o#Kq`qox@y5{;j6qK02 zz=w#hLE#9HJP7~&1Hb>7AeN1mR9`Y)=t5V%f47%rPJgj=daj)TlKLuYjl@~RQ=}@N z`S4MkD-I)Tb0VA;-~?8sgdVNaOxoc_rMd9HMf`lDPv8R$PKE>rpFYI|jt{$>PGXbr ziOGpdBOy**EGP=$la|P+aFyhTMAb5U^Ykth7ABoW35#88HI25m1?c5!>|bg&Qj8ckfjLS-&I056K2~Jn!Z#Zoh1H5GiBZ9wuF2^pV$H@V%%B zxrt=Y(4<2~YU>-%u72k6+5>kA&+3c0RAN~FR_wH$`vCrFy!Hy>_L)-M3Gsk|JUb&q z`J+}(j%^BM!MR7e64mI%M!H7RcSa%H9j{Gm^dShe%*tWGCz^vaQ(=>GO}J-&4k?me zv9}(D$X2;@dB1S?(uljL&RllqDbx_6t6wjMJel;+bsmQyP>WG2FAVJ*NL!sm|5{C;1v+1@HTN?VVP%C2nOV^yd!uEb(Hl>hsDcooyVECX ziz!$&9{N;Dy!_Os9ITa55}3v4s}22_0L~4`x@JntPiDT|=)f(}FDZ%QDPAtn<+;r0 zO#NykBQA4XuA1P?Z+3u$5UP@m%sleIvY%T{j;|=2IArwXN>pC$RqlXk7EG?i8qhULA+Zooy3VcSQfSImN8iGleLJ;M{qD=z7t}bp+^j%WVvDvnkm<4G zjTk?8wpJ40IJiUZ$zNWR* zD(?h(dra>`6mJt!xK+{TPJJSw@|g=B%6zLPO4f%hwvV|y&m=Up6)SBgXi8LlU3^>a z;rN*F;GyOu2v=E_Zcd;@Ft}G^yPv-^PI+9YPwqtd#`k$E&J z3KDvdFYtlh_Vv{N>4?vp?C{4k7HSNl2^`fM zLxHjcXi$-K@^52}+_&ErQ_Gs0t}dX>{m0D$Oz8J3zE;<_Uqxp&IIc>0huje|WYfI< zD7HkUd^px?y?C6+RrXHqPT5z~g~tgitxH!|H@YTcv_lI^PF+a7^Nl6$ELuXHj}7Z8 zH#XpY@QGvVaAQ#Nag^twC&|ODZ&)+d_DdXws_viXOV-4?y}|S0(qdC+H$0r=!g@R~ zM>K8dTprMHzYq*+Cpp$htu;`SDt^eE`vz)SBG^cYL1do;t0WbORZAqAIUR(4t8i2E zFSfMcOWcK>gPyfuh>M(VgsH>2G40DJ9^O24nMNeaE^a^8zV6lzrzuF~nK0gshYotO z&3nb&wId*-qcQApASR9O8R^j=mK?#p$XYT7TaJA^9qJ!fYcs_|AH7>{Zx4-CeXvbe zg3L@;LI^Ho*CsQFb%_)viKz!3FeRE^IYQ$AJ?ylSvWO}Xy(_wSCLX=g<8mJr<;=J* zuM%D^5vNG?TW}C$c4#(hi(zguLGRepN-_;ZCNX3SQ3C1@Bis-r3!gLd|MOEg& ztdvHBo3!uFH?W!4QdyhGRF?aZ+I5gP&QOGGksyYFc{GyTG0NhH?R zMpIM5rCj7{p1U&!)OYgGaaTDfPAD9<6^!W3jhm{2vC`IB?|!7#LQ&N*z(eHz*sQ)qhO%rB{*BLcJ2DsbmHHufZn}|+ zu|j51I+qa=E;x~EK2HuFe})3TRN4LIn-e!&)c0gKOMt<)h&bQV+?1qb12&v@{N-r< zZQ-0xt1eRfkSiq-8Kj`n%F|N$wWd;Xmn+K*V{cT7%jMOzk=Pn;HL^tA>?}Rm za<`6+tFOziT<_ZOm=9NLKSfZ^Z+xsLX7<(${OmIqXxhTqV`A3IeP0xG$w^A|!|Xeo z<S(MGTNx*ut+d-PU9KUSmrf`SUm@8KmH3v<0pgn&S>*AHyk14Ghp@xnU{ENY3fd@YlOs+6b&kC7?bheV#BW{OG*HD9ncQeJrLwf4G z^TIKlemYK@;F-gu}XGvJdqD4R!cs6O$$Pr3cX zE;eX83f2eaBQwdO zBIoBAr7Gu_h!XT^oQ_%3LCu)GV~(a%h2{hbPBBN!&2pQ+3U-PuRPJw2KCl|>jOweF zDM+#wbBMDPXV;KI30?_#mJSoEqQhe^eJng)kX~{%9W4pQ9^`r*O^4YTq%Y)vnlIDC zO)Cg_xl)uV>8o1)m)oIkrfv&vv6GSR!8e)P+cHcG)?`tvi9)Z63!{BWVaEDX&r2iY zF$G!YeUxZtvTTFE*Np8GWy{mA*|&QnnHl5Rq--eos~iI060+m&?R3b4m6(?-7#`}p zHA=m#0=^;9sJV2uX5~~;=@{CgwMkP!h6FJV5qgW{iZXudGn`2Lwh< z+)MFp#rqN}BV^f?-!NLs-ppORX_kJt3Vzm1*uM1qfBG2sr>Qg>rw9UeYMIH**jBHS1d)$QE(JqD5k!v(|tymqgJGHd> z*gjJJ!>H3S!PPxh{8_Em&@UGc4H}2ZCfn=mawc@x)n2#CEqW7p{Bc++PilPh*`q5`5%6&ALSt#?Xie(-3)jm1VtXPZ9)h0QA;2|!Lj5$hS_JwC$~t|wB0`{{;F<@=LaOc$*qS`&i%YiCq*-;!2#|2C%} zc>-FRdpCCbNA=dlQ1et_&D-Qu)^8U+JXF|Ig~s*mzj=|dYqjbP3G7|$Yy7lH$JTpr zWG(qFotfc$&I}@`(j#3}68zR`QJ1whMo9zX9(P{Y0h0xd-2SbiT{1L)$gDxzUQg%} zyQW9{+O<-}6;~|ve8JAQ!;k%;VC&gFia887n}kr#3-7BE7sK77So*cu zP01r7FJ3^`TSk3=NidCfGb4|o+GH<-{4FO^6w5z}q02FmR@UJa4?s1M`c>TC0}R;4 zeh`;xOf~BB#Wuv#shrg$LV}%=#L}Q!6f2Rs!#!Qx4BHPz(;Z@<7_eIuq4?fhqpKrj3$r_<}_J*^z4YF~Ge`|T4) zYSbgnUe!WrH^?RCe4k4%wOwK6==_*0l`F5D*rk?eI(MF5%ve(zf)G^F4tWGz5+&zd zoCLR=uW=6+otV)R$w=pavsdUq?gGj1MyaEd!By>-9V6PB17VhLzzFYj*-5=8Y>5Ys zGxL*6M)(pb=T5g3EVAoA;Z%uA&EVm`Fe=MC)Dn@oF~kvP0@_9wV&=9H2g_0JD>O4` zP&$$bwr?TxRzx(; z82g8aI`%f6Q83nrcCbSWicc8+Em69bFA(Ad@!pz_r#i4wy}hqX$Vxk7gm|$Hpp(a? z>lW>bdEQly7@lx@E3(^pgQ#+dQt$41bHYRVAZ7rEy4TKsO0 z4^Y_T8|V;qqszP~K5g|(oum!<=NKBl+(7Q}Z;W|pxRxJ3){iSbn7+rVq!Amf%uMRG z<9pCJXsv+$PTTw98B;5ZQ&4#9TKyJ|qY?e3L&K@fP z3D~ZOxO1nI*GE&5T=;TbjJ(d36p&HcPoz-%z&gmJ@5=v zKnLA9&*BYE3Pm>4x6iX)SnEIY>MOT&AxXg2@+@3tzf`en2N@bCX&U2!j&~%{3x#!J zJo44`l}H6zsL!t)v~DC>7HNxrUSz%*jl0ZDdh%%zSw8)K9&v!hd9f_7gJ&dCS$6HU z0x4a!x#dB@_Q|} z!nn1m$Qd6V+-(UEgWArs&&%(gz&>pXa$+NZQu&)$;&K<(B&PiaG(O-}Ed&`~*J{Y`UFBTI_ag0< zK%-sNkm9~j2rA`?RaDtKGtBCk;Bh-8Xs)d!u9y?UzO$RZH|~DkJB6v$q5TJsEQ3?z zRs(L}V5UJ2V$Wn2SUyViYWkHaVh78i|?ry4p-$#R}P3K zltQt1&Zdo4?5L4^V$LMI?HT_=&bGsDdA1*4E0&qH-{JSZA_M0Re){EgnzVA8-S)_Z zT`M?N2DI{khEE|vTK$6NGx;q4ynp~)+;%n8`tC5=<;U_v`q$im%8ogqDU zu^dK;u%@}jFYG|3qD-J9IT=cOnVf~2sQ5#PlY-~)lVQ1_b8nL z8zt)P5*y+HJ~9vYSS!(XL8RiasJtPPbb5`qJUpw5m(>6n0YceBbHroVrCGq+jHycD z_Krpm^dhzJ1`*PuQ`~Rb^smwFjVo+S98y}qGhcH#GwOQ4<~JH{x0_4uayo8e_QdG$ zHGhL99=N>g++NXZWJsxMyV5~Ml2aR%mJf3=Pc>3;6@p|W6I_&^SM|C0Jd#mMnlme2 zyKlx=5ys2T!n_pw+H`@3d`G`Tr(6fVPT2)sNI z#8d9Vf0UpL!B?dhpS}*){R2kDzk(6Su_Pi1Sc^bF-I|I(PX%lQWCJwYin7391cU@O zAiyI)=+;^U%mjo4tOJ7XpSmoNZ@1PWP;&txfxQVpx|L>u%?J$7f3O*W(wg=s7)x^P zjZwOb1KfRD^~9n2KG5|9&bOo8Vh8e>oL-aK@T{z*u2#vz>{D z^`YC}!|44T#te>MU<}XuHyDMP+avuj3rwz#;^b|p7`|D zdlN=D;-Q)a0ONH4MxozfY~O@Y;J=5F{dXAKH(`|d6-LGN$3rY^mTLfv-hTt5%y$@t zHen3^e+FajpJ6Ox0bmqh*@BVnAHyi7HJty0!}*w$H*6CdR@BPhYOY_5J|P77*SMP} z^#^^e-%R#;S)`3*x>nI7y{d$ZcJBpS@Y)-CTAX{5DX+_p)J@k3Z`6GIKv~|dct4T$ z**~cQuX*CSWJs^4xnx>q(f)lKH~Z^e_)nCgcHIOtvoA%I@3;9@YGJf)YHB@Hd`0P< z{;`|z*`iYi*87VLGe3JwT3z=F()ME#+G(*{Pvh}D@snSXsr$HJcxPt$Iu}jYiDt<> zt5&WC*+xaG?@*DiEaiFXf@qBIY>)ctISUOx3hfekx~9GnAW6*i*Um0$2Sc|Lx*T|7 zW(r9##7JNp7=(Lv23@`_z#A2&a#xJ$O9)RZ6cm`K1L{3?(qw91s83gD_xcVESst6j z_0Nl^a%>flT41dsx20nt!F6tl5O}xe!`mO;^zKzCRAp{BQgAN*lUYl?FN6zee|F>e zv2T#BENi=Yc8R;mFQ@wX9_!>Xa}hvLNpc&fxvyb-)V5`7i!0~77*H`15ek{2p^~f4 zqg9_TCtuQ%164q5JSO*;FH_NUT{udK03x(NgeQH^jK^X=p8UiD#`nYNvhAT6{#PV? zL6Q5|j|&8KHFsty>iCXg6JhFf#RT&hjkr9Kjoiz(xD6yDEQ`a0DlEY}5zk|N%aCvRt~txQOJSi>UwD?A9ju+wkEjwQt{>`S&9ac}se2(`8GYBw<07-h)j`N^ zm9*U~7qbdQT3!df=d3HY%A&VRe<8PkO|mh;*SU-E%o(>(Q-1n`RmLh$-HllT1B~vq zn5Y<)^l}zc0vSCEdQzqLIpe1CqIc-4m6=kz!~HG6VxvX3fT`G^Ov`t~wK5T66m|Q# z`GwWyKnF1Q&k2fic=q#{feAGBatfGBO|GCkk-}F#2V*y9T6 zAJ@uF7&lb}c-*{M{$m(7GwQ$DSYl630x#MCHimE7D1>xR^rug7cK)!j1PuRSBkvCz z#RCj5-)-dmDWhTl8)Y{$ssPik85RG#jhy&DY!v@H8}WZ_qby*f)OQ>0{%oTx;a3~C zGHTZMj0*nAM)IbO3;)1IvI&0buWjVT8E<7&4hNgEu>ao1OSJ!BW7?*TOaE?T^xxTN z1lXtx*jWAl#755FZ3I&2_t7Z(tBneqHop81Hp>2gv9aKPu(9Yr*vRysN8`^nUIT38 zrv2a9co=LUzGWll|Jufk|1UObglt$Vm_DKRtOQ;|AAToqt@>EOL8Ap+tH+%nYEyHr z@47zw1NKu)tc_wup(XDV*xSuo%+k=?2$ZeXKqZm7g?EN%Q6WDl&r@*E6Dfu56QUh$d=LAH^yA1Lj39?nI5uf_OE zB52CsG2ms&RD|Y=`VN^lXP!U8KsqA{%uo!)tQZ8}sJ{Yh%MIIi*~B&ld5iZmG7M%% zW!1C?35?}~aG%&MYCcAQXCHnC@#Ab~+vk%0NWnTRNQe-r)zT{Wq}C4i#HcD{~jb)Zel;O)x$A<-l9c5OMAw;@E zy$dBEoNAfSQcJjbIGUQvAFY4K#-578STh#Lx)96Ll^n$c(8#Qtnh1l{{X?(R?)xn; zYoh^T{uC2@>aInRIh9MP1tPs{P(H~>_^}s z4`AeGRRw6=e8sa_Re{2~RaJpkJb;dyRTY?xe_2%lAc2QGKv>b?KEclNqpC_#f3K>r?^P85@~5hb-Kwfm*dJAu z_g~}jm#WIXRaLiu{M)Ju`{m9k1>6~bR8{LgS5>L+RTccxo$-IFs{g}y-1%2`Mv@8c zZ^q-#RTThoI}`VRtg6n0@8gmEm+|Nf`8ObM0#%jw=kZwaKUP)Ot*ROgf2bz@*HxAG zpO44itE$wmK+3H@wg$#y2>_(?-;778pQ`Hq|8hLS{J$)}WIESDw`ZDE<(ZhQ%2isltk?;Vr99(N>)y|=YrC~rU)R>&F=nLfg3pH5 z41mh)6nhn3YPN&4x9e1Q+cPOqZwZN;_fy%8k7;Rp+nPQIq_)nA?g}N|h~Lxxj9G&G zAf!bC^V<2j=@AbotZ1BrvH^bhMs%o1J_ z-e0&!<7;IN@1rmJQO}k^oqObhDzy~Sld0tmceJ7@;GyxTS1}aoDDScoqSQ!znjy}o ztY=_=p)#|KR!HXbPX zE|zZ~f>@O!ZUPdswZmxE8QlxDrtXnMVmu5~v1pV{)~--#H%sKcC-skr`-Wb!EB85a zh|flAHRkT(_9FUjSvLE4Ijh_FsQ55C_%}*F!9f5$g8zi zTmftaR`t5Z;&e_!xkHl<7>!FuOQ+I`MJ!Dc988=bDkM_g)f6YAy|PE$&gi<&Ir=j3 z7VUq890Tr*EE;U@@KRahUZiIpZOPMkZvF)xSC|`wm)X1{>u&7)gtYv->Lx!~XNKML7O>*%p4-Zhyw(gphL>`Jy+dHSMzer;2AXyuH7e_9 zGUa{nQAbM`C5G_(n0*R~1A`vTfq8eZy5e8SR^fRPOuQ}b&DO?!hY|*hno&-7exuPc zj_{Pk2e(_M5opG#6_EmJU{+*HWQj2qibjGF=2b$PFbtCnfxx_TX1HEK9QHewe=4oO zngs5Tz?$5AlmpaHAeC;eNnj}gUIJ1AOajXiP;>JZ3Czu{cR8C=64-^Ck8*w|Gb3wj zO#)4J^Q^gfhy*4npeC?T0W3EUnwu9%pxFKcJO84z0&iN4lQv5$)c8kf6*PCRIvqE0 zq4NipWxAVdavmvG{`@FstF->xn*6=Ae)=!gBuiY+B@5tDjyXQBJBn&&g*joS-o|hv<=s z!W(fzQn?rlG#Cn6ekH1X(C1l`omjbguvAz~bM(6=Q|#~+5hce&>l;L}tdqoH)+(*z z*vjd?%%m3Qdhu2ZS&yAj6K(40nU9>r8aqt1KV0LL@@7KUB{jS@^FgF(WTgiO%@hYz zG*$^sgr^Q?1+=bWBn3G}9?jg__{e?AK@k4}a!u_ZeTUaEu7(HHuRVS~b@r1=^;5(g z(@rw$JvejzuB>xkmtJ(Rq?`q&0V_gTSYi7{;A0YKcxm3=aYKKY7P&+}ZWy8H^(n@F zUkYQJOYpD&Hu{jfUn>*7$;LF?I3F_|*Z%ULV*(KrJ+Wm?R1GeRNr-|}8mEBljY@9!3nksNHKgTsp`Vwz z8>pK*m7Nzm?2Yb0tF=g_8tfH;Z`?np)ei5SR%Q5(PsgM~Uv-Qr=-!lTDQE_zl)fnu zL5a6`&K2?#Z|w~f8e*E^q}nw9c9P;-L#m)~w6CAjRL(Umz=_b;YY0mn#44@*4(#@E zo=iCDftP0duw2D5R6`u8Q8)Y&q;-8xjdnlKWH=wBI@8m7g>c6gViE0jX0BKy-&5tL zmV4IwVVhqb^@PcSE#CyPu3yB&u2)D_8zkpT4x6hm2_*(Iz z)-lX`W3VQ*ZgH^XN`=qN0YTf7=1~p!V5`DPpJA3JiQbp5ZO`(qw+ktILf`UASH6U2 z+@!Cg&M3wOUVobQ{%ow9g^$OY!uji88+y0pcpbvmbZc5<^HaYZyxGi`Ja?7sM3Ylew|;$($q5 z6FOs2i#@Z+pDSibY8FYtc4R(}{zSx`aRbD*Dk^(|BUq!^Vfu7+qYS2>aKbO_1av3? zQh*H~s}knfS&WcT@YEV9>pSHy-p*D?q+7L2?oTF*;epGg;$k?jU^R_P^LdmC?zrYg z%DYI%mM}q2y52^Y(w?T~7u$qos9ZP9X(9@^3!HQ!i#fmHgNaz7?Vkwnwq-s@3Pbh% z#eQLHZurFtazfw?i|CvFG=&a_aC39Am@ccq6EF*>PV)4&S5-ZQag0mEC^36FneCl! zVqjw^RpHBEgVNVzOsj~PGKWK(LyJ#37i>=H*pt3PA}Uhdp_1q%h^ZdAHnT)vR_2;) zvKQ?U^2`M5-nv6kCu#zuYDirW0V9)BG;T?9X_wYg$^8Vndv<%=oXqufE<<^Ss`HH( zgy=Q!uDJzKH&ijIu)ATt^%e9i9==DH_3mk-yK=(!jCR=`hzk!NQjsvVHQKh<-_nKj zD3@D>S?3IZrPmZh>G~50I#I`DXo#CZkQ{+h&F$6$FVaK~qX(;4?q{ne!>7V5`JKX{ zb&Q=imx*j>UuwU}YMCJn^1F<(zs|`2@Rg9w1LS62-CUBuB@$2)n2|s+-MT(*o*w^n zcl`OwAe%?WKYbJAA2|5)fZY5YkTK;?H%QsB%VgG}`B4mR5`khj@`8e~Wv!;rZ5$b$U7#HOmJ>@@f(JUYSHYfsL#5jB#<^F z!4s|<9eTpu@}VE9Q}!C|O0%<9iXDG=rDsk+vcZjHQYKa+6?Hj95K(NO`b0T{F|;bklF6qw#wDJKHl3KDTJlgQiq%--X{%Bl{Y4q+GGG)(;E5BZ@yz z;1CK7Wa|Lv{Zz(hy~;n1YBPkLFY+2UGtF*G?c@rvi?_k}|b=>r5Xrv} z&a{ze-@-B^;HS&Vha)_9S(A6oX5~lZQWWj7lUiJsb<=Vkm=mkOM(}*Y8GkjY0bPZJ zI@rB41u2fR!9oyF8>`j_aor2-?nSoe$=aXL6f2mzmEzvXD0Y~T_Q7Zv-=|f{RKw;d zK2a2Lg%BPWZiGyCVF}R9SX^o=L~)&->&h`qpgAUzmD~qdlPV&{5Qp1w7G_e&Md+B3CW(==vV$0BN@rZzJ0c%KxMFlzZk*?@ONE!B28z);ay6FK=fwzF(h?q;*C ze1^8Ds9iptxRgIya$~ZdufCTK?b^|mDcXznBQ}k|3p_-@i@hhRx)nOQnq^@N$n1|k zp48jK)2hSj3E*;i*~GQtr$hwqu_eCqJ8w~(X_)FE8fNp>aRFf>z^5S-P+N& zJHwKmwrp@@SV2|AtbIh8?RH+Lo?Bt+6fvZ94IM0<&&XxNUx7?I6+K363=|39I-qz~ zo=+^CTX;QSH)QAm^sV>fc+t!=McY#r+qQwV9!My5)FO{!&)qo27?@nwy)9gsq4{?I zMxbH(3B{AsP=ZnT*#nHXirqrYsaRD{#L=6~5XcL8$n z#)i77yiN1S5Is%ve8|edmZVH*&O$6dG3K}v`^Y%bvgQ5$k`vpZ6RR6dyR`T#5*%LY zPJI|;QIo@Z!2*B7nB;}G%|G{X8W(Si?nvt}!25-xM2W^t*Y{El0DPardtktBN`HhMMiQ zRF!I^B#Tqyud3L%zG8G{95HvbQL%3^Aaq3BU}1W?KBCMeee#57?L74&+r~AGru?$a`tHM{ejEuH4qy%68rClI8gf1uy^J zOP;XL*JT1Nu9y^z8~`2Pe$;LM=`#1yQK6UY(HXR;ONk)XTbGp0l$98LxR?9qf*vww zVMrrLu*MU;iZJkEao-rPgUZfolntoTdGz8PJO2Y8b*I^>V+rbU_nJ=bKETTlqjEep zBA?Rr;rHdWtTNs^+&Ul&PS2u2%s{nr6|uG_h_{fGMk!| z9VsAv!ev}I=iyb8>~BXLKBztN!nnbrOU`Flux^+pb?5b^BfsI(Dz7c99MQVxD2!US ze7O42oMYoa;{ldlEd@%OJ?}GdqXVqhS?;INd!-zQ;rw+Yo zQ_COWu63yy!Hlnor`oyt6GFFLI&H(V!jVuhz4mDF2)eVfv}EU+#8Ldg;Wk0~wv>YA zy)Q!Q-GByjn7?OQR}ohJ&5;hWT;$swgiN9Bq$#8|jl~aZd#8nM@^Bj6<@>JSv+%_E1vdvuO2;KDCqQo7aTnJw2&SKRP zwfaD960<$n_;8|e_O0rx@>(fYHd^7LN=fv4; int: return self.amount_e.get_amount() or 0 def on_finalize_done(self, pi): + self.showSpinner(False) self.update_fields() if pi.error: self.show_error(pi.error) From 30abcad999d9d81c6f09c830ef51fded11deac4d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 27 Jun 2023 12:03:05 +0200 Subject: [PATCH 1024/1143] payment_identifier: move amount_range into payment_identifier --- electrum/gui/qt/send_tab.py | 8 +++----- electrum/payment_identifier.py | 9 +++++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index ee1af66d5..f83ca4040 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -418,11 +418,9 @@ def update_fields(self): self.set_field_validated(self.payto_e, validated=fields.validated) # LNURLp amount range - if pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR] \ - and pi.state == PaymentIdentifierState.LNURLP_FINALIZE \ - and pi.lnurl_data.min_sendable_sat != pi.lnurl_data.max_sendable_sat: - self.amount_e.setToolTip(_('Amount must be between %d and %d sat.') \ - % (pi.lnurl_data.min_sendable_sat, pi.lnurl_data.max_sendable_sat)) + if fields.amount_range: + amin, amax = fields.amount_range + self.amount_e.setToolTip(_('Amount must be between %d and %d sat.') % (amin, amax)) else: self.amount_e.setToolTip('') diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 157042182..8667d9263 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -204,6 +204,7 @@ class FieldsForGUI(NamedTuple): description: Optional[str] validated: Optional[bool] comment: Optional[int] + amount_range: Optional[tuple[int, int]] class PaymentIdentifier(Logger): @@ -632,6 +633,7 @@ def get_fields_for_GUI(self) -> FieldsForGUI: description = None validated = None comment = None + amount_range = None if (self.emaillike or self.domainlike) and self.openalias_data: key = self.emaillike if self.emaillike else self.domainlike @@ -650,10 +652,13 @@ def get_fields_for_GUI(self) -> FieldsForGUI: elif self.lnurl and self.lnurl_data: domain = urllib.parse.urlparse(self.lnurl).netloc recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>" - amount = self.lnurl_data.min_sendable_sat if self.lnurl_data.min_sendable_sat else None description = self.lnurl_data.metadata_plaintext if self.lnurl_data.comment_allowed: comment = self.lnurl_data.comment_allowed + if self.lnurl_data.min_sendable_sat: + amount = self.lnurl_data.min_sendable_sat + if self.lnurl_data.min_sendable_sat != self.lnurl_data.max_sendable_sat: + amount_range = (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat) elif self.bip70 and self.bip70_data: pr = self.bip70_data @@ -681,7 +686,7 @@ def get_fields_for_GUI(self) -> FieldsForGUI: description = label return FieldsForGUI(recipient=recipient, amount=amount, description=description, - comment=comment, validated=validated) + comment=comment, validated=validated, amount_range=amount_range) def _get_bolt11_fields(self, bolt11_invoice): """Parse ln invoice, and prepare the send tab for it.""" From 81544fdaedcd013ebff6f3b569f1fbd34774cf04 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 27 Jun 2023 12:12:23 +0200 Subject: [PATCH 1025/1143] send_tab: simplify lock_recipient check --- electrum/gui/qt/send_tab.py | 6 +++--- electrum/payment_identifier.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index f83ca4040..670111281 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -392,9 +392,9 @@ def update_fields(self): self.send_button.setEnabled(False) return - lock_recipient = pi.type != PaymentIdentifierType.SPK \ - and not (pi.type in [PaymentIdentifierType.EMAILLIKE, PaymentIdentifierType.DOMAINLIKE] \ - and pi.state in [PaymentIdentifierState.NOT_FOUND, PaymentIdentifierState.NEED_RESOLVE]) + lock_recipient = pi.type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR, + PaymentIdentifierType.OPENALIAS, PaymentIdentifierType.BIP70, + PaymentIdentifierType.BIP21, PaymentIdentifierType.BOLT11] and not pi.need_resolve() lock_amount = pi.is_amount_locked() lock_max = lock_amount or pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21] diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 8667d9263..a2619339a 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -298,7 +298,7 @@ def is_amount_locked(self): elif self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]: # amount limits known after resolve, might be specific amount or locked to range if self.need_resolve(): - return True + return False if self.need_finalize(): self.logger.debug(f'lnurl f {self.lnurl_data.min_sendable_sat}-{self.lnurl_data.max_sendable_sat}') return not (self.lnurl_data.min_sendable_sat < self.lnurl_data.max_sendable_sat) From 8ef2495096d0d4edf26eea7cb07113904923d3fe Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 29 Jun 2023 13:48:02 +0000 Subject: [PATCH 1026/1143] lnworker: use NamedTuple for received_mpp_htlcs. add/fix type hints try to avoid long plain tuples --- electrum/lnworker.py | 108 ++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 38 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 468596ace..c8b62d988 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -10,7 +10,7 @@ import operator from enum import IntEnum from typing import (Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, - NamedTuple, Union, Mapping, Any, Iterable, AsyncGenerator, DefaultDict) + NamedTuple, Union, Mapping, Any, Iterable, AsyncGenerator, DefaultDict, Callable) import threading import socket import aiohttp @@ -167,6 +167,13 @@ class PaymentInfo(NamedTuple): status: int +class ReceivedMPPStatus(NamedTuple): + is_expired: bool + is_accepted: bool + expected_msat: int + htlc_set: Set[Tuple[ShortChannelID, UpdateAddHtlc]] + + class ErrorAddingPeer(Exception): pass @@ -665,7 +672,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.sent_htlcs = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]] self.sent_htlcs_info = dict() # (RHASH, scid, htlc_id) -> route, payment_secret, amount_msat, bucket_msat, trampoline_fee_level self.sent_buckets = dict() # payment_secret -> (amount_sent, amount_failed) - self.received_mpp_htlcs = dict() # RHASH -> mpp_status, htlc_set + self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_secret -> ReceivedMPPStatus self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self) # detect inflight payments @@ -676,7 +683,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.trampoline_forwarding_failures = {} # todo: should be persisted # map forwarded htlcs (fw_info=(scid_hex, htlc_id)) to originating peer pubkeys self.downstream_htlc_to_upstream_peer_map = {} # type: Dict[Tuple[str, int], bytes] - self.hold_invoice_callbacks = {} # payment_hash -> callback, timeout + # payment_hash -> callback, timeout: + self.hold_invoice_callbacks = {} # type: Dict[bytes, Tuple[Callable[[bytes], None], int]] self.payment_bundles = [] # lists of hashes. todo:persist @@ -1891,11 +1899,13 @@ def get_payment_info(self, payment_hash: bytes) -> Optional[PaymentInfo]: amount_msat, direction, status = self.payment_info[key] return PaymentInfo(payment_hash, amount_msat, direction, status) - def add_payment_info_for_hold_invoice(self, payment_hash, lightning_amount_sat): + def add_payment_info_for_hold_invoice(self, payment_hash: bytes, lightning_amount_sat: int): info = PaymentInfo(payment_hash, lightning_amount_sat * 1000, RECEIVED, PR_UNPAID) self.save_payment_info(info, write_to_disk=False) - def register_callback_for_hold_invoice(self, payment_hash, cb, timeout: Optional[int] = None): + def register_callback_for_hold_invoice( + self, payment_hash: bytes, cb: Callable[[bytes], None], timeout: int, + ): expiry = int(time.time()) + timeout self.hold_invoice_callbacks[payment_hash] = cb, expiry @@ -1907,7 +1917,12 @@ def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> if write_to_disk: self.wallet.save_db() - def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddHtlc, expected_msat: int) -> Optional[bool]: + def check_received_htlc( + self, payment_secret: bytes, + short_channel_id: ShortChannelID, + htlc: UpdateAddHtlc, + expected_msat: int, + ) -> Optional[bool]: """ return MPP status: True (accepted), False (expired) or None (waiting) """ payment_hash = htlc.payment_hash @@ -1952,47 +1967,64 @@ def check_received_htlc(self, payment_secret, short_channel_id, htlc: UpdateAddH self.maybe_cleanup_mpp_status(payment_secret, short_channel_id, htlc) return True if is_accepted else (False if is_expired else None) - def update_mpp_with_received_htlc(self, payment_secret, short_channel_id, htlc, expected_msat): + def update_mpp_with_received_htlc( + self, + payment_secret: bytes, + short_channel_id: ShortChannelID, + htlc: UpdateAddHtlc, + expected_msat: int, + ): # add new htlc to set - is_expired, is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs.get(payment_secret, (False, False, expected_msat, set())) - assert expected_msat == _expected_msat + mpp_status = self.received_mpp_htlcs.get(payment_secret) + if mpp_status is None: + mpp_status = ReceivedMPPStatus( + is_expired=False, + is_accepted=False, + expected_msat=expected_msat, + htlc_set=set(), + ) + assert expected_msat == mpp_status.expected_msat key = (short_channel_id, htlc) - if key not in htlc_set: - htlc_set.add(key) - self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, _expected_msat, htlc_set - - def get_mpp_status(self, payment_secret): - is_expired, is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs[payment_secret] - return is_expired, is_accepted - - def set_mpp_status(self, payment_secret, is_expired, is_accepted): - _is_expired, _is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs[payment_secret] - self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, _expected_msat, htlc_set + if key not in mpp_status.htlc_set: + mpp_status.htlc_set.add(key) # side-effecting htlc_set + self.received_mpp_htlcs[payment_secret] = mpp_status + + def get_mpp_status(self, payment_secret: bytes) -> Tuple[bool, bool]: + mpp_status = self.received_mpp_htlcs[payment_secret] + return mpp_status.is_expired, mpp_status.is_accepted + + def set_mpp_status(self, payment_secret: bytes, is_expired: bool, is_accepted: bool): + mpp_status = self.received_mpp_htlcs[payment_secret] + self.received_mpp_htlcs[payment_secret] = mpp_status._replace( + is_expired=is_expired, + is_accepted=is_accepted, + ) - def is_mpp_amount_reached(self, payment_secret): - mpp = self.received_mpp_htlcs.get(payment_secret) - if not mpp: + def is_mpp_amount_reached(self, payment_secret: bytes) -> bool: + mpp_status = self.received_mpp_htlcs.get(payment_secret) + if not mpp_status: return False - is_expired, is_accepted, _expected_msat, htlc_set = mpp - total = sum([_htlc.amount_msat for scid, _htlc in htlc_set]) - return total >= _expected_msat + total = sum([_htlc.amount_msat for scid, _htlc in mpp_status.htlc_set]) + return total >= mpp_status.expected_msat - def get_first_timestamp_of_mpp(self, payment_secret): - mpp = self.received_mpp_htlcs.get(payment_secret) - if not mpp: + def get_first_timestamp_of_mpp(self, payment_secret: bytes) -> int: + mpp_status = self.received_mpp_htlcs.get(payment_secret) + if not mpp_status: return int(time.time()) - is_expired, is_accepted, _expected_msat, htlc_set = mpp - return min([_htlc.timestamp for scid, _htlc in htlc_set]) + return min([_htlc.timestamp for scid, _htlc in mpp_status.htlc_set]) - def maybe_cleanup_mpp_status(self, payment_secret, short_channel_id, htlc): - is_expired, is_accepted, _expected_msat, htlc_set = self.received_mpp_htlcs[payment_secret] - if not is_accepted and not is_expired: + def maybe_cleanup_mpp_status( + self, + payment_secret: bytes, + short_channel_id: ShortChannelID, + htlc: UpdateAddHtlc, + ) -> None: + mpp_status = self.received_mpp_htlcs[payment_secret] + if not mpp_status.is_accepted and not mpp_status.is_expired: return key = (short_channel_id, htlc) - htlc_set.remove(key) - if len(htlc_set) > 0: - self.received_mpp_htlcs[payment_secret] = is_expired, is_accepted, _expected_msat, htlc_set - elif payment_secret in self.received_mpp_htlcs: + mpp_status.htlc_set.remove(key) # side-effecting htlc_set + if not mpp_status.htlc_set and payment_secret in self.received_mpp_htlcs: self.received_mpp_htlcs.pop(payment_secret) def get_payment_status(self, payment_hash: bytes) -> int: From a66b0c6a12b2ed7ef4b381a289acaf5fab56ec07 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Jun 2023 17:23:12 +0000 Subject: [PATCH 1027/1143] lnaddr: rm some tests where feature bits do not make sense Not all feature flags are supposed to go into the invoice. --- electrum/tests/test_bolt11.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py index 7265bc874..9d5fb19fd 100644 --- a/electrum/tests/test_bolt11.py +++ b/electrum/tests/test_bolt11.py @@ -93,12 +93,6 @@ def test_roundtrip(self): "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qzg2f9ep5rqksjjdjzq20eqkwvsd0gx0llf2lv6x395l3ph82naeqkg3slj7s326sqnk4ql32acs2fft4p5tyjt8ujxtnhauu4mp7w4xgaqpp7a6ha"), (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9))]), "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qzs2035s0h84dfv9lykfcscuh5phy8mmq53nyu9szwln7d02xaz57t59p22pkzavenfa8qetvtkf27l9h9n3k55puvx6573d7fwhmwp6cvcqjvjqe7"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 7) + (1 << 11))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrzy2tq24ful9ktl7dsnpr8y53dg5w6g2cak8q4pchzjepedmrxhv7qm3z5hhca5c3yjd34cvcc0qd7ntwgefrxxn0cmcsn4cxlnkvrmx5gcp3mmpw5"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 12))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qryq226vdxsf8jq83t80fmunnlkj3va9nmw54x9ze0tqnyvvqch675y29pm978ppkhgp6hnwj98g4zalgecpqkckr9x90ugq44e5tnfe7kxqplr63uz"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 13))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrgq2f4fm9v4qzp3072d6vaslq99m0rhmfa7plx6wumu6rpdpz53l2zuhc56xekrzwqwsdaahsl8jg0vh3zhpvc78ywc9cas859mvs28xfpgpgn8usc"), (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]), "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrss2y8hzphx329clpfz86r60zd3ctn2q0uuakge6qws075r7sf43r8wpmrv36ujj68mzdw6rhkxy4mal5zullec8v6yjnnsh093qjwc5cuspz34uag"), (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]), From fc6486ecdb01667f54e57d9081b54a65233b1686 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Jun 2023 18:28:07 +0000 Subject: [PATCH 1028/1143] lnaddr: make payment_secret field mandatory, in both directions we now require payment_secret both for sending and for receiving (previously was optional for both) see https://github.com/lightning/bolts/pull/898 https://github.com/ACINQ/eclair/pull/1810 https://github.com/ElementsProject/lightning/pull/4646 note: payment_secret depends on var_onion_optin, so that becomes mandatory as well, however this commit does not yet remove the ability of creating legacy onions --- electrum/lnaddr.py | 17 ++++++- electrum/lnonion.py | 18 +++---- electrum/lnpeer.py | 30 ++++++------ electrum/lnworker.py | 14 +++--- electrum/tests/test_bolt11.py | 88 +++++++++++++++++++++-------------- electrum/tests/test_lnpeer.py | 2 +- electrum/trampoline.py | 14 ++++-- 7 files changed, 111 insertions(+), 72 deletions(-) diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index fea9e2ce6..0476bf0c1 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -184,6 +184,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: tags_set = set() # Payment hash + assert addr.paymenthash is not None data += tagged_bytes('p', addr.paymenthash) tags_set.add('p') @@ -196,7 +197,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: # BOLT #11: # # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, - if k in ('d', 'h', 'n', 'x', 'p', 's'): + if k in ('d', 'h', 'n', 'x', 'p', 's', '9'): if k in tags_set: raise LnEncodeException("Duplicate '{}' tag".format(k)) @@ -317,6 +318,17 @@ def get_features(self) -> 'LnFeatures': from .lnutil import LnFeatures return LnFeatures(self.get_tag('9') or 0) + def validate_and_compare_features(self, myfeatures: 'LnFeatures') -> None: + """Raises IncompatibleOrInsaneFeatures. + + note: these checks are not done by the parser (in lndecode), as then when we started requiring a new feature, + old saved already paid invoices could no longer be parsed. + """ + from .lnutil import validate_features, ln_compare_features + invoice_features = self.get_features() + validate_features(invoice_features) + ln_compare_features(myfeatures.for_invoice(), invoice_features) + def __str__(self): return "LnAddr[{}, amount={}{} tags=[{}]]".format( hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None, @@ -381,6 +393,9 @@ def serialize(self): return self.pubkey.get_public_key_bytes(True) def lndecode(invoice: str, *, verbose=False, net=None) -> LnAddr: + """Parses a string into an LnAddr object. + Can raise LnDecodeException or IncompatibleOrInsaneFeatures. + """ if net is None: net = constants.net decoded_bech32 = bech32_decode(invoice, ignore_long_length=True) diff --git a/electrum/lnonion.py b/electrum/lnonion.py index 95c81ba0e..9dc687a1b 100644 --- a/electrum/lnonion.py +++ b/electrum/lnonion.py @@ -150,7 +150,7 @@ def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle': elif first_byte == b'\x01': # reserved for future use raise Exception("unsupported hop payload: length==1") - else: + else: # tlv format hop_payload_length = read_bigsize_int(fd) hop_payload = fd.read(hop_payload_length) if hop_payload_length != len(hop_payload): @@ -266,8 +266,9 @@ def calc_hops_data_for_payment( route: 'LNPaymentRoute', amount_msat: int, final_cltv: int, *, - total_msat=None, - payment_secret: bytes = None) -> Tuple[List[OnionHopsDataSingle], int, int]: + total_msat: int, + payment_secret: bytes, +) -> Tuple[List[OnionHopsDataSingle], int, int]: """Returns the hops_data to be used for constructing an onion packet, and the amount_msat and cltv to be used on our immediate channel. @@ -283,12 +284,11 @@ def calc_hops_data_for_payment( } # for multipart payments we need to tell the receiver about the total and # partial amounts - if payment_secret is not None: - hop_payload["payment_data"] = { - "payment_secret": payment_secret, - "total_msat": total_msat, - "amount_msat": amt - } + hop_payload["payment_data"] = { + "payment_secret": payment_secret, + "total_msat": total_msat, + "amount_msat": amt + } hops_data = [OnionHopsDataSingle( is_tlv_payload=route[-1].has_feature_varonion(), payload=hop_payload)] # payloads, backwards from last hop (but excluding the first edge): diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index e94841f89..a49b627b2 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1441,7 +1441,7 @@ def pay(self, *, total_msat: int, payment_hash: bytes, min_final_cltv_expiry: int, - payment_secret: bytes = None, + payment_secret: bytes, trampoline_onion=None) -> UpdateAddHtlc: assert amount_msat > 0, "amount_msat is not greater zero" @@ -1786,7 +1786,8 @@ def log_fail_reason(reason: str): try: total_msat = processed_onion.hop_data.payload["payment_data"]["total_msat"] except Exception: - total_msat = amt_to_forward # fall back to "amt_to_forward" + log_fail_reason(f"'total_msat' missing from onion") + raise exc_incorrect_or_unknown_pd if not is_trampoline and amt_to_forward != htlc.amount_msat: log_fail_reason(f"amt_to_forward != htlc.amount_msat") @@ -1795,14 +1796,10 @@ def log_fail_reason(reason: str): data=htlc.amount_msat.to_bytes(8, byteorder="big")) try: - payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"] + payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"] # type: bytes except Exception: - if total_msat > amt_to_forward: - # payment_secret is required for MPP - log_fail_reason(f"'payment_secret' missing from onion") - raise exc_incorrect_or_unknown_pd - # TODO fail here if invoice has set PAYMENT_SECRET_REQ - payment_secret_from_onion = None + log_fail_reason(f"'payment_secret' missing from onion") + raise exc_incorrect_or_unknown_pd payment_status = self.lnworker.check_received_htlc(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) if payment_status is None: @@ -1825,14 +1822,13 @@ def log_fail_reason(reason: str): log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}") raise exc_incorrect_or_unknown_pd preimage = self.lnworker.get_preimage(htlc.payment_hash) - if payment_secret_from_onion: - expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)] - if preimage: - # legacy secret for old invoices - expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) - if payment_secret_from_onion not in expected_payment_secrets: - log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}') - raise exc_incorrect_or_unknown_pd + expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)] + if preimage: + # legacy secret for old invoices + expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) + if payment_secret_from_onion not in expected_payment_secrets: + log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}') + raise exc_incorrect_or_unknown_pd invoice_msat = info.amount_msat if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat): log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}") diff --git a/electrum/lnworker.py b/electrum/lnworker.py index c8b62d988..10f71ae8d 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -194,6 +194,8 @@ class ErrorAddingPeer(Exception): pass | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ | LnFeatures.GOSSIP_QUERIES_REQ + | LnFeatures.VAR_ONION_REQ + | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.BASIC_MPP_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT @@ -1238,7 +1240,7 @@ async def pay_to_node( self, *, node_pubkey: bytes, payment_hash: bytes, - payment_secret: Optional[bytes], + payment_secret: bytes, amount_to_pay: int, # in msat min_cltv_expiry: int, r_tags, @@ -1389,7 +1391,7 @@ async def pay_to_route( total_msat: int, amount_receiver_msat:int, payment_hash: bytes, - payment_secret: Optional[bytes], + payment_secret: bytes, min_cltv_expiry: int, trampoline_onion: bytes = None, trampoline_fee_level: int, @@ -1546,8 +1548,7 @@ def _decode_channel_update_msg(cls, chan_upd_msg: bytes) -> Optional[Dict[str, A except Exception: return None - @staticmethod - def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr: + def _check_invoice(self, invoice: str, *, amount_msat: int = None) -> LnAddr: addr = lndecode(invoice) if addr.is_expired(): raise InvoiceError(_("This invoice has expired")) @@ -1562,6 +1563,7 @@ def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr: raise InvoiceError("{}\n{}".format( _("Invoice wants us to risk locking funds for unreasonably long."), f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}")) + addr.validate_and_compare_features(self.features) return addr def is_trampoline_peer(self, node_id: bytes) -> bool: @@ -1615,8 +1617,8 @@ async def create_routes_for_payment( min_cltv_expiry, r_tags, invoice_features: int, - payment_hash, - payment_secret, + payment_hash: bytes, + payment_secret: bytes, trampoline_fee_level: int, use_two_trampolines: bool, fwd_trampoline_onion=None, diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py index 9d5fb19fd..148cbb9b5 100644 --- a/electrum/tests/test_bolt11.py +++ b/electrum/tests/test_bolt11.py @@ -7,13 +7,14 @@ from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode, u5_to_bitarray, bitarray_to_u5 from electrum.segwit_addr import bech32_encode, bech32_decode from electrum import segwit_addr -from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures +from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage, LnFeatures, IncompatibleLightningFeatures from electrum import constants from . import ElectrumTestCase RHASH=unhexlify('0001020304050607080900010203040506070809000102030405060708090102') +PAYMENT_SECRET=unhexlify('1111111111111111111111111111111111111111111111111111111111111111') CONVERSION_RATE=1200 PRIVKEY=unhexlify('e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734') PUBKEY=unhexlify('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad') @@ -65,6 +66,41 @@ def test_roundtrip(self): timestamp = 1615922274 tests = [ + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, tags=[('d', ''), ('9', 33282)]), + "lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdqq9qypqszpyrpe4tym8d3q87d43cgdhhlsrt78epu7u99mkzttmt2wtsx0304rrw50addkryfrd3vn3zy467vxwlmf4uz7yvntuwjr2hqjl9lw5cqwtp2dy"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('9', 0x28200)]), + "lnbc1m1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5xysxxatsyp3k7enxv4jsxqzpu9qy9qsqw8l2pulslacwjt86vle3sgfdmcct5v34gtcpfnujsf6ufqa7v7jzdpddnwgte82wkscdlwfwucrgn8z36rv9hzk5mukltteh0yqephqpk5vegu"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=Decimal('1'), tags=[('h', longdescription), ('9', 0x28200)]), + "lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsq0jnua6dc4p984aeafs6ss7tjjj7553ympvg82qrjq0zgdqgtdvt5wlwkvw4ds5sn96nazp6ct9ts37tcw708kzkk4p8znahpsgp9tnspnycsf7"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription), ('9', 0x28200)]), + "lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqy5826t0z3sn29z396pmr4kv73lcx0v7y6vas6h3pysmqllmzwgm5ps2t468gm4psj52usjy6y4xcry4k84n2zggs6f9agwg95454v6gqrwmh4f"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[ + ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), + (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]), + ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'), + ('h', longdescription), + ('9', 0x28200)]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqfnk063vsrgjx7l6td6v42skuxql7epn5tmrl4qte2e78nqnsjlgjg3sgkxreqex5fw4c9chnvtc2hykqnyxr84zwfr8f3d9q3h0nfdgqenlzvj"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription), ('9', 0x28200)]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqqf6z4r7ruzr5txm5ln4netwa2f4x233tud7jy8gxrynyx07rxt7qm92yk2krlgwr7d8jknglur75sujeyapmda5nf3femrk2mep8a2cp4hlvup"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription), ('9', 0x28200)]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfppqw508d6qejxtdg4y5r3zarvary0c5xw7khp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqy4wp73jma5uktd9y7yha56f98n2k0hxgnvp2qdcury00dapps3k3urgfy8tvv8jzwcafpy576msk5xx2hladf06m3s5mgx5msn4elfqqaaqjhk"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription), ('9', 0x28200)]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsqgt4gg9uktlpgnnuvczazusp5uwjv78na305ucsw06c8uk58e5stjqj9sz7fgavw0z688alt364js72mc9mg8yumhpes2dsmq5k9nr5qqddykxy"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('n', PUBKEY), ('h', longdescription), ('9', 0x28200)]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qy9qsq2y235rxw7v0gkn2t9ehc742tm3p22q2yjjykq4d85ze6g62yk60navxqz0ga96sqrszju8nlfajthem4gngxvyz4hwy39j4nqm8kv0qq9znxs7"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 2 + (1 << 9) + (1 << 15))]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqszrwfgrl5k3rt4q4mclc8t00p2tcjsf9pmpcq6lu5zhmampyvk43fk30eqpdm8t5qmdpzan25aqxqaqdzmy0smrtduazjcxx975vz78ccpx0qhev"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8) + (1 << 15))]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqg2wans8f6vkfd3l7zjv547hlc7wd7eqyxfwhtdudnkkgrpk6p9ffykwrvdtwm0aakaxujurdxgd7cllnfypmj22cvy7z333udg6zncgacqzmd2z9"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqs2dr525u5f4kjxdv0hq5c822qwxrtttjl4u586yl84x0kvvx66gz9ygy76005s5sjwgr7fp55ccsae47vpl4gqvwhc3exps964g743j5gqwtt68t"), + (LnAddr(date=timestamp, paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]), + "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrss2f8kr98446xls02yndup2ynwjh46u8kdeuuncexx2hnets0j0064nyq25gkd6jnttldzt5qqtszum5dufvuvryxt204w2p24557udxgcp0nlwtw"), + ] + # Some old tests follow that do not have payment_secret. Note that if the parser raised due to the lack of features/payment_secret, + # old wallets that have these invoices saved (as paid/expired), could not be opened (though we could do a db upgrade and delete them). + tests.extend([ (LnAddr(date=timestamp, paymenthash=RHASH, tags=[('d', '')]), "lnbc1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqd9n3kwjjwglnfne5p4rvkze998m3xcxrc8kunl5khkchlaqhwhlyztuuwkrglv47mqg96mcqjjx70hh9luaj4te0u4ww6aclxwve3fqpkmdxlj"), (LnAddr(date=timestamp, paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]), @@ -73,33 +109,8 @@ def test_roundtrip(self): "lnbc11ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs2qjafckq94q3js6lvqz2kmenn9ysjejyj8fm4hlx0xtqhaxfzlxjappkgp0hmm40dnuan4v3jy83lqjup2n0fdzgysg049y9l9uc98qq07kfd3"), (LnAddr(date=timestamp, paymenthash=RHASH, net=constants.BitcoinTestnet, tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]), "lntb1ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un98hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsr9zktgu78k8p9t8555ve37qwfvqn6ga37fnfwhgexmf20nzdpmuhwvuv7zra3xrh8y2ggxxuemqfsgka9x7uzsrcx8rfv85c8pmhq9gq4sampn"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[ - ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), - (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]), - ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'), - ('h', longdescription)]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzqfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsq68hmxx9ar8eh9nq6gcafxd4vn4mqy458f744t0lms3anm2svydxx2lv84ardcks83u0h34u3lvflh0x9y8qdgjj3q3lxqp5kzqueygqema2z9"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z9hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfa9a608cewefn0n6wflmd27s4nvevru262k2uj34wq58c4y5tqjrs77kvd5umnjgpndxfchde0h0mc07l65agyh9dqlgz5ujhpe8ewspsve8hh"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7khp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqstrtguf9h6ur3n3dchft84q46yy50gf0vugq8g3n88txqcn25dhg98tt4wvlhy967cdarj6cznwn3uyssqeu0e3jgdt9mh5nz9xyqsggpnp2hht"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsvv679nlk4m93cahuxv04qqv6q8gshqu5f5tcgcasayuejxny4t4rpugqh4fy4zrma23ts93zclhsm694pu9ll0qlfaqkpstu7u02l8gq6fr4jy"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66hp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqst7hmgl7lmqxaael9g7w3e43acceyz93920457yv2egsfkcpnxqf9p0wu8x6dy34k580rulrtvt77f757g2k9lkf7ggph4pyux6e8wksq5ejkr3"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 514)]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qzsz20x48k6dgxsrrsqhccvuwtsjny2flcyhlpyuz5lufn4wvjml7wwkaaxfyxpkk2j84hq4xdvm2pt265hm7jy97p5f34gu2tcwgvd9j4gqcam6kj"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qzg2f9ep5rqksjjdjzq20eqkwvsd0gx0llf2lv6x395l3ph82naeqkg3slj7s326sqnk4ql32acs2fft4p5tyjt8ujxtnhauu4mp7w4xgaqpp7a6ha"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qzs2035s0h84dfv9lykfcscuh5phy8mmq53nyu9szwln7d02xaz57t59p22pkzavenfa8qetvtkf27l9h9n3k55puvx6573d7fwhmwp6cvcqjvjqe7"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrss2y8hzphx329clpfz86r60zd3ctn2q0uuakge6qws075r7sf43r8wpmrv36ujj68mzdw6rhkxy4mal5zullec8v6yjnnsh093qjwc5cuspz34uag"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqs2gc0fc84x29vk0pmq6p4qcn2ttn9azxtfrf2xqz00e79cfvf4nqvx96hz94uqsh4j4hnyywp63nagddwm0zdscprvkqlhltysa478x3sqkee5v9"), - (LnAddr(date=timestamp, paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 33282)], payment_secret=b"\x11" * 32), - "lnbc241ps9zprzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qypqszrwfgrl5k3rt4q4mclc8t00p2tcjsf9pmpcq6lu5zhmampyvk43fk30eqpdm8t5qmdpzan25aqxqaqdzmy0smrtduazjcxx975vz78ccpx0qhev"), - ] + + ]) # Roundtrip for lnaddr1, invoice_str1 in tests: @@ -112,7 +123,7 @@ def test_n_decoding(self): # We flip the signature recovery bit, which would normally give a different # pubkey. _, hrp, data = bech32_decode( - lnencode(LnAddr(paymenthash=RHASH, amount=24, tags=[('d', '')]), PRIVKEY), + lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('9', 33282)]), PRIVKEY), ignore_long_length=True) databits = u5_to_bitarray(data) databits.invert(-1) @@ -121,7 +132,7 @@ def test_n_decoding(self): # But not if we supply expliciy `n` specifier! _, hrp, data = bech32_decode( - lnencode(LnAddr(paymenthash=RHASH, amount=24, tags=[('d', ''), ('n', PUBKEY)]), PRIVKEY), + lnencode(LnAddr(paymenthash=RHASH, payment_secret=PAYMENT_SECRET, amount=24, tags=[('d', ''), ('n', PUBKEY), ('9', 33282)]), PRIVKEY), ignore_long_length=True) databits = u5_to_bitarray(data) databits.invert(-1) @@ -129,7 +140,7 @@ def test_n_decoding(self): assert lnaddr.pubkey.serialize() == PUBKEY def test_min_final_cltv_expiry_decoding(self): - lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", + lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qsp5qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqsdqqcqzys9qypqsqp2h6a5xeytuc3fad2ed4gxvhd593lwjdna3dxsyeem0qkzjx6guk44jend0xq4zzvp6f3fy07wnmxezazzsxgmvqee8shxjuqu2eu0qpnvc95x", net=constants.BitcoinSimnet) self.assertEqual(144, lnaddr.get_min_final_cltv_expiry()) @@ -139,15 +150,16 @@ def test_min_final_cltv_expiry_decoding(self): def test_min_final_cltv_expiry_roundtrip(self): for cltv in (1, 15, 16, 31, 32, 33, 150, 511, 512, 513, 1023, 1024, 1025): - lnaddr = LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv)]) + lnaddr = LnAddr( + paymenthash=RHASH, payment_secret=b"\x01"*32, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', cltv), ('9', 33282)]) self.assertEqual(cltv, lnaddr.get_min_final_cltv_expiry()) invoice = lnencode(lnaddr, PRIVKEY) self.assertEqual(cltv, lndecode(invoice).get_min_final_cltv_expiry()) def test_features(self): - lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl") - self.assertEqual(514, lnaddr.get_tag('9')) - self.assertEqual(LnFeatures(514), lnaddr.get_features()) + lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9qypqsztrz5v3jfnxskfv7g8chmyzyrfhf2vupcavuq5rce96kyt6g0zh337h206awccwp335zarqrud4wccgdn39vur44d8um4hmgv06aj0sgpdrv73z") + self.assertEqual(33282, lnaddr.get_tag('9')) + self.assertEqual(LnFeatures(33282), lnaddr.get_features()) with self.assertRaises(UnknownEvenFeatureBits): lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7") @@ -161,3 +173,9 @@ def test_derive_payment_secret_from_payment_preimage(self): preimage = bytes.fromhex("cc3fc000bdeff545acee53ada12ff96060834be263f77d645abbebc3a8d53b92") self.assertEqual("bfd660b559b3f452c6bb05b8d2906f520c151c107b733863ed0cc53fc77021a8", derive_payment_secret_from_payment_preimage(preimage).hex()) + + def test_validate_and_compare_features(self): + lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l") + lnaddr.validate_and_compare_features(LnFeatures((1 << 8) + (1 << 14) + (1 << 15))) + with self.assertRaises(IncompatibleLightningFeatures): + lnaddr.validate_and_compare_features(LnFeatures((1 << 8) + (1 << 14) + (1 << 16))) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index f78c04603..e291f8e8a 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -256,7 +256,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln get_preimage = LNWallet.get_preimage create_route_for_payment = LNWallet.create_route_for_payment create_routes_for_payment = LNWallet.create_routes_for_payment - _check_invoice = staticmethod(LNWallet._check_invoice) + _check_invoice = LNWallet._check_invoice pay_to_route = LNWallet.pay_to_route pay_to_node = LNWallet.pay_to_node pay_invoice = LNWallet.pay_invoice diff --git a/electrum/trampoline.py b/electrum/trampoline.py index 2253bc06f..c3c5dd6f7 100644 --- a/electrum/trampoline.py +++ b/electrum/trampoline.py @@ -232,7 +232,15 @@ def create_trampoline_route( return route -def create_trampoline_onion(*, route, amount_msat, final_cltv, total_msat, payment_hash, payment_secret): +def create_trampoline_onion( + *, + route, + amount_msat, + final_cltv, + total_msat: int, + payment_hash: bytes, + payment_secret: bytes, +): # all edges are trampoline hops_data, amount_msat, cltv = calc_hops_data_for_payment( route, @@ -281,8 +289,8 @@ def create_trampoline_route_and_onion( my_pubkey: bytes, node_id, r_tags, - payment_hash, - payment_secret, + payment_hash: bytes, + payment_secret: bytes, local_height: int, trampoline_fee_level: int, use_two_trampolines: bool, From 6b43eac6fdba2d42a36291185c3c0337e98e0b43 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 16 Jun 2023 20:01:56 +0000 Subject: [PATCH 1029/1143] lnonion: rm support for legacy (pre-TLV) onions see https://github.com/lightning/bolts/pull/962 --- electrum/lnonion.py | 110 +++++-------------- electrum/lnrouter.py | 6 ++ electrum/tests/test_lnrouter.py | 184 +++++++------------------------- 3 files changed, 70 insertions(+), 230 deletions(-) diff --git a/electrum/lnonion.py b/electrum/lnonion.py index 9dc687a1b..c7cd9d44e 100644 --- a/electrum/lnonion.py +++ b/electrum/lnonion.py @@ -42,7 +42,6 @@ HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 TRAMPOLINE_HOPS_DATA_SIZE = 400 -LEGACY_PER_HOP_FULL_SIZE = 65 PER_HOP_HMAC_SIZE = 32 @@ -51,53 +50,9 @@ class InvalidOnionMac(Exception): pass class InvalidOnionPubkey(Exception): pass -class LegacyHopDataPayload: - - def __init__(self, *, short_channel_id: bytes, amt_to_forward: int, outgoing_cltv_value: int): - self.short_channel_id = ShortChannelID(short_channel_id) - self.amt_to_forward = amt_to_forward - self.outgoing_cltv_value = outgoing_cltv_value - - def to_bytes(self) -> bytes: - ret = self.short_channel_id - ret += int.to_bytes(self.amt_to_forward, length=8, byteorder="big", signed=False) - ret += int.to_bytes(self.outgoing_cltv_value, length=4, byteorder="big", signed=False) - ret += bytes(12) # padding - if len(ret) != 32: - raise Exception('unexpected length {}'.format(len(ret))) - return ret - - def to_tlv_dict(self) -> dict: - d = { - "amt_to_forward": {"amt_to_forward": self.amt_to_forward}, - "outgoing_cltv_value": {"outgoing_cltv_value": self.outgoing_cltv_value}, - "short_channel_id": {"short_channel_id": self.short_channel_id}, - } - return d - - @classmethod - def from_bytes(cls, b: bytes) -> 'LegacyHopDataPayload': - if len(b) != 32: - raise Exception('unexpected length {}'.format(len(b))) - return LegacyHopDataPayload( - short_channel_id=b[:8], - amt_to_forward=int.from_bytes(b[8:16], byteorder="big", signed=False), - outgoing_cltv_value=int.from_bytes(b[16:20], byteorder="big", signed=False), - ) - - @classmethod - def from_tlv_dict(cls, d: dict) -> 'LegacyHopDataPayload': - return LegacyHopDataPayload( - short_channel_id=d["short_channel_id"]["short_channel_id"] if "short_channel_id" in d else b"\x00" * 8, - amt_to_forward=d["amt_to_forward"]["amt_to_forward"], - outgoing_cltv_value=d["outgoing_cltv_value"]["outgoing_cltv_value"], - ) - - class OnionHopsDataSingle: # called HopData in lnd - def __init__(self, *, is_tlv_payload: bool, payload: dict = None): - self.is_tlv_payload = is_tlv_payload + def __init__(self, *, payload: dict = None): if payload is None: payload = {} self.payload = payload @@ -107,29 +62,20 @@ def __init__(self, *, is_tlv_payload: bool, payload: dict = None): def to_bytes(self) -> bytes: hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) if self._raw_bytes_payload is not None: - ret = write_bigsize_int(len(self._raw_bytes_payload)) - ret += self._raw_bytes_payload - ret += hmac_ - return ret - if not self.is_tlv_payload: - ret = b"\x00" # realm==0 - legacy_payload = LegacyHopDataPayload.from_tlv_dict(self.payload) - ret += legacy_payload.to_bytes() + ret = self._raw_bytes_payload ret += hmac_ - if len(ret) != LEGACY_PER_HOP_FULL_SIZE: - raise Exception('unexpected length {}'.format(len(ret))) return ret - else: # tlv - payload_fd = io.BytesIO() - OnionWireSerializer.write_tlv_stream(fd=payload_fd, - tlv_stream_name="payload", - **self.payload) - payload_bytes = payload_fd.getvalue() - with io.BytesIO() as fd: - fd.write(write_bigsize_int(len(payload_bytes))) - fd.write(payload_bytes) - fd.write(hmac_) - return fd.getvalue() + # adding TLV payload. note: legacy hop data format no longer supported. + payload_fd = io.BytesIO() + OnionWireSerializer.write_tlv_stream(fd=payload_fd, + tlv_stream_name="payload", + **self.payload) + payload_bytes = payload_fd.getvalue() + with io.BytesIO() as fd: + fd.write(write_bigsize_int(len(payload_bytes))) + fd.write(payload_bytes) + fd.write(hmac_) + return fd.getvalue() @classmethod def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle': @@ -139,14 +85,7 @@ def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle': fd.seek(-1, io.SEEK_CUR) # undo read if first_byte == b'\x00': # legacy hop data format - b = fd.read(LEGACY_PER_HOP_FULL_SIZE) - if len(b) != LEGACY_PER_HOP_FULL_SIZE: - raise Exception(f'unexpected length {len(b)}') - ret = OnionHopsDataSingle(is_tlv_payload=False) - legacy_payload = LegacyHopDataPayload.from_bytes(b[1:33]) - ret.payload = legacy_payload.to_tlv_dict() - ret.hmac = b[33:] - return ret + raise Exception("legacy hop data format no longer supported") elif first_byte == b'\x01': # reserved for future use raise Exception("unsupported hop payload: length==1") @@ -155,7 +94,7 @@ def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle': hop_payload = fd.read(hop_payload_length) if hop_payload_length != len(hop_payload): raise Exception(f"unexpected EOF") - ret = OnionHopsDataSingle(is_tlv_payload=True) + ret = OnionHopsDataSingle() ret.payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload), tlv_stream_name="payload") ret.hmac = fd.read(PER_HOP_HMAC_SIZE) @@ -163,7 +102,7 @@ def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle': return ret def __repr__(self): - return f"" + return f"" class OnionPacket: @@ -226,8 +165,14 @@ def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes], return hop_shared_secrets -def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, - hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes, trampoline=False) -> OnionPacket: +def new_onion_packet( + payment_path_pubkeys: Sequence[bytes], + session_key: bytes, + hops_data: Sequence[OnionHopsDataSingle], + *, + associated_data: bytes, + trampoline: bool = False, +) -> OnionPacket: num_hops = len(payment_path_pubkeys) assert num_hops == len(hops_data) hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key) @@ -289,8 +234,7 @@ def calc_hops_data_for_payment( "total_msat": total_msat, "amount_msat": amt } - hops_data = [OnionHopsDataSingle( - is_tlv_payload=route[-1].has_feature_varonion(), payload=hop_payload)] + hops_data = [OnionHopsDataSingle(payload=hop_payload)] # payloads, backwards from last hop (but excluding the first edge): for edge_index in range(len(route) - 1, 0, -1): route_edge = route[edge_index] @@ -304,9 +248,7 @@ def calc_hops_data_for_payment( "short_channel_id": {"short_channel_id": route_edge.short_channel_id}, } hops_data.append( - OnionHopsDataSingle( - is_tlv_payload=route[edge_index-1].has_feature_varonion(), - payload=hop_payload)) + OnionHopsDataSingle(payload=hop_payload)) if not is_trampoline: amt += route_edge.fee_for_edge(amt) cltv += route_edge.cltv_expiry_delta diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index d6b8227f0..c89ba7f33 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -489,6 +489,12 @@ def _edge_cost( route_edge = private_route_edges.get(short_channel_id, None) if route_edge is None: node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node) + if node_info: + # it's ok if we are missing the node_announcement (node_info) for this node, + # but if we have it, we enforce that they support var_onion_optin + node_features = LnFeatures(node_info.features) + if not node_features.supports(LnFeatures.VAR_ONION_OPT): + return float('inf'), 0 route_edge = RouteEdge.from_channel_policy( channel_policy=channel_policy, short_channel_id=short_channel_id, diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py index ff3aaefbd..8187934a2 100644 --- a/electrum/tests/test_lnrouter.py +++ b/electrum/tests/test_lnrouter.py @@ -298,7 +298,7 @@ def test_liquidity_hints(self): self.assertEqual(1200, liquidity_hints.penalty(node_from, node_to, channel_id, 1_000_000)) @needs_test_with_all_chacha20_implementations - def test_new_onion_packet_legacy(self): + def test_new_onion_packet(self): # test vector from bolt-04 payment_path_pubkeys = [ bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), @@ -310,74 +310,26 @@ def test_new_onion_packet_legacy(self): session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') hops_data = [ - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 0}, - "outgoing_cltv_value": {"outgoing_cltv_value": 0}, - "short_channel_id": {"short_channel_id": bfh('0000000000000000')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 1}, - "outgoing_cltv_value": {"outgoing_cltv_value": 1}, - "short_channel_id": {"short_channel_id": bfh('0101010101010101')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 2}, - "outgoing_cltv_value": {"outgoing_cltv_value": 2}, - "short_channel_id": {"short_channel_id": bfh('0202020202020202')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 3}, - "outgoing_cltv_value": {"outgoing_cltv_value": 3}, - "short_channel_id": {"short_channel_id": bfh('0303030303030303')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 4}, - "outgoing_cltv_value": {"outgoing_cltv_value": 4}, - "short_channel_id": {"short_channel_id": bfh('0404040404040404')}, - }), + OnionHopsDataSingle(), + OnionHopsDataSingle(), + OnionHopsDataSingle(), + OnionHopsDataSingle(), + OnionHopsDataSingle(), ] - packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) - self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71e87f9aab8f6378c6ff744c1f34b393ad28d065b535c1a8668d85d3b34a1b3befd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a1f9e7abc789266cc861cabd95818c0fc8efbdfdc14e3f7c2bc7eb8d6a79ef75ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d014698cf05d742557763d9cb743faeae65dcc79dddaecf27fe5942be5380d15e9a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040a2a2fba158a0d8085926dc2e44f0c88bf487da56e13ef2d5e676a8589881b4869ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565a9f99728426ce2380a9580e2a9442481ceae7679906c30b1a0e21a10f26150e0645ab6edfdab1ce8f8bea7b1dee511c5fd38ac0e702c1c15bb86b52bca1b71e15b96982d262a442024c33ceb7dd8f949063c2e5e613e873250e2f8708bd4e1924abd45f65c2fa5617bfb10ee9e4a42d6b5811acc8029c16274f937dac9e8817c7e579fdb767ffe277f26d413ced06b620ede8362081da21cf67c2ca9d6f15fe5bc05f82f5bb93f8916bad3d63338ca824f3bbc11b57ce94a5fa1bc239533679903d6fec92a8c792fd86e2960188c14f21e399cfd72a50c620e10aefc6249360b463df9a89bf6836f4f26359207b765578e5ed76ae9f31b1cc48324be576e3d8e44d217445dba466f9b6293fdf05448584eb64f61e02903f834518622b7d4732471c6e0e22e22d1f45e31f0509eab39cdea5980a492a1da2aaac55a98a01216cd4bfe7abaa682af0fbff2dfed030ba28f1285df750e4d3477190dd193f8643b61d8ac1c427d590badb1f61a05d480908fbdc7c6f0502dd0c4abb51d725e92f95da2a8facb79881a844e2026911adcc659d1fb20a2fce63787c8bb0d9f6789c4b231c76da81c3f0718eb7156565a081d2be6b4170c0e0bcebddd459f53db2590c974bca0d705c055dee8c629bf854a5d58edc85228499ec6dde80cce4c8910b81b1e9e8b0f43bd39c8d69c3a80672729b7dc952dd9448688b6bd06afc2d2819cda80b66c57b52ccf7ac1a86601410d18d0c732f69de792e0894a9541684ef174de766fd4ce55efea8f53812867be6a391ac865802dbc26d93959df327ec2667c7256aa5a1d3c45a69a6158f285d6c97c3b8eedb09527848500517995a9eae4cd911df531544c77f5a9a2f22313e3eb72ca7a07dba243476bc926992e0d1e58b4a2fc8c7b01e0cad726237933ea319bad7537d39f3ed635d1e6c1d29e97b3d2160a09e30ee2b65ac5bce00996a73c008bcf351cecb97b6833b6d121dcf4644260b2946ea204732ac9954b228f0beaa15071930fd9583dfc466d12b5f0eeeba6dcf23d5ce8ae62ee5796359d97a4a15955c778d868d0ef9991d9f2833b5bb66119c5f8b396fd108baed7906cbb3cc376d13551caed97fece6f42a4c908ee279f1127fda1dd3ee77d8de0a6f3c135fa3f1cffe38591b6738dc97b55f0acc52be9753ce53e64d7e497bb00ca6123758df3b68fad99e35c04389f7514a8e36039f541598a417275e77869989782325a15b5342ac5011ff07af698584b476b35d941a4981eac590a07a092bb50342da5d3341f901aa07964a8d02b623c7b106dd0ae50bfa007a22d46c8772fa55558176602946cb1d11ea5460db7586fb89c6d3bcd3ab6dd20df4a4db63d2e7d52380800ad812b8640887e027e946df96488b47fbc4a4fadaa8beda4abe446fafea5403fae2ef'), + hops_data[0]._raw_bytes_payload = bfh("1202023a98040205dc06080000000000000001") + hops_data[1]._raw_bytes_payload = bfh("52020236b00402057806080000000000000002fd02013c0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f") + hops_data[2]._raw_bytes_payload = bfh("12020230d4040204e206080000000000000003") + hops_data[3]._raw_bytes_payload = bfh("1202022710040203e806080000000000000004") + hops_data[4]._raw_bytes_payload = bfh("fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a") + packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data) + self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde664725a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b179166ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16eded5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf9939d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb233acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916cfa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c38f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d556af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d87744d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef517b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e64e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360bc24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa383758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e2588aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca13277ef65db3fd47ccc2179126aaefb627719f421e20'), packet.to_bytes()) @needs_test_with_all_chacha20_implementations - def test_new_onion_packet_mixed_payloads(self): - # test vector from bolt-04 - payment_path_pubkeys = [ - bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), - bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), - bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'), - bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), - bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'), - ] - session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') - associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') - hops_data = [ - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 0}, - "outgoing_cltv_value": {"outgoing_cltv_value": 0}, - "short_channel_id": {"short_channel_id": bfh('0000000000000000')}, - }), - OnionHopsDataSingle(is_tlv_payload=True), - OnionHopsDataSingle(is_tlv_payload=True), - OnionHopsDataSingle(is_tlv_payload=True), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 4}, - "outgoing_cltv_value": {"outgoing_cltv_value": 4}, - "short_channel_id": {"short_channel_id": bfh('0404040404040404')}, - }), - ] - hops_data[1]._raw_bytes_payload = bfh("0101010101010101000000000000000100000001") - hops_data[2]._raw_bytes_payload = bfh("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") - hops_data[3]._raw_bytes_payload = bfh("0303030303030303000000000000000300000003") - packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) - self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a710f8eaf9ccc768f66bb5dec1f7827f33c43fe2ddd05614c8283aa78e9e7573f87c50f7d61ab590531cf08000178a333a347f8b4072e1cea42da7552402b10765adae3f581408f35ff0a71a34b78b1d8ecae77df96c6404bae9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf057f5995d9731c4bf796fb0e41c885d488dcbc68eb742e27f44310b276edc6f652658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d710ee5c193921909bdd75db331cd9d7581a39fca50814ed8d9d402b86e7f8f6ac2f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa3379834bc2380d58e9d23237821475a1874484783a15d68f47d3dc339f38d9bf925655d5c946778680fd6d1f062f84128895aff09d35d6c92cca63d3f95a9ee8f2a84f383b4d6a087533e65de12fc8dcaf85777736a2088ff4b22462265028695b37e70963c10df8ef2458756c73007dc3e544340927f9e9f5ea4816a9fd9832c311d122e9512739a6b4714bba590e31caa143ce83cb84b36c738c60c3190ff70cd9ac286a9fd2ab619399b68f1f7447be376ce884b5913c8496d01cbf7a44a60b6e6747513f69dc538f340bc1388e0fde5d0c1db50a4dcb9cc0576e0e2474e4853af9623212578d502757ffb2e0e749695ed70f61c116560d0d4154b64dcf3cbf3c91d89fb6dd004dc19588e3479fcc63c394a4f9e8a3b8b961fce8a532304f1337f1a697a1bb14b94d2953f39b73b6a3125d24f27fcd4f60437881185370bde68a5454d816e7a70d4cea582effab9a4f1b730437e35f7a5c4b769c7b72f0346887c1e63576b2f1e2b3706142586883f8cf3a23595cc8e35a52ad290afd8d2f8bcd5b4c1b891583a4159af7110ecde092079209c6ec46d2bda60b04c519bb8bc6dffb5c87f310814ef2f3003671b3c90ddf5d0173a70504c2280d31f17c061f4bb12a978122c8a2a618bb7d1edcf14f84bf0fa181798b826a254fca8b6d7c81e0beb01bd77f6461be3c8647301d02b04753b0771105986aa0cbc13f7718d64e1b3437e8eef1d319359914a7932548c91570ef3ea741083ca5be5ff43c6d9444d29df06f76ec3dc936e3d180f4b6d0fbc495487c7d44d7c8fe4a70d5ff1461d0d9593f3f898c919c363fa18341ce9dae54f898ccf3fe792136682272941563387263c51b2a2f32363b804672cc158c9230472b554090a661aa81525d11876eefdcc45442249e61e07284592f1606491de5c0324d3af4be035d7ede75b957e879e9770cdde2e1bbc1ef75d45fe555f1ff6ac296a2f648eeee59c7c08260226ea333c285bcf37a9bbfa57ba2ab8083c4be6fc2ebe279537d22da96a07392908cf22b233337a74fe5c603b51712b43c3ee55010ee3d44dd9ba82bba3145ec358f863e04bbfa53799a7a9216718fd5859da2f0deb77b8e315ad6868fdec9400f45a48e6dc8ddbaeb3'), - packet.to_bytes()) - - @needs_test_with_all_chacha20_implementations - def test_process_onion_packet_mixed_payloads(self): + def test_process_onion_packet(self): # this test is not from bolt-04, but is based on the one there; - # here the TLV payloads are actually sane... + # here the TLV payloads are all known types. This allows testing + # decoding the onion and parsing hops_data into known TLV dicts. payment_path_pubkeys = [ bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), @@ -395,89 +347,29 @@ def test_process_onion_packet_mixed_payloads(self): session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') hops_data = [ - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 0}, - "outgoing_cltv_value": {"outgoing_cltv_value": 0}, - "short_channel_id": {"short_channel_id": bfh('0000000000000000')}, - }), - OnionHopsDataSingle(is_tlv_payload=True, payload={ - "amt_to_forward": {"amt_to_forward": 1}, - "outgoing_cltv_value": {"outgoing_cltv_value": 1}, - "short_channel_id": {"short_channel_id": bfh('0101010101010101')}, - }), - OnionHopsDataSingle(is_tlv_payload=True, payload={ - "amt_to_forward": {"amt_to_forward": 2}, - "outgoing_cltv_value": {"outgoing_cltv_value": 2}, - "short_channel_id": {"short_channel_id": bfh('0202020202020202')}, - }), - OnionHopsDataSingle(is_tlv_payload=True, payload={ - "amt_to_forward": {"amt_to_forward": 3}, - "outgoing_cltv_value": {"outgoing_cltv_value": 3}, - "short_channel_id": {"short_channel_id": bfh('0303030303030303')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 4}, - "outgoing_cltv_value": {"outgoing_cltv_value": 4}, - "short_channel_id": {"short_channel_id": bfh('0404040404040404')}, - }), - ] - packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) - self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71bde5adfa90b337f34616d8673d09dd055937273045566ce537ffbe3f9d1f263dc10c7d61ae590536c609010079a232a247922a5395359a63dfbefb85f40317e23254f3023f7d4a98f746c9ab06647645ce55c67308e3c77dc87a1caeac51b03b23c60f05e536e1d757c8c1093e34accfc4f97b5920f6dd2069d5b9ddbb384c3ac575e999a92a4434470ab0aa040c4c3cace3162a405842a88be783e64fad54bd6727c23fc446b7ec0dc3eec5a03eb6c70ec2784911c9e6d274322ec465f0972eb8e771b149f319582ba64dbc2b8e56a3ea79002801c09354f1541cf79bd1dccf5d6bd6b6bacc87a0f24ce497e14e8037e5a79fb4d9ca63fe47f17765963e8f17468a5eaec19a6cca2bfc4e4a366fea3a92112a945856be55e45197ecbab523025e7589529c30cc8addc8fa39d23ef64fa2e51a219c3bd4d3c484832f8e5af16bc46cdba0403991f4fc1b74beef857acf15fefed82ac8678ca66d26262c681beddfdb485aa498813b1a6c5833f1339c1a35244ab76baa0ccaf681ec1f54004e387063335648a77b65d90dde74f1c4b0a729ca25fa53256f7db6d35818b4e5910ba78ec69cf3646bf248ef46cf9cc33062662de2afe4dcf005951b85fd759429fa1ae490b78b14132ccb791232a6c680f03634c0136817f51bf9603a0dba405e7b347830be4327fceccd4734456842b82cf6275393b279bc6ac93d743e00a2d6042960089f70c782ce554b9f73eeeefeea50df7f6f80de1c4e869a7b502f9a5df30d1175402fa780812d35c6d489a30bb0cea53a1088669a238cccf416ecb37f8d8e6ea1327b64979d48e937db69a44a902923a75113685a4aca4a8d9c62b388b48d9c9e2ab9c2df4d529223144de6e16f2dd95a063da79163b3fe006a80263cde4410648f7c3e1f4a7707f82eb0e209002d972c7e57b4ff8ce063fa7b4140f52f569f0cc8793a97a170613efb6b27ba3a0370f8ea74fc0d6aabba54e0ee967abc70e87b580d2aac244236b7752db9d83b159afc1faf6b44b697643235bf59e99f43428caff409d26b9139538865b1f5cf4699f9296088aca461209024ad1dd00e3566e4fde2117b7b3ffced6696b735816a00199890056de86dcbb1b930228143dbf04f07c0eb34370089ea55c43b2c4546cbe1ff0c3a6217d994af9b4225f4b5acb1e3129f5f5b98d381a4692a8561c670b2ee95869f9614e76bb07f623c5194e1c9d26334026f8f5437ec1cde526f914fa094a465f0adcea32b79bfa44d2562536b0d8366da9ee577666c1d5e39615444ca5c900b8199fafac002b8235688eaa0c6887475a913b37d9a4ed43a894ea4576102e5d475ae0b962240ea95fc367b7ac214a4f8682448a9c0d2eea35727bdedc235a975ecc8148a5b03d6291a051dbefe19c8b344d2713c6664dd94ced53c6be39a837fbf1169cca6a12b0a2710f443ba1afeecb51e94236b2a6ed1c2f365b595443b1515de86dcb8c67282807789b47c331cde2fdd721262bef165fa96b7919d11bc5f2022f5affffdd747c7dbe3de8add829a0a8913519fdf7dba4e8a7a25456d2d559746d39ea6ffa31c7b904792fb734bba30f2e1adf7457a994513a1807785fe7b22bf419d1f407f8e2db8b22c0512b078c0cfdfd599e6c4a9d0cc624b9e24b87f30541c3248cd6643df15d251775cc457df4ea6b4e4c5990d87541028c6f0eb28502db1c11a92797168d0b68cb0a0d345b3a3ad05fc4016862f403c64670c41a2c0c6d4e384f5f7da6a204a24530a51182fd7164f120e74a78decb1ab6cda6b9cfc68ac0a35f7a57e750ead65a8e0429cc16e733b9e4feaea25d06c1a4768'), - packet.to_bytes()) - for i, privkey in enumerate(payment_path_privkeys): - processed_packet = process_onion_packet(packet, associated_data, privkey) - self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes()) - packet = processed_packet.next_packet - - @needs_test_with_all_chacha20_implementations - def test_process_onion_packet_legacy(self): - # this test is not from bolt-04, but is based on the one there; - # except here we have the privkeys for these pubkeys - payment_path_pubkeys = [ - bfh('03d75c0ee70f68d73d7d13aeb6261d8ace11416800860c7e59407afe4e2e2d42bb'), - bfh('03960a0b830c7b8e76de745b819f252c62508346196b916f5e813cdb0773283cce'), - bfh('0385620e0a571cbc3552620f8bf1bdcdab2d1a4a59c36fa10b8249114ccbdda40d'), - bfh('02ee242cf6c38b7285f0152c33804ff777f5c51fd352ca8132e845e2cf23b3d8ba'), - bfh('025c585fd2e174bf8245b2b4a119e52a417688904228643ea3edaa1728bf2a258e'), - ] - payment_path_privkeys = [ - bfh('3463a278617b3dd83f79bda7f97673f12609c54386e1f0d2b67b1c6354fda14e'), - bfh('7e1255fddb52db1729fc3ceb21a46f95b8d9fe94cc83425e936a6c5223bb679d'), - bfh('c7ce8c1462c311eec24dff9e2532ac6241e50ae57e7d1833af21942136972f23'), - bfh('3d885f374d79a5e777459b083f7818cdc9493e5c4994ac9c7b843de8b70be661'), - bfh('dd72ab44729527b7942e195e7a835e7c71f9c0ff61844eb21274d9c26166a8f8'), - ] - session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') - associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') - hops_data = [ - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 0}, - "outgoing_cltv_value": {"outgoing_cltv_value": 0}, - "short_channel_id": {"short_channel_id": bfh('0000000000000000')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 1}, - "outgoing_cltv_value": {"outgoing_cltv_value": 1}, - "short_channel_id": {"short_channel_id": bfh('0101010101010101')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 2}, - "outgoing_cltv_value": {"outgoing_cltv_value": 2}, - "short_channel_id": {"short_channel_id": bfh('0202020202020202')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 3}, - "outgoing_cltv_value": {"outgoing_cltv_value": 3}, - "short_channel_id": {"short_channel_id": bfh('0303030303030303')}, - }), - OnionHopsDataSingle(is_tlv_payload=False, payload={ - "amt_to_forward": {"amt_to_forward": 4}, - "outgoing_cltv_value": {"outgoing_cltv_value": 4}, - "short_channel_id": {"short_channel_id": bfh('0404040404040404')}, - }), + OnionHopsDataSingle(payload={ + 'amt_to_forward': {'amt_to_forward': 15000}, + 'outgoing_cltv_value': {'outgoing_cltv_value': 1500}, + 'short_channel_id': {'short_channel_id': bfh('0000000000000001')}}), + OnionHopsDataSingle(payload={ + 'amt_to_forward': {'amt_to_forward': 14000}, + 'outgoing_cltv_value': {'outgoing_cltv_value': 1400}, + 'short_channel_id': {'short_channel_id': bfh('0000000000000002')}}), + OnionHopsDataSingle(payload={ + 'amt_to_forward': {'amt_to_forward': 12500}, + 'outgoing_cltv_value': {'outgoing_cltv_value': 1250}, + 'short_channel_id': {'short_channel_id': bfh('0000000000000003')}}), + OnionHopsDataSingle(payload={ + 'amt_to_forward': {'amt_to_forward': 10000}, + 'outgoing_cltv_value': {'outgoing_cltv_value': 1000}, + 'short_channel_id': {'short_channel_id': bfh('0000000000000004')}}), + OnionHopsDataSingle(payload={ + 'amt_to_forward': {'amt_to_forward': 10000}, + 'outgoing_cltv_value': {'outgoing_cltv_value': 1000}, + 'payment_data': {'payment_secret': bfh('24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704'), 'total_msat': 10000}}), ] - packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) - self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a049d041ffc5be934b8575c6ff4371f2f88d4edfd73e445534d3f6ae15b64b0d8308390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec65b251704041f7c4cc40a0016ba172fbf805ec59132a65a4c7eb1f41337931c5df0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0229c3be444ee50b40686bc1306838b93c65771de1b6ca05dace1ff9814a6e58b2dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32fb5521cce7cd668f7434c909c1be027d8595d85893e5f612c49a93eeeed80a78bab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b94056bf7a21730afc3f254ffa41ca140a5d87ff470f536d08619e8004d50de2fe5954d6aa4a00570da397ba15ae9ea4d7d1f136256a9093f0a787a36cbb3520b6a3cf4d1b13b16bf399c4b0326da1382a90bd79cf92f4808c8c84eaa50a8ccf44acbde0e35b2e6b72858c8446d6a05f3ba70fb4adc70af27cea9bd1dc1ea35fb3cc236b8b9b69b614903db339b22ad5dc2ddda7ac65fd7de24e60b7dbba7aafc9d26c0f9fcb03f1bb85dfc21762f862620651db52ea703ae60aa7e07febf11caa95c4245a4b37eb9c233e1ab1604fb85849e7f49cb9f7c681c4d91b7c320eb4b89b9c6bcb636ceadda59f6ed47aa5b1ea0a946ea98f6ad2614e79e0d4ef96d6d65903adb0479709e03008bbdf3355dd87df7d68965fdf1ad5c68b6dc2761b96b10f8eb4c0329a646bf38cf4702002e61565231b4ac7a9cde63d23f7b24c9d42797b3c434558d71ed8bf9fcab2c2aee3e8b38c19f9edc3ad3dfe9ebba7387ce4764f97ed1c1a83552dff8315546761479a6f929c39bcca0891d4a967d1b77fa80feed6ae74ac82ed5fb7be225c3f2b0ebdc652afc2255c47bc318ac645bbf19c0819ff527ff6708a78e19c8ca3dc8087035e10d5ac976e84b71148586c8a5a7b26ed11b5b401ce7bb2ac532207eaa24d2f53aaa8024607da764d807c91489e82fcad04e6b8992a507119367f576ee5ffe6807d5723d60234d4c3f94adce0acfed9dba535ca375446a4e9b500b74ad2a66e1c6b0fc38933f282d3a4a877bceceeca52b46e731ca51a9534224a883c4a45587f973f73a22069a4154b1da03d307d8575c821bef0eef87165b9a1bbf902ecfca82ddd805d10fbb7147b496f6772f01e9bf542b00288f3a6efab32590c1f34535ece03a0587ca187d27a98d4c9aa7c044794baa43a81abbe307f51d0bda6e7b4cf62c4be553b176321777e7fd483d6cec16df137293aaf3ad53608e1c7831368675bb9608db04d5c859e7714edab3d2389837fa071f0795adfabc51507b1adbadc7f83e80bd4e4eb9ed1a89c9e0a6dc16f38d55181d5666b02150651961aab34faef97d80fa4e1960864dfec3b687fd4eadf7aa6c709cb4698ae86ae112f386f33731d996b9d41926a2e820c6ba483a61674a4bae03af37e872ffdc0a9a8a034327af17e13e9e7ac619c9188c2a5c12a6ebf887721455c0e2822e67a621ed49f1f50dfc38b71c29d0224954e84ced086c80de552cca3a14adbe43035901225bafc3db3b672c780e4fa12b59221f93690527efc16a28e7c63d1a99fc881f023b03a157076a7e999a715ed37521adb483e2477d75ba5a55d4abad22b024c5317334b6544f15971591c774d896229e4e668fc1c7958fbd76fa0b152a6f14c95692083badd066b6621367fd73d88ba8d860566e6d55b871d80c68296b80ae8847d'), + packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data=associated_data) + self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f7f3416a5aa36dc7eeb3ec6d421e9615471ab858ba970cd3cceb768b44e692be2f390c0b7fe70122abae84d7801db070dfb1638cd8d263072206dbed0234f6505e21e282abd8587124c572aad8de04610a136d6c71a7648c0ef66f1b3655d8a9eea1f92349132c93befbd6c37dbfc55615814ae09e4cbef721c01b487007811bbbfdc1fc7bd869aeb70eb08b4140ff5f501394b3653ada2a3b36a263535ea421d26818afb278df46abcec093305b715cac22b0b03645f8f4797cf2987b1bf4bfdd9ed8648ed42ed1a831fc36ccd45416a132580281ddac4e7470e4d2afd675baad9282ec6335403a73e1391427e330996c834db93848b4ae29dd975f678b2f5155ad6865ca23190725d4b7238fb44f0e3762dd59091b45c97d45df8164a15d9ca0329ec76f957b0a0e49ae372154620708df5c0fa991f0dd12b6bff1ebaf9e2376bb64bc24713f7c57da569bcd9c43a50c088416564b786a87d1f40936a051a3dbfe023bd867a5e66148b61cdd24a79f8c18682150e55aa6969ce9becf51f7c69e72deafcd0659f6be4f78463eaef8716e56615c77b3fbea8190806359909dcbec13c1592523b3d2985ec3e83d42cb7286a66a22f58704ddf6979ceb6883ab4ad8ac99d30251035189ffd514e03ce1576844513d66965d4adfc2523f4eee0dede229ab96303e31348c72bc0c8c816c666a904e5ccbabadf5a919720438f4a14dbd4a802f8d4b942f0ca8572f59644c9ac1912c8c8efefc4afa7f19e27411d46b7541c55985e28ce5cd7620b335fea51de55fa00ef977e8522181ad19e5e04f93bcfc83a36edd7e96fe48e846f2e54fe7a7090fe8e46ba72123e1cdee0667777c38c4930e50401074d8ab31a9717457fcefaa46323003af553bee2b49ea7f907eb2ff3301463e64a8c53975c853bbdd2956b9001b5ce1562264963fce84201daaf752de6df7ca31291226969c9851d1fc4ea88ca67d38c38587c2cdd8bc4d3f7bdf705497a1e054246f684554b3b8dfac43194f1eadec7f83b711e663b5645bde6d7f8cefb59758303599fed25c3b4d2e4499d439c915910dd283b3e7118320f1c6e7385009fbcb9ae79bab72a85e644182b4dafc0a173241f2ae68ae6a504f17f102da1e91de4548c7f5bc1c107354519077a4e83407f0d6a8f0975b4ac0c2c7b30637a998dda27b56b56245371296b816876b859677bcf3473a07e0f300e788fdd60c51b1626b46050b182457c6d716994847aaef667ca45b2cede550c92d336ff29ce6effd933b875f81381cda6e59e9727e728a58c0b3e74035beeeb639ab7463744322bf40138b81895e9a8e8850c9513782dc7a79f04380c216cb177951d8940d576486b887a232fcd382adcbd639e70af0c1a08bcf1405496606fce4645aef10d769dc0c010a8a433d8cd24d5943843a89cdbc8d16531db027b312ab2c03a7f1fdb7f2bcb128639c49e86705c948137fd42d0080fda4be4e9ee812057c7974acbf0162730d3b647b355ac1a5adbb2993832eba443b7c9b5a0ae1fc00a6c0c2b0b65b9019690565739d6439bf602066a3a9bd9c67b83606de51792d25ae517cbbdf6e1827fa0e8b2b5c6023cbb1e9f0e10b786dc6fa154e282fd9c90b8d46ca685d0f4434760035073c92d131564b6845ef57457488add4f709073bbb41f5f31f8226904875a9fd9e1b7a2901e71426104d7a298a05af0d4ab549fbd69c539ebe64949a9b6088f16e2e4bc827c305cb8d64536b8364dc3d5f7519c3b431faa38b47a958cf0c6dcabf205280693abf747c262f44cd6ffa11b32fc38d4f9c3631d554d8b57389f1390ac65c06357843ee6d9f289bb054ef25de45c5149c090fe6ddcd4095696dcc9a5cfc09c8bdfd5b83a153'), packet.to_bytes()) for i, privkey in enumerate(payment_path_privkeys): processed_packet = process_onion_packet(packet, associated_data, privkey) From eeda06e7518ef242a1b6aedae342963066974c69 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 30 Jun 2023 11:44:13 +0200 Subject: [PATCH 1030/1143] payment_identifier: fix error path for bip70 --- electrum/payment_identifier.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index a2619339a..ddbe3dddc 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -4,7 +4,7 @@ import re from decimal import Decimal, InvalidOperation from enum import IntEnum -from typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING +from typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING, Tuple from . import bitcoin from .contacts import AliasNotFoundException @@ -204,7 +204,7 @@ class FieldsForGUI(NamedTuple): description: Optional[str] validated: Optional[bool] comment: Optional[int] - amount_range: Optional[tuple[int, int]] + amount_range: Optional[Tuple[int, int]] class PaymentIdentifier(Logger): @@ -291,7 +291,7 @@ def is_amount_locked(self): if self._type == PaymentIdentifierType.BIP21: return bool(self.bip21.get('amount')) elif self._type == PaymentIdentifierType.BIP70: - return True # TODO always given? + return not self.need_resolve() # always fixed after resolve? elif self._type == PaymentIdentifierType.BOLT11: lnaddr = lndecode(self.bolt11) return bool(lnaddr.amount) @@ -442,9 +442,13 @@ async def _do_resolve(self, *, on_finished=None): self.set_state(PaymentIdentifierState.NOT_FOUND) elif self.bip70: from . import paymentrequest - data = await paymentrequest.get_payment_request(self.bip70) - self.bip70_data = data - self.set_state(PaymentIdentifierState.MERCHANT_NOTIFY) + pr = await paymentrequest.get_payment_request(self.bip70) + if not pr.error: + self.bip70_data = pr + self.set_state(PaymentIdentifierState.MERCHANT_NOTIFY) + else: + self.error = pr.error + self.set_state(PaymentIdentifierState.ERROR) elif self.lnurl: data = await request_lnurl(self.lnurl) self.lnurl_data = data From 0d29733419ae589e099829d65353920b0c58aa20 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Jun 2023 10:11:16 +0000 Subject: [PATCH 1031/1143] qml/qedaemon.py: don't use daemon._wallets field directly it's more robust to use the public methods --- electrum/gui/qml/qedaemon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py index 4d7c52d9d..5c38e0b12 100644 --- a/electrum/gui/qml/qedaemon.py +++ b/electrum/gui/qml/qedaemon.py @@ -180,7 +180,7 @@ def loadWallet(self, path=None, password=None): if not password: password = self._password - wallet_already_open = self._path in self.daemon._wallets + wallet_already_open = self.daemon.get_wallet(self._path) is not None if not wallet_already_open: # pre-checks, let walletdb trigger any necessary user interactions @@ -234,7 +234,8 @@ def load_wallet_task(): @pyqtSlot(str) def _on_backend_wallet_loaded(self, password = None): self._logger.debug('_on_backend_wallet_loaded') - wallet = self.daemon._wallets[self._path] + wallet = self.daemon.get_wallet(self._path) + assert wallet is not None self._current_wallet = QEWallet.getInstanceFor(wallet) self.availableWallets.updateWallet(self._path) self._current_wallet.password = password if password else None From d65aa3369f373ba6340248b0dbd59e394ee56026 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 30 Jun 2023 10:49:26 +0000 Subject: [PATCH 1032/1143] daemon: split standardize_path from daemon._wallets keying - standardize_path is made more lenient: it no longer calls os.path.realpath, as that was causing issues on Windows with some mounted drives - daemon._wallets is still keyed on the old strict standardize_path, but filesystem operations in WalletStorage use the new lenient standardize_path. - daemon._wallets is strict to forbid opening the same logical file twice concurrently - fs operations however work better on the non-resolved paths, so they use those closes https://github.com/spesmilo/electrum/issues/8495 --- electrum/daemon.py | 29 ++++++++++++++++++++--------- electrum/util.py | 5 +++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 8b6485801..086f10773 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -402,7 +402,7 @@ def __init__( if not self.config.NETWORK_OFFLINE: self.network = Network(config, daemon=self) self.fx = FxThread(config=config) - # path -> wallet; make sure path is standardized. + # wallet_key -> wallet self._wallets = {} # type: Dict[str, Abstract_Wallet] self._wallet_lock = threading.RLock() daemon_jobs = [] @@ -452,6 +452,17 @@ def start_network(self): if self.config.LIGHTNING_USE_GOSSIP: self.network.start_gossip() + @staticmethod + def _wallet_key_from_path(path) -> str: + """This does stricter path standardization than 'standardize_path'. + It is used for keying the _wallets dict, but not for the actual filesystem operations. (see #8495) + """ + path = standardize_path(path) + # also resolve symlinks and windows network mounts/etc: + path = os.path.realpath(path) + path = os.path.normcase(path) + return str(path) + def with_wallet_lock(func): def func_wrapper(self: 'Daemon', *args, **kwargs): with self._wallet_lock: @@ -461,9 +472,9 @@ def func_wrapper(self: 'Daemon', *args, **kwargs): @with_wallet_lock def load_wallet(self, path, password, *, manual_upgrades=True) -> Optional[Abstract_Wallet]: path = standardize_path(path) + wallet_key = self._wallet_key_from_path(path) # wizard will be launched if we return - if path in self._wallets: - wallet = self._wallets[path] + if wallet := self._wallets.get(wallet_key): return wallet wallet = self._load_wallet(path, password, manual_upgrades=manual_upgrades, config=self.config) if wallet is None: @@ -503,13 +514,13 @@ def _load_wallet( @with_wallet_lock def add_wallet(self, wallet: Abstract_Wallet) -> None: path = wallet.storage.path - path = standardize_path(path) - self._wallets[path] = wallet + wallet_key = self._wallet_key_from_path(path) + self._wallets[wallet_key] = wallet run_hook('daemon_wallet_loaded', self, wallet) def get_wallet(self, path: str) -> Optional[Abstract_Wallet]: - path = standardize_path(path) - return self._wallets.get(path) + wallet_key = self._wallet_key_from_path(path) + return self._wallets.get(wallet_key) @with_wallet_lock def get_wallets(self) -> Dict[str, Abstract_Wallet]: @@ -531,8 +542,8 @@ def stop_wallet(self, path: str) -> bool: @with_wallet_lock async def _stop_wallet(self, path: str) -> bool: """Returns True iff a wallet was found.""" - path = standardize_path(path) - wallet = self._wallets.pop(path, None) + wallet_key = self._wallet_key_from_path(path) + wallet = self._wallets.pop(wallet_key, None) if not wallet: return False await wallet.stop() diff --git a/electrum/util.py b/electrum/util.py index c31ba5711..8312f1bd0 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -522,12 +522,13 @@ def assert_file_in_datadir_available(path, config_path): def standardize_path(path): + # note: os.path.realpath() is not used, as on Windows it can return non-working paths (see #8495). + # This means that we don't resolve symlinks! return os.path.normcase( - os.path.realpath( os.path.abspath( os.path.expanduser( path - )))) + ))) def get_new_wallet_name(wallet_folder: str) -> str: From b6010aad0fe3559fd9f4831bff4efb7be189949f Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 3 Jul 2023 13:59:57 +0200 Subject: [PATCH 1033/1143] paytoedit: promote to QWidget and encapsulate QLineEdit vs QTextEdit juggling --- electrum/gui/qt/paytoedit.py | 181 ++++++++++++++++++++++++----------- electrum/gui/qt/send_tab.py | 4 +- 2 files changed, 126 insertions(+), 59 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 418cc9cd6..320e75999 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -24,19 +24,16 @@ # SOFTWARE. from functools import partial -from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import Qt, QTimer, QSize from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtGui import QFontMetrics, QFont -from PyQt5.QtWidgets import QApplication, QTextEdit, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QTextEdit, QWidget, QLineEdit, QStackedLayout, QSizePolicy -from electrum.i18n import _ from electrum.payment_identifier import PaymentIdentifier from electrum.logging import Logger -from .qrtextedit import ScanQRTextEdit -from .completion_text_edit import CompletionTextEdit from . import util from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent, ColorScheme @@ -54,10 +51,15 @@ class InvalidPaymentIdentifier(Exception): class ResizingTextEdit(QTextEdit): + textReallyChanged = pyqtSignal() + resized = pyqtSignal() + def __init__(self): QTextEdit.__init__(self) + self._text = '' + self.setAcceptRichText(False) + self.textChanged.connect(self.on_text_changed) document = self.document() - document.contentsChanged.connect(self.update_size) fontMetrics = QFontMetrics(document.defaultFont()) self.fontSpacing = fontMetrics.lineSpacing() margins = self.contentsMargins() @@ -67,49 +69,81 @@ def __init__(self): self.verticalMargins += documentMargin * 2 self.heightMin = self.fontSpacing + self.verticalMargins self.heightMax = (self.fontSpacing * 10) + self.verticalMargins - self.single_line = True self.update_size() + def on_text_changed(self): + # QTextEdit emits spurious textChanged events + if self.toPlainText() != self._text: + self._text = self.toPlainText() + self.textReallyChanged.emit() + self.update_size() + def update_size(self): docLineCount = self.document().lineCount() - docHeight = max(1 if self.single_line else 3, docLineCount) * self.fontSpacing + docHeight = max(3, docLineCount) * self.fontSpacing h = docHeight + self.verticalMargins h = min(max(h, self.heightMin), self.heightMax) self.setMinimumHeight(int(h)) self.setMaximumHeight(int(h)) - if self.single_line: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) - else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) - self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) + self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth) + self.resized.emit() + + def sizeHint(self) -> QSize: + return QSize(0, self.minimumHeight()) -class PayToEdit(QObject, Logger, GenericInputHandler): +class PayToEdit(QWidget, Logger, GenericInputHandler): paymentIdentifierChanged = pyqtSignal() + textChanged = pyqtSignal() def __init__(self, send_tab: 'SendTab'): - QObject.__init__(self, parent=send_tab) + QWidget.__init__(self, parent=send_tab) Logger.__init__(self) GenericInputHandler.__init__(self) + self._text = '' + self._layout = QStackedLayout() + self.setLayout(self._layout) + + def text_edit_changed(): + text = self.text_edit.toPlainText() + if self._text != text: + # sync and emit + self._text = text + self.line_edit.setText(text) + self.textChanged.emit() + + def text_edit_resized(): + self.update_height() + + def line_edit_changed(): + text = self.line_edit.text() + if self._text != text: + # sync and emit + self._text = text + self.text_edit.setPlainText(text) + self.textChanged.emit() + + self.line_edit = QLineEdit() + self.line_edit.textChanged.connect(line_edit_changed) self.text_edit = ResizingTextEdit() - self.text_edit.textChanged.connect(self._handle_text_change) + self.text_edit.textReallyChanged.connect(text_edit_changed) + self.text_edit.resized.connect(text_edit_resized) + + self.textChanged.connect(self._handle_text_change) + + self._layout.addWidget(self.line_edit) + self._layout.addWidget(self.text_edit) + + self.multiline = False + self._is_paytomany = False self.text_edit.setFont(QFont(MONOSPACE_FONT)) self.send_tab = send_tab self.config = send_tab.config - self.app = QApplication.instance() - - self.is_multiline = False - self.payto_scriptpubkey = None # type: Optional[bytes] - self.previous_payto = '' - # editor methods - self.setStyleSheet = self.text_edit.setStyleSheet - self.setText = self.text_edit.setText - self.setFocus = self.text_edit.setFocus - self.setToolTip = self.text_edit.setToolTip + # button handlers self.on_qr_from_camera_input_btn = partial( self.input_qr_from_camera, @@ -141,24 +175,46 @@ def __init__(self, send_tab: 'SendTab'): self.payment_identifier = None - def set_text(self, text: str): - self.text_edit.setText(text) + @property + def multiline(self): + return self._multiline + + @multiline.setter + def multiline(self, b: bool) -> None: + if b is None: + return + self._multiline = b + self._layout.setCurrentWidget(self.text_edit if b else self.line_edit) + self.update_height() + + def update_height(self) -> None: + h = self._layout.currentWidget().sizeHint().height() + self.setMaximumHeight(h) + + def setText(self, text: str) -> None: + if self._text != text: + self.line_edit.setText(text) + self.text_edit.setText(text) + + def setFocus(self, reason=None) -> None: + if self.multiline: + self.text_edit.setFocus(reason) + else: + self.line_edit.setFocus(reason) - def update_editor(self): - if self.text_edit.toPlainText() != self.payment_identifier.text: - self.text_edit.setText(self.payment_identifier.text) - self.text_edit.single_line = not self.payment_identifier.is_multiline() - self.text_edit.update_size() + def setToolTip(self, tt: str) -> None: + self.line_edit.setToolTip(tt) + self.text_edit.setToolTip(tt) '''set payment identifier only if valid, else exception''' - def try_payment_identifier(self, text): + def try_payment_identifier(self, text) -> None: text = text.strip() pi = PaymentIdentifier(self.send_tab.wallet, text) if not pi.is_valid(): raise InvalidPaymentIdentifier('Invalid payment identifier') self.set_payment_identifier(text) - def set_payment_identifier(self, text): + def set_payment_identifier(self, text) -> None: text = text.strip() if self.payment_identifier and self.payment_identifier.text == text: # no change. @@ -167,60 +223,69 @@ def set_payment_identifier(self, text): self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text) # toggle to multiline if payment identifier is a multiline - self.is_multiline = self.payment_identifier.is_multiline() - if self.is_multiline and not self._is_paytomany: + if self.payment_identifier.is_multiline() and not self._is_paytomany: self.set_paytomany(True) - # if payment identifier gets set externally, we want to update the text_edit + # if payment identifier gets set externally, we want to update the edit control # Note: this triggers the change handler, but we shortcut if it's the same payment identifier - self.update_editor() + self.setText(text) self.paymentIdentifierChanged.emit() def set_paytomany(self, b): self._is_paytomany = b - self.text_edit.single_line = not self._is_paytomany - self.text_edit.update_size() + self.multiline = b self.send_tab.paytomany_menu.setChecked(b) - def toggle_paytomany(self): + def toggle_paytomany(self) -> None: self.set_paytomany(not self._is_paytomany) def is_paytomany(self): return self._is_paytomany - def setFrozen(self, b): + def setReadOnly(self, b: bool) -> None: + self.line_edit.setReadOnly(b) self.text_edit.setReadOnly(b) - self.text_edit.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') + + def isReadOnly(self): + return self.line_edit.isReadOnly() + + def setStyleSheet(self, stylesheet: str) -> None: + self.line_edit.setStyleSheet(stylesheet) + self.text_edit.setStyleSheet(stylesheet) + + def setFrozen(self, b) -> None: + self.setReadOnly(b) + self.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') def isFrozen(self): - return self.text_edit.isReadOnly() + return self.isReadOnly() - def do_clear(self): - self.is_multiline = False + def do_clear(self) -> None: self.set_paytomany(False) - self.text_edit.setText('') + self.setText('') + self.setToolTip('') self.payment_identifier = None - def setGreen(self): + def setGreen(self) -> None: self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True)) - def setExpired(self): + def setExpired(self) -> None: self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) - def _handle_text_change(self): + def _handle_text_change(self) -> None: if self.isFrozen(): # if editor is frozen, we ignore text changes as they might not be a payment identifier # but a user friendly representation. return # pushback timer if timer active or PI needs resolving - pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText()) + pi = PaymentIdentifier(self.send_tab.wallet, self._text) if not pi.is_valid() or pi.need_resolve() or self.edit_timer.isActive(): self.edit_timer.start() else: - self.set_payment_identifier(self.text_edit.toPlainText()) + self.set_payment_identifier(self._text) - def _on_edit_timer(self): + def _on_edit_timer(self) -> None: if not self.isFrozen(): - self.set_payment_identifier(self.text_edit.toPlainText()) + self.set_payment_identifier(self._text) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 670111281..94838c248 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -73,7 +73,7 @@ def __init__(self, window: 'ElectrumWindow'): "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 0, 0) - grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4) + grid.addWidget(self.payto_e, 0, 1, 1, 4) #completer = QCompleter() #completer.setCaseSensitivity(False) @@ -339,6 +339,8 @@ def do_clear(self): for w in [self.save_button, self.send_button]: w.setEnabled(False) self.window.update_status() + self.paytomany_menu.setChecked(self.payto_e.multiline) + run_hook('do_clear', self) def prepare_for_send_tab_network_lookup(self): From 5f8b8ce97e636ae63fbf8cf525125e8e856d890a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 4 Jul 2023 10:55:00 +0200 Subject: [PATCH 1034/1143] qml: show channel backup and explanatory message before local force close, and let user confirm before doing the close operation also show message dialog after close succeeded instead of just closing the channel close dialog --- .../gui/qml/components/CloseChannelDialog.qml | 57 +++++++++++++++++-- .../gui/qml/components/GenericShareDialog.qml | 2 + electrum/gui/qml/qechanneldetails.py | 16 +++++- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index 529ce1c0e..6c7279baa 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -19,6 +19,7 @@ ElDialog { iconSource: Qt.resolvedUrl('../../icons/lightning_disconnected.png') property bool _closing: false + property string _closing_method closePolicy: Popup.NoAutoClose @@ -163,12 +164,50 @@ ElDialog { icon.source: '../../icons/closebutton.png' enabled: !_closing onClicked: { - _closing = true - channeldetails.closeChannel(closetypegroup.checkedButton.closetype) + if (closetypegroup.checkedButton.closetype == 'local_force') { + showBackupThenConfirmClose() + } else { + doCloseChannel() + } } - } + } + function showBackupThenConfirmClose() { + var sharedialog = app.genericShareDialog.createObject(app, { + title: qsTr('Save channel backup and force close'), + text_qr: channeldetails.channelBackup(), + text_help: channeldetails.messageForceCloseBackup, + helpTextIconStyle: InfoTextArea.IconStyle.Warn + }) + sharedialog.closed.connect(function() { + confirmCloseChannel() + }) + sharedialog.open() + } + + function confirmCloseChannel() { + var confirmdialog = app.messageDialog.createObject(app, { + title: qsTr('Confirm force close?'), + yesno: true + }) + confirmdialog.accepted.connect(function() { + doCloseChannel() + }) + confirmdialog.open() + } + + function doCloseChannel() { + _closing = true + _closing_method = closetypegroup.checkedButton.closetype + channeldetails.closeChannel(_closing_method) + } + + function showCloseMessage(text) { + var msgdialog = app.messageDialog.createObject(app, { + text: text + }) + msgdialog.open() } ChannelDetails { @@ -176,7 +215,10 @@ ElDialog { wallet: Daemon.currentWallet channelid: dialog.channelid - onChannelChanged : { + onChannelChanged: { + if (!channeldetails.canClose) + return + // init default choice if (channeldetails.canCoopClose) closetypeCoop.checked = true @@ -186,6 +228,13 @@ ElDialog { onChannelCloseSuccess: { _closing = false + if (_closing_method == 'local_force') { + showCloseMessage(qsTr('Channel closed. You may need to wait at least %1 blocks, because of CSV delays').arg(channeldetails.toSelfDelay)) + } else if (_closing_method == 'remote_force') { + showCloseMessage(qsTr('Request sent')) + } else if (_closing_method == 'cooperative') { + showCloseMessage(qsTr('Channel closed')) + } dialog.close() } diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index 8a9b8e49c..aa85a12e2 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -12,6 +12,7 @@ ElDialog { property string text_qr // if text_qr is undefined text will be used property string text_help + property int helpTextIconStyle: InfoTextArea.IconStyle.Info title: '' @@ -65,6 +66,7 @@ ElDialog { InfoTextArea { Layout.leftMargin: constants.paddingMedium Layout.rightMargin: constants.paddingMedium + iconStyle: helpTextIconStyle visible: dialog.text_help text: dialog.text_help Layout.fillWidth: true diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 233a67731..48585e9a9 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -13,6 +13,7 @@ from .qetypes import QEAmount from .util import QtEventListener, qt_event_listener, event_listener + class QEChannelDetails(QObject, QtEventListener): _logger = get_logger(__name__) @@ -171,13 +172,26 @@ def canDelete(self): return self._channel.can_be_deleted() @pyqtProperty(str, notify=channelChanged) - def messageForceClose(self, notify=channelChanged): + def messageForceClose(self): return messages.MSG_REQUEST_FORCE_CLOSE.strip() + @pyqtProperty(str, notify=channelChanged) + def messageForceCloseBackup(self): + return ' '.join([ + _('If you force-close this channel, the funds you have in it will not be available for {} blocks.').format(self.toSelfDelay), + _('During that time, funds will not be recoverable from your seed, and may be lost if you lose your device.'), + _('To prevent that, please save this channel backup.'), + _('It may be imported in another wallet with the same seed.') + ]) + @pyqtProperty(bool, notify=channelChanged) def isBackup(self): return self._channel.is_backup() + @pyqtProperty(int, notify=channelChanged) + def toSelfDelay(self): + return self._channel.config[REMOTE].to_self_delay + @pyqtSlot() def freezeForSending(self): lnworker = self._channel.lnworker From c4e8869c1a624b8d6a2e51258794b2925620a272 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 4 Jul 2023 14:05:10 +0200 Subject: [PATCH 1035/1143] qml: add PIN auth to close channel operation. --- .../gui/qml/components/CloseChannelDialog.qml | 41 +++++++------------ electrum/gui/qml/qechanneldetails.py | 25 +++++++++-- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/electrum/gui/qml/components/CloseChannelDialog.qml b/electrum/gui/qml/components/CloseChannelDialog.qml index 6c7279baa..924173469 100644 --- a/electrum/gui/qml/components/CloseChannelDialog.qml +++ b/electrum/gui/qml/components/CloseChannelDialog.qml @@ -18,7 +18,6 @@ ElDialog { title: qsTr('Close Channel') iconSource: Qt.resolvedUrl('../../icons/lightning_disconnected.png') - property bool _closing: false property string _closing_method closePolicy: Popup.NoAutoClose @@ -114,21 +113,21 @@ ElDialog { id: closetypeCoop ButtonGroup.group: closetypegroup property string closetype: 'cooperative' - enabled: !_closing && channeldetails.canCoopClose + enabled: !channeldetails.isClosing && channeldetails.canCoopClose text: qsTr('Cooperative close') } RadioButton { id: closetypeRemoteForce ButtonGroup.group: closetypegroup property string closetype: 'remote_force' - enabled: !_closing && channeldetails.canForceClose + enabled: !channeldetails.isClosing && channeldetails.canForceClose text: qsTr('Request Force-close') } RadioButton { id: closetypeLocalForce ButtonGroup.group: closetypegroup property string closetype: 'local_force' - enabled: !_closing && channeldetails.canForceClose && !channeldetails.isBackup + enabled: !channeldetails.isClosing && channeldetails.canForceClose && !channeldetails.isBackup text: qsTr('Local Force-close') } } @@ -141,17 +140,17 @@ ElDialog { id: errorText Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width - visible: !_closing && errorText.text + visible: !channeldetails.isClosing && errorText.text iconStyle: InfoTextArea.IconStyle.Error } Label { Layout.alignment: Qt.AlignHCenter text: qsTr('Closing...') - visible: _closing + visible: channeldetails.isClosing } BusyIndicator { Layout.alignment: Qt.AlignHCenter - visible: _closing + visible: channeldetails.isClosing } } } @@ -162,10 +161,10 @@ ElDialog { Layout.fillWidth: true text: qsTr('Close channel') icon.source: '../../icons/closebutton.png' - enabled: !_closing + enabled: !channeldetails.isClosing onClicked: { if (closetypegroup.checkedButton.closetype == 'local_force') { - showBackupThenConfirmClose() + showBackupThenClose() } else { doCloseChannel() } @@ -173,7 +172,7 @@ ElDialog { } } - function showBackupThenConfirmClose() { + function showBackupThenClose() { var sharedialog = app.genericShareDialog.createObject(app, { title: qsTr('Save channel backup and force close'), text_qr: channeldetails.channelBackup(), @@ -181,24 +180,12 @@ ElDialog { helpTextIconStyle: InfoTextArea.IconStyle.Warn }) sharedialog.closed.connect(function() { - confirmCloseChannel() - }) - sharedialog.open() - } - - function confirmCloseChannel() { - var confirmdialog = app.messageDialog.createObject(app, { - title: qsTr('Confirm force close?'), - yesno: true - }) - confirmdialog.accepted.connect(function() { doCloseChannel() }) - confirmdialog.open() + sharedialog.open() } function doCloseChannel() { - _closing = true _closing_method = closetypegroup.checkedButton.closetype channeldetails.closeChannel(_closing_method) } @@ -215,8 +202,12 @@ ElDialog { wallet: Daemon.currentWallet channelid: dialog.channelid + onAuthRequired: { + app.handleAuthRequired(channeldetails, method, authMessage) + } + onChannelChanged: { - if (!channeldetails.canClose) + if (!channeldetails.canClose || channeldetails.isClosing) return // init default choice @@ -227,7 +218,6 @@ ElDialog { } onChannelCloseSuccess: { - _closing = false if (_closing_method == 'local_force') { showCloseMessage(qsTr('Channel closed. You may need to wait at least %1 blocks, because of CSV delays').arg(channeldetails.toSelfDelay)) } else if (_closing_method == 'remote_force') { @@ -239,7 +229,6 @@ ElDialog { } onChannelCloseFailed: { - _closing = false errorText.text = message } } diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 48585e9a9..af929dd0a 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -1,4 +1,3 @@ -import asyncio import threading from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS @@ -9,12 +8,13 @@ from electrum.lnutil import LOCAL, REMOTE from electrum.lnchannel import ChanCloseOption, ChannelState +from .auth import AuthMixin, auth_protect from .qewallet import QEWallet from .qetypes import QEAmount -from .util import QtEventListener, qt_event_listener, event_listener +from .util import QtEventListener, event_listener -class QEChannelDetails(QObject, QtEventListener): +class QEChannelDetails(AuthMixin, QObject, QtEventListener): _logger = get_logger(__name__) class State: # subset, only ones we currently need in UI @@ -26,6 +26,7 @@ class State: # subset, only ones we currently need in UI channelChanged = pyqtSignal() channelCloseSuccess = pyqtSignal() channelCloseFailed = pyqtSignal([str], arguments=['message']) + isClosingChanged = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) @@ -39,6 +40,7 @@ def __init__(self, parent=None): self._remote_capacity = QEAmount() self._can_receive = QEAmount() self._can_send = QEAmount() + self._is_closing = False self.register_callbacks() self.destroyed.connect(lambda: self.on_destroy()) @@ -192,6 +194,12 @@ def isBackup(self): def toSelfDelay(self): return self._channel.config[REMOTE].to_self_delay + @pyqtProperty(bool, notify=isClosingChanged) + def isClosing(self): + # Note: isClosing only applies to a closing action started by this instance, not + # whether the channel is closing + return self._is_closing + @pyqtSlot() def freezeForSending(self): lnworker = self._channel.lnworker @@ -212,19 +220,30 @@ def freezeForReceiving(self): @pyqtSlot(str) def closeChannel(self, closetype): + self.do_close_channel(closetype) + + @auth_protect(message=_('Close Lightning channel?')) + def do_close_channel(self, closetype): channel_id = self._channel.channel_id + def do_close(): try: + self._is_closing = True + self.isClosingChanged.emit() if closetype == 'remote_force': self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.request_force_close(channel_id)) elif closetype == 'local_force': self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.force_close_channel(channel_id)) else: self._wallet.wallet.network.run_from_another_thread(self._wallet.wallet.lnworker.close_channel(channel_id)) + self._logger.debug('Channel close successful') self.channelCloseSuccess.emit() except Exception as e: self._logger.exception("Could not close channel: " + repr(e)) self.channelCloseFailed.emit(_('Could not close channel: ') + repr(e)) + finally: + self._is_closing = False + self.isClosingChanged.emit() threading.Thread(target=do_close, daemon=True).start() From be0ef5f961e5dcc7be2394728679674cdf81fb5d Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 7 Jul 2023 12:33:55 +0200 Subject: [PATCH 1036/1143] trezor: allow PIN of length 50 for T1 firmware >=1.10.0 and TT firmware >=2.4.0 (closes #8526) --- electrum/plugins/trezor/clientbase.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 30ee4fdd0..8683c8b42 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -287,8 +287,15 @@ def get_pin(self, code=None): pin = self.handler.get_pin(msg.format(self.device), show_strength=show_strength) if not pin: raise Cancelled - if len(pin) > 9: - self.handler.show_error(_('The PIN cannot be longer than 9 characters.')) + # check PIN length. Depends on model and firmware version + # https://github.com/trezor/trezor-firmware/issues/1167 + limit = 9 + if self.features.model == "1" and (1, 10, 0) <= self.client.version: + limit = 50 + elif self.features.model == "2" and (2, 4, 0) <= self.client.version: + limit = 50 + if len(pin) > limit: + self.handler.show_error(_('The PIN cannot be longer than {} characters.').format(limit)) raise Cancelled return pin From 44f83b78e2530a2952adffcd388e6a162976278b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 7 Jul 2023 14:00:27 +0200 Subject: [PATCH 1037/1143] trezor: model T is 'T', not '2' --- electrum/plugins/trezor/clientbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index 8683c8b42..b4822ada0 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -292,7 +292,7 @@ def get_pin(self, code=None): limit = 9 if self.features.model == "1" and (1, 10, 0) <= self.client.version: limit = 50 - elif self.features.model == "2" and (2, 4, 0) <= self.client.version: + elif self.features.model == "T" and (2, 4, 0) <= self.client.version: limit = 50 if len(pin) > limit: self.handler.show_error(_('The PIN cannot be longer than {} characters.').format(limit)) From 9b41bcf992104ae2e8f2a547bad8037f5f1b9e31 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 7 Jul 2023 17:05:08 +0200 Subject: [PATCH 1038/1143] setFrozen: use light blue, black text is difficult to read on a blue background --- electrum/gui/qt/amountedit.py | 2 +- electrum/gui/qt/paytoedit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/amountedit.py b/electrum/gui/qt/amountedit.py index be1d20f86..4e3073d08 100644 --- a/electrum/gui/qt/amountedit.py +++ b/electrum/gui/qt/amountedit.py @@ -21,7 +21,7 @@ class FreezableLineEdit(QLineEdit): def setFrozen(self, b): self.setReadOnly(b) - self.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') + self.setStyleSheet(ColorScheme.LIGHTBLUE.as_stylesheet(True) if b else '') self.frozen.emit() def isFrozen(self): diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 320e75999..3fb8f659f 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -256,7 +256,7 @@ def setStyleSheet(self, stylesheet: str) -> None: def setFrozen(self, b) -> None: self.setReadOnly(b) - self.setStyleSheet(ColorScheme.BLUE.as_stylesheet(True) if b else '') + self.setStyleSheet(ColorScheme.LIGHTBLUE.as_stylesheet(True) if b else '') def isFrozen(self): return self.isReadOnly() From db6779bf048bba6402c956b8a8be75cd6c7f4478 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 7 Jul 2023 20:48:29 +0200 Subject: [PATCH 1039/1143] qt: show send tab if payment identifier is passed on the cmdline --- electrum/gui/qt/__init__.py | 1 + run_electrum | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 7891d1f21..f09af5368 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -393,6 +393,7 @@ def start_new_window( window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive) window.activateWindow() if uri: + window.show_send_tab() window.send_tab.set_payment_identifier(uri) return window diff --git a/run_electrum b/run_electrum index 46aca8a67..413e7b4b8 100755 --- a/run_electrum +++ b/run_electrum @@ -365,12 +365,6 @@ def main(): if not config_options.get('verbosity'): warnings.simplefilter('ignore', DeprecationWarning) - # check if we received a valid payment identifier - uri = config_options.get('url') - if uri and not PaymentIdentifier(None, uri).is_valid(): - print_stderr('unknown command:', uri) - sys.exit(1) - config = SimpleConfig(config_options) cmdname = config.get('cmd') @@ -398,6 +392,12 @@ def main(): elif config.get('signet'): constants.set_signet() + # check if we received a valid payment identifier + uri = config_options.get('url') + if uri and not PaymentIdentifier(None, uri).is_valid(): + print_stderr('unknown command:', uri) + sys.exit(1) + if cmdname == 'daemon' and config.get("detach"): # detect lockfile. # This is not as good as get_file_descriptor, but that would require the asyncio loop From 7f766f6dfbc551e02a91f2c9b3efaf2abbc49061 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 7 Jul 2023 22:54:46 +0200 Subject: [PATCH 1040/1143] payment_identifiers: also match local contacts --- electrum/contacts.py | 11 +++++++++++ electrum/payment_identifier.py | 23 ++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index fd941bfc8..febfcd57d 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -101,6 +101,17 @@ def resolve(self, k): } raise AliasNotFoundException("Invalid Bitcoin address or alias", k) + def by_name(self, name): + for k in self.keys(): + _type, addr = self[k] + if addr.casefold() == name.casefold(): + return { + 'name': addr, + 'type': _type, + 'address': k + } + return None + def fetch_openalias(self, config): self.alias_info = None alias = config.OPENALIAS_ID diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index ddbe3dddc..939a7fd2a 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -380,6 +380,18 @@ def parse(self, text): self._type = PaymentIdentifierType.SPK self.spk = scriptpubkey self.set_state(PaymentIdentifierState.AVAILABLE) + elif contact := self.contacts.by_name(text): + if contact['type'] == 'address': + self._type = PaymentIdentifierType.BIP21 + self.bip21 = { + 'address': contact['address'], + 'label': contact['name'] + } + self.set_state(PaymentIdentifierState.AVAILABLE) + elif contact['type'] == 'openalias': + self._type = PaymentIdentifierType.EMAILLIKE + self.emaillike = contact['address'] + self.set_state(PaymentIdentifierState.NEED_RESOLVE) elif re.match(RE_EMAIL, text): self._type = PaymentIdentifierType.EMAILLIKE self.emaillike = text @@ -681,13 +693,14 @@ def get_fields_for_GUI(self) -> FieldsForGUI: pass elif self.bip21: - recipient = self.bip21.get('address') - amount = self.bip21.get('amount') label = self.bip21.get('label') + address = self.bip21.get('address') + recipient = f'{label} <{address}>' if label else address + amount = self.bip21.get('amount') description = self.bip21.get('message') - # use label as description (not BIP21 compliant) - if label and not description: - description = label + # TODO: use label as description? (not BIP21 compliant) + # if label and not description: + # description = label return FieldsForGUI(recipient=recipient, amount=amount, description=description, comment=comment, validated=validated, amount_range=amount_range) From f980bd97b5029ada1d796de5b12172b12dfe1926 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 8 Jul 2023 12:16:43 +0200 Subject: [PATCH 1041/1143] payment_identifier: factor out bip21 functions to bip21.py to break cyclic dependencies, parse bolt11 only once, store invoice internally instead of bolt11 string add is_onchain method to indicate if payment identifier can be paid onchain --- electrum/bip21.py | 127 ++++++++++++++++++++++ electrum/gui/kivy/main_window.py | 2 +- electrum/gui/kivy/uix/screens.py | 3 +- electrum/gui/qml/qeapp.py | 2 +- electrum/gui/qml/qeinvoice.py | 8 +- electrum/gui/qt/main_window.py | 3 +- electrum/invoices.py | 2 +- electrum/payment_identifier.py | 175 ++++++------------------------- electrum/tests/test_util.py | 2 +- electrum/transaction.py | 3 +- electrum/wallet.py | 3 +- 11 files changed, 174 insertions(+), 156 deletions(-) create mode 100644 electrum/bip21.py diff --git a/electrum/bip21.py b/electrum/bip21.py new file mode 100644 index 000000000..bcf6bd361 --- /dev/null +++ b/electrum/bip21.py @@ -0,0 +1,127 @@ +import urllib +import re +from decimal import Decimal +from typing import Optional + +from . import bitcoin +from .util import format_satoshis_plain +from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC +from .lnaddr import lndecode, LnDecodeException + +# note: when checking against these, use .lower() to support case-insensitivity +BITCOIN_BIP21_URI_SCHEME = 'bitcoin' +LIGHTNING_URI_SCHEME = 'lightning' + + +class InvalidBitcoinURI(Exception): + pass + + +def parse_bip21_URI(uri: str) -> dict: + """Raises InvalidBitcoinURI on malformed URI.""" + + if not isinstance(uri, str): + raise InvalidBitcoinURI(f"expected string, not {repr(uri)}") + + if ':' not in uri: + if not bitcoin.is_address(uri): + raise InvalidBitcoinURI("Not a bitcoin address") + return {'address': uri} + + u = urllib.parse.urlparse(uri) + if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME: + raise InvalidBitcoinURI("Not a bitcoin URI") + address = u.path + + # python for android fails to parse query + if address.find('?') > 0: + address, query = u.path.split('?') + pq = urllib.parse.parse_qs(query) + else: + pq = urllib.parse.parse_qs(u.query) + + for k, v in pq.items(): + if len(v) != 1: + raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}') + + out = {k: v[0] for k, v in pq.items()} + if address: + if not bitcoin.is_address(address): + raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}") + out['address'] = address + if 'amount' in out: + am = out['amount'] + try: + m = re.match(r'([0-9.]+)X([0-9])', am) + if m: + k = int(m.group(2)) - 8 + amount = Decimal(m.group(1)) * pow(Decimal(10), k) + else: + amount = Decimal(am) * COIN + if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: + raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC") + out['amount'] = int(amount) + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e + if 'message' in out: + out['message'] = out['message'] + out['memo'] = out['message'] + if 'time' in out: + try: + out['time'] = int(out['time']) + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e + if 'exp' in out: + try: + out['exp'] = int(out['exp']) + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e + if 'sig' in out: + try: + out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex() + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e + if 'lightning' in out: + try: + lnaddr = lndecode(out['lightning']) + except LnDecodeException as e: + raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e + amount_sat = out.get('amount') + if amount_sat: + # allow small leeway due to msat precision + if abs(amount_sat - int(lnaddr.get_amount_sat())) > 1: + raise InvalidBitcoinURI("Inconsistent lightning field in bip21: amount") + address = out.get('address') + ln_fallback_addr = lnaddr.get_fallback_address() + if address and ln_fallback_addr: + if ln_fallback_addr != address: + raise InvalidBitcoinURI("Inconsistent lightning field in bip21: address") + + return out + + +def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], + *, extra_query_params: Optional[dict] = None) -> str: + if not bitcoin.is_address(addr): + return "" + if extra_query_params is None: + extra_query_params = {} + query = [] + if amount_sat: + query.append('amount=%s' % format_satoshis_plain(amount_sat)) + if message: + query.append('message=%s' % urllib.parse.quote(message)) + for k, v in extra_query_params.items(): + if not isinstance(k, str) or k != urllib.parse.quote(k): + raise Exception(f"illegal key for URI: {repr(k)}") + v = urllib.parse.quote(v) + query.append(f"{k}={v}") + p = urllib.parse.ParseResult( + scheme=BITCOIN_BIP21_URI_SCHEME, + netloc='', + path=addr, + params='', + query='&'.join(query), + fragment='' + ) + return str(urllib.parse.urlunparse(p)) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index c3bcba6a7..590e7b0fb 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -87,7 +87,7 @@ from electrum.util import NoDynamicFeeEstimates, NotEnoughFunds, UserFacingException -from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME +from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog from .uix.dialogs.lightning_channels import LightningChannelsDialog, SwapDialog diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 73690c5af..d5191b97a 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -19,7 +19,8 @@ from electrum import lnutil from electrum.transaction import tx_from_any, PartialTxOutput from electrum.util import TxMinedInfo, InvoiceError, format_time, parse_max_spend -from electrum.payment_identifier import parse_bip21_URI, BITCOIN_BIP21_URI_SCHEME, maybe_extract_lightning_payment_identifier, InvalidBitcoinURI +from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME, parse_bip21_URI, InvalidBitcoinURI +from electrum.payment_identifier import maybe_extract_lightning_payment_identifier from electrum.lnaddr import lndecode, LnInvoiceException from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data from electrum.logging import Logger diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index 24f861434..bb2245c4f 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -16,7 +16,7 @@ from electrum import version, constants from electrum.i18n import _ from electrum.logging import Logger, get_logger -from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME +from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue from electrum.network import Network diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index e9e3a51b0..171959dbd 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -16,9 +16,9 @@ from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl from electrum.bitcoin import COIN from electrum.paymentrequest import PaymentRequest -from electrum.payment_identifier import (parse_bip21_URI, InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, +from electrum.payment_identifier import (maybe_extract_lightning_payment_identifier, PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType) - +from electrum.bip21 import parse_bip21_URI, InvalidBitcoinURI from .qetypes import QEAmount from .qewallet import QEWallet from .util import status_update_timer_interval, QtEventListener, event_listener @@ -526,7 +526,7 @@ def _update_from_payment_identifier(self): self.validationSuccess.emit() return elif self._pi.type == PaymentIdentifierType.BOLT11: - lninvoice = Invoice.from_bech32(self._pi.bolt11) + lninvoice = self._pi.bolt11 if not self._wallet.wallet.has_lightning() and not lninvoice.get_address(): self.validationError.emit('no_lightning', _('Detected valid Lightning invoice, but Lightning not enabled for wallet and no fallback address found.')) @@ -539,7 +539,7 @@ def _update_from_payment_identifier(self): self.validationSuccess.emit() elif self._pi.type == PaymentIdentifierType.BIP21: if self._wallet.wallet.has_lightning() and self._wallet.wallet.lnworker.channels and self._pi.bolt11: - lninvoice = Invoice.from_bech32(self._pi.bolt11) + lninvoice = self._pi.bolt11 self.setValidLightningInvoice(lninvoice) self.validationSuccess.emit() else: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 9b9ff53d1..cf70c2f83 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -57,7 +57,8 @@ from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword, UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, AddTransactionException, os_chmod) -from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, PaymentIdentifier +from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME +from electrum.payment_identifier import PaymentIdentifier from electrum.invoices import PR_PAID, Invoice from electrum.transaction import (Transaction, PartialTxInput, PartialTransaction, PartialTxOutput) diff --git a/electrum/invoices.py b/electrum/invoices.py index 61a90b781..d5bf7bfdc 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -7,7 +7,7 @@ from .json_db import StoredObject, stored_in from .i18n import _ from .util import age, InvoiceError, format_satoshis -from .payment_identifier import create_bip21_uri +from .bip21 import create_bip21_uri from .lnutil import hex_to_bytes from .lnaddr import lndecode, LnAddr from . import constants diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 939a7fd2a..9b7719d64 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -9,14 +9,16 @@ from . import bitcoin from .contacts import AliasNotFoundException from .i18n import _ +from .invoices import Invoice from .logging import Logger -from .util import parse_max_spend, format_satoshis_plain +from .util import parse_max_spend, format_satoshis_plain, InvoiceError from .util import get_asyncio_loop, log_exceptions from .transaction import PartialTxOutput from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, lightning_address_to_url from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, opcodes, construct_script from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures +from .bip21 import parse_bip21_URI, InvalidBitcoinURI, LIGHTNING_URI_SCHEME, BITCOIN_BIP21_URI_SCHEME if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -34,125 +36,6 @@ def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: return None -# note: when checking against these, use .lower() to support case-insensitivity -BITCOIN_BIP21_URI_SCHEME = 'bitcoin' -LIGHTNING_URI_SCHEME = 'lightning' - - -class InvalidBitcoinURI(Exception): - pass - - -def parse_bip21_URI(uri: str) -> dict: - """Raises InvalidBitcoinURI on malformed URI.""" - - if not isinstance(uri, str): - raise InvalidBitcoinURI(f"expected string, not {repr(uri)}") - - if ':' not in uri: - if not bitcoin.is_address(uri): - raise InvalidBitcoinURI("Not a bitcoin address") - return {'address': uri} - - u = urllib.parse.urlparse(uri) - if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME: - raise InvalidBitcoinURI("Not a bitcoin URI") - address = u.path - - # python for android fails to parse query - if address.find('?') > 0: - address, query = u.path.split('?') - pq = urllib.parse.parse_qs(query) - else: - pq = urllib.parse.parse_qs(u.query) - - for k, v in pq.items(): - if len(v) != 1: - raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}') - - out = {k: v[0] for k, v in pq.items()} - if address: - if not bitcoin.is_address(address): - raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}") - out['address'] = address - if 'amount' in out: - am = out['amount'] - try: - m = re.match(r'([0-9.]+)X([0-9])', am) - if m: - k = int(m.group(2)) - 8 - amount = Decimal(m.group(1)) * pow(Decimal(10), k) - else: - amount = Decimal(am) * COIN - if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: - raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC") - out['amount'] = int(amount) - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e - if 'message' in out: - out['message'] = out['message'] - out['memo'] = out['message'] - if 'time' in out: - try: - out['time'] = int(out['time']) - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e - if 'exp' in out: - try: - out['exp'] = int(out['exp']) - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e - if 'sig' in out: - try: - out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex() - except Exception as e: - raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e - if 'lightning' in out: - try: - lnaddr = lndecode(out['lightning']) - except LnDecodeException as e: - raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e - amount_sat = out.get('amount') - if amount_sat: - # allow small leeway due to msat precision - if abs(amount_sat - int(lnaddr.get_amount_sat())) > 1: - raise InvalidBitcoinURI("Inconsistent lightning field in bip21: amount") - address = out.get('address') - ln_fallback_addr = lnaddr.get_fallback_address() - if address and ln_fallback_addr: - if ln_fallback_addr != address: - raise InvalidBitcoinURI("Inconsistent lightning field in bip21: address") - - return out - - -def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], - *, extra_query_params: Optional[dict] = None) -> str: - if not bitcoin.is_address(addr): - return "" - if extra_query_params is None: - extra_query_params = {} - query = [] - if amount_sat: - query.append('amount=%s' % format_satoshis_plain(amount_sat)) - if message: - query.append('message=%s' % urllib.parse.quote(message)) - for k, v in extra_query_params.items(): - if not isinstance(k, str) or k != urllib.parse.quote(k): - raise Exception(f"illegal key for URI: {repr(k)}") - v = urllib.parse.quote(v) - query.append(f"{k}={v}") - p = urllib.parse.ParseResult( - scheme=BITCOIN_BIP21_URI_SCHEME, - netloc='', - path=addr, - params='', - query='&'.join(query), - fragment='' - ) - return str(urllib.parse.urlunparse(p)) - - def is_uri(data: str) -> bool: data = data.lower() if (data.startswith(LIGHTNING_URI_SCHEME + ":") or @@ -279,7 +162,14 @@ def is_available(self): return self._state in [PaymentIdentifierState.AVAILABLE] def is_lightning(self): - return self.lnurl or self.bolt11 + return bool(self.lnurl) or bool(self.bolt11) + + def is_onchain(self): + if self._type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE, PaymentIdentifierType.BIP70, + PaymentIdentifierType.OPENALIAS]: + return True + if self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNADDR]: + return bool(self.bolt11) and bool(self.bolt11.get_address()) def is_multiline(self): return bool(self.multiline_outputs) @@ -293,8 +183,7 @@ def is_amount_locked(self): elif self._type == PaymentIdentifierType.BIP70: return not self.need_resolve() # always fixed after resolve? elif self._type == PaymentIdentifierType.BOLT11: - lnaddr = lndecode(self.bolt11) - return bool(lnaddr.amount) + return bool(self.bolt11.get_amount_sat()) elif self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]: # amount limits known after resolve, might be specific amount or locked to range if self.need_resolve(): @@ -339,16 +228,12 @@ def parse(self, text): else: self._type = PaymentIdentifierType.BOLT11 try: - lndecode(invoice_or_lnurl) - except LnInvoiceException as e: - self.error = _("Error parsing Lightning invoice") + f":\n{e}" - self.set_state(PaymentIdentifierState.INVALID) - return - except IncompatibleOrInsaneFeatures as e: - self.error = _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}" + self.bolt11 = Invoice.from_bech32(invoice_or_lnurl) + except InvoiceError as e: + self.error = self._get_error_from_invoiceerror(e) self.set_state(PaymentIdentifierState.INVALID) + self.logger.debug(f'Exception cause {e.args!r}') return - self.bolt11 = invoice_or_lnurl self.set_state(PaymentIdentifierState.AVAILABLE) elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): try: @@ -643,6 +528,16 @@ def parse_address(self, line): assert bitcoin.is_address(address) return address + def _get_error_from_invoiceerror(self, e: 'InvoiceError') -> str: + error = _("Error parsing Lightning invoice") + f":\n{e!r}" + if e.args and len(e.args): + arg = e.args[0] + if isinstance(arg, LnInvoiceException): + error = _("Error parsing Lightning invoice") + f":\n{e}" + elif isinstance(arg, IncompatibleOrInsaneFeatures): + error = _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}" + return error + def get_fields_for_GUI(self) -> FieldsForGUI: recipient = None amount = None @@ -662,8 +557,8 @@ def get_fields_for_GUI(self) -> FieldsForGUI: self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' 'security check, DNSSEC, and thus may not be correct.').format(key) - elif self.bolt11 and self.wallet.has_lightning(): - recipient, amount, description = self._get_bolt11_fields(self.bolt11) + elif self.bolt11: + recipient, amount, description = self._get_bolt11_fields() elif self.lnurl and self.lnurl_data: domain = urllib.parse.urlparse(self.lnurl).netloc @@ -705,9 +600,8 @@ def get_fields_for_GUI(self) -> FieldsForGUI: return FieldsForGUI(recipient=recipient, amount=amount, description=description, comment=comment, validated=validated, amount_range=amount_range) - def _get_bolt11_fields(self, bolt11_invoice): - """Parse ln invoice, and prepare the send tab for it.""" - lnaddr = lndecode(bolt11_invoice) # + def _get_bolt11_fields(self): + lnaddr = self.bolt11._lnaddr # TODO: improve access to lnaddr pubkey = lnaddr.pubkey.serialize().hex() for k, v in lnaddr.tags: if k == 'd': @@ -740,20 +634,17 @@ def has_expired(self): if self.bip70: return self.bip70_data.has_expired() elif self.bolt11: - lnaddr = lndecode(self.bolt11) - return lnaddr.is_expired() + return self.bolt11.has_expired() elif self.bip21: expires = self.bip21.get('exp') + self.bip21.get('time') if self.bip21.get('exp') else 0 return bool(expires) and expires < time.time() return False def get_invoice(self, amount_sat, message): - from .invoices import Invoice if self.is_lightning(): - invoice_str = self.bolt11 - if not invoice_str: + invoice = self.bolt11 + if not invoice: return - invoice = Invoice.from_bech32(invoice_str) if invoice.amount_msat is None: invoice.amount_msat = int(amount_sat * 1000) return invoice diff --git a/electrum/tests/test_util.py b/electrum/tests/test_util.py index 6ff020710..1767bc8bf 100644 --- a/electrum/tests/test_util.py +++ b/electrum/tests/test_util.py @@ -5,7 +5,7 @@ from electrum.util import (format_satoshis, format_fee_satoshis, is_hash256_str, chunks, is_ip_address, list_enabled_bits, format_satoshis_plain, is_private_netaddress, is_hex_str, is_integer, is_non_negative_integer, is_int_or_float, is_non_negative_int_or_float) -from electrum.payment_identifier import parse_bip21_URI, InvalidBitcoinURI +from electrum.bip21 import parse_bip21_URI, InvalidBitcoinURI from . import ElectrumTestCase, as_testnet diff --git a/electrum/transaction.py b/electrum/transaction.py index 09fdf7ce4..248642905 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -42,8 +42,7 @@ from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node -from .util import profiler, to_bytes, bfh, chunks, is_hex_str -from .payment_identifier import parse_max_spend +from .util import profiler, to_bytes, bfh, chunks, is_hex_str, parse_max_spend from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, diff --git a/electrum/wallet.py b/electrum/wallet.py index 4376d8f77..65475b251 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -58,7 +58,6 @@ WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex) -from .payment_identifier import create_bip21_uri, parse_max_spend from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE from .bitcoin import COIN, TYPE_ADDRESS from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold @@ -66,7 +65,7 @@ from . import keystore from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric, CannotDerivePubkey) -from .util import multisig_type +from .util import multisig_type, parse_max_spend from .storage import StorageEncryptionVersion, WalletStorage from .wallet_db import WalletDB from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 From ae8c4f1281f972151e50f067390f8aa39601cdf7 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sat, 8 Jul 2023 12:19:37 +0200 Subject: [PATCH 1042/1143] payment_identifier: check if payment identifier is usable and enable/disable Send/Pay buttons --- electrum/gui/qt/send_tab.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 94838c248..2dfe8be09 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -426,8 +426,11 @@ def update_fields(self): else: self.amount_e.setToolTip('') - self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error()) - self.save_button.setEnabled(not pi.is_error() and pi.type not in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR]) + pi_unusable = pi.is_error() or (not self.wallet.has_lightning() and not pi.is_onchain()) + + self.send_button.setEnabled(not pi_unusable and bool(self.amount_e.get_amount()) and not pi.has_expired()) + self.save_button.setEnabled(not pi_unusable and pi.type not in [PaymentIdentifierType.LNURLP, + PaymentIdentifierType.LNADDR]) def _handle_payment_identifier(self): self.update_fields() From 6bacd65a80274907db9ac4ed80276ca07955cdd7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 9 Jul 2023 10:05:31 +0200 Subject: [PATCH 1043/1143] payment_identifter: add FIXME --- electrum/payment_identifier.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 9b7719d64..c601889a2 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -641,6 +641,8 @@ def has_expired(self): return False def get_invoice(self, amount_sat, message): + # FIXME: this should not be a PI method + # ideally, PI should not have a reference to wallet. if self.is_lightning(): invoice = self.bolt11 if not invoice: From 4b29a4689015e1124ddd21fd1c9a94d5d3fb36dc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 9 Jul 2023 10:06:46 +0200 Subject: [PATCH 1044/1143] lnpeer: fix logging of 'will fullfill htlc' --- electrum/lnpeer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index a49b627b2..c0532800c 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1833,7 +1833,8 @@ def log_fail_reason(reason: str): if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat): log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}") raise exc_incorrect_or_unknown_pd - self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") + if preimage: + self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") self.lnworker.set_request_status(htlc.payment_hash, PR_PAID) return preimage, None From 5f2fee5184b7e641236cd7838f272df39587f9dc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sun, 9 Jul 2023 11:55:06 +0200 Subject: [PATCH 1045/1143] qml: strip whitespace of data pasted/scanned before processing --- electrum/gui/qml/components/SendDialog.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml index a887b4e41..d54d2a385 100644 --- a/electrum/gui/qml/components/SendDialog.qml +++ b/electrum/gui/qml/components/SendDialog.qml @@ -24,6 +24,7 @@ ElDialog { } function dispatch(data) { + data = data.trim() if (bitcoin.isRawTx(data)) { txFound(data) } else if (Daemon.currentWallet.isValidChannelBackup(data)) { From 016b5eb7434fe99fa12bcbdf7e875cc612824a28 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 16 May 2023 12:15:38 +0200 Subject: [PATCH 1046/1143] qml: show private key in address details --- .../gui/qml/components/AddressDetails.qml | 59 ++++++++++++++++++- electrum/gui/qml/qeaddressdetails.py | 20 ++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index eb5d815fa..b72fff689 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -172,13 +172,67 @@ Pane { } Label { + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingSmall + visible: !Daemon.currentWallet.isWatchOnly + text: qsTr('Private key') + color: Material.accentColor + } + + TextHighlightPane { + Layout.columnSpan: 2 + Layout.fillWidth: true + visible: !Daemon.currentWallet.isWatchOnly + RowLayout { + width: parent.width + Label { + id: privateKeyText + Layout.fillWidth: true + visible: addressdetails.privkey + text: addressdetails.privkey + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + font.family: FixedFont + } + Label { + id: showPrivateKeyText + Layout.fillWidth: true + visible: !addressdetails.privkey + horizontalAlignment: Text.AlignHCenter + text: qsTr('Tap to show private key') + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeLarge + } + ToolButton { + icon.source: '../../icons/share.png' + visible: addressdetails.privkey + onClicked: { + var dialog = app.genericShareDialog.createObject(root, { + title: qsTr('Private key'), + text: addressdetails.privkey + }) + dialog.open() + } + } + + MouseArea { + anchors.fill: parent + enabled: !addressdetails.privkey + onClicked: addressdetails.requestShowPrivateKey() + } + } + } + + Label { + Layout.topMargin: constants.paddingSmall text: qsTr('Script type') color: Material.accentColor } Label { - text: addressdetails.scriptType + Layout.topMargin: constants.paddingSmall Layout.fillWidth: true + text: addressdetails.scriptType } Label { @@ -235,5 +289,8 @@ Pane { address: root.address onFrozenChanged: addressDetailsChanged() onLabelChanged: addressDetailsChanged() + onAuthRequired: { + app.handleAuthRequired(addressdetails, method, authMessage) + } } } diff --git a/electrum/gui/qml/qeaddressdetails.py b/electrum/gui/qml/qeaddressdetails.py index 096ba506f..f09cbb630 100644 --- a/electrum/gui/qml/qeaddressdetails.py +++ b/electrum/gui/qml/qeaddressdetails.py @@ -2,12 +2,13 @@ from electrum.logging import get_logger +from .auth import auth_protect, AuthMixin from .qetransactionlistmodel import QETransactionListModel from .qetypes import QEAmount from .qewallet import QEWallet -class QEAddressDetails(QObject): +class QEAddressDetails(AuthMixin, QObject): _logger = get_logger(__name__) detailsChanged = pyqtSignal() @@ -66,6 +67,10 @@ def balance(self): def pubkeys(self): return self._pubkeys + @pyqtProperty(str, notify=detailsChanged) + def privkey(self): + return self._privkey + @pyqtProperty(str, notify=detailsChanged) def derivationPath(self): return self._derivationPath @@ -108,6 +113,19 @@ def historyModel(self): onchain_domain=[self._address], include_lightning=False) return self._historyModel + @pyqtSlot() + def requestShowPrivateKey(self): + self.retrieve_private_key() + + @auth_protect(method='wallet') + def retrieve_private_key(self): + try: + self._privkey = self._wallet.wallet.export_private_key(self._address, self._wallet.password) + except Exception: + self._privkey = '' + + self.detailsChanged.emit() + def update(self): if self._wallet is None: self._logger.error('wallet undefined') From 2d95c457dd3100bc7b9393b0b3f96af8b273f88c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sun, 9 Jul 2023 00:11:59 +0200 Subject: [PATCH 1047/1143] qml: addressdetails item order, add technical properties header --- .../gui/qml/components/AddressDetails.qml | 110 +++++++++--------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/electrum/gui/qml/components/AddressDetails.qml b/electrum/gui/qml/components/AddressDetails.qml index b72fff689..e779a8e03 100644 --- a/electrum/gui/qml/components/AddressDetails.qml +++ b/electrum/gui/qml/components/AddressDetails.qml @@ -44,10 +44,18 @@ Pane { text: qsTr('Address details') } - Label { - text: qsTr('Address') + RowLayout { Layout.columnSpan: 2 - color: Material.accentColor + Label { + text: qsTr('Address') + color: Material.accentColor + } + + Tag { + visible: addressdetails.isFrozen + text: qsTr('Frozen') + labelcolor: 'white' + } } TextHighlightPane { @@ -76,6 +84,24 @@ Pane { } } + Label { + text: qsTr('Balance') + color: Material.accentColor + } + + FormattedAmount { + amount: addressdetails.balance + } + + Label { + text: qsTr('Transactions') + color: Material.accentColor + } + + Label { + text: addressdetails.numTx + } + Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall @@ -135,6 +161,34 @@ Pane { } } + Heading { + Layout.columnSpan: 2 + text: qsTr('Technical Properties') + } + + Label { + Layout.topMargin: constants.paddingSmall + text: qsTr('Script type') + color: Material.accentColor + } + + Label { + Layout.topMargin: constants.paddingSmall + Layout.fillWidth: true + text: addressdetails.scriptType + } + + Label { + visible: addressdetails.derivationPath + text: qsTr('Derivation path') + color: Material.accentColor + } + + Label { + visible: addressdetails.derivationPath + text: addressdetails.derivationPath + } + Label { Layout.columnSpan: 2 Layout.topMargin: constants.paddingSmall @@ -222,56 +276,6 @@ Pane { } } } - - Label { - Layout.topMargin: constants.paddingSmall - text: qsTr('Script type') - color: Material.accentColor - } - - Label { - Layout.topMargin: constants.paddingSmall - Layout.fillWidth: true - text: addressdetails.scriptType - } - - Label { - text: qsTr('Balance') - color: Material.accentColor - } - - FormattedAmount { - amount: addressdetails.balance - } - - Label { - text: qsTr('Transactions') - color: Material.accentColor - } - - Label { - text: addressdetails.numTx - } - - Label { - visible: addressdetails.derivationPath - text: qsTr('Derivation path') - color: Material.accentColor - } - - Label { - visible: addressdetails.derivationPath - text: addressdetails.derivationPath - } - - Label { - text: qsTr('Frozen') - color: Material.accentColor - } - - Label { - text: addressdetails.isFrozen ? qsTr('Frozen') : qsTr('Not frozen') - } } } From f5a8cc7076d3e9688965a3de7fb635605b10394d Mon Sep 17 00:00:00 2001 From: Toporin Date: Mon, 10 Jul 2023 09:50:35 +0100 Subject: [PATCH 1048/1143] Patch error caused by the method "parse_URI()" being moved and renamed: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aucun périphérique matériel détecté. Pour déclencher un nouveau scan, pressez 'Suivant'. Sur Linux, vous pouvez avoir à ajouter une nouvelle permission à vos règles udev. Message de débogage bitbox02: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading bitbox02 plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") coldcard: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading coldcard plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") digitalbitbox: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading digitalbitbox plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") jade: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading jade plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") keepkey: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading keepkey plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") ledger: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading ledger plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") safe_t: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading safe_t plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") satochip: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading satochip plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") trezor: (error during plugin init) Vous avez peut-être une bibliothèque incompatible. Error loading trezor plugin: ImportError("cannot import name 'parse_URI' from 'electrum.util' (/Users/satochip/Documents/github/electrum-satochip/dist/Electrum.app/Contents/MacOS/electrum/util.pyc)") --- electrum/plugins/hw_wallet/qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index e3b7deac0..c181900fe 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -40,7 +40,8 @@ from electrum.i18n import _ from electrum.logging import Logger -from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled, UserFacingException +from electrum.util import UserCancelled, UserFacingException +from electrum.bip21 import parse_bip21_URI, InvalidBitcoinURI from electrum.plugin import hook, DeviceUnpairableError from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase From 6dd630cf3bb1521db9323876223f1305d4e846ee Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 10 Jul 2023 11:39:16 +0200 Subject: [PATCH 1049/1143] followup f5a8cc7076d3e9688965a3de7fb635605b10394d --- electrum/gui/text.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/gui/text.py b/electrum/gui/text.py index 5fc954c2e..fc6794da3 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -11,7 +11,7 @@ import electrum from electrum.gui import BaseElectrumGui -from electrum import util +from electrum.bip21 import parse_bip21_URI from electrum.util import format_satoshis, format_time from electrum.util import EventListener, event_listener from electrum.bitcoin import is_address, address_to_script, COIN @@ -32,12 +32,14 @@ _ = lambda x:x # i18n + def parse_bip21(text): try: - return util.parse_URI(text) + return parse_bip21_URI(text) except Exception: return + def parse_bolt11(text): from electrum.lnaddr import lndecode try: @@ -46,7 +48,6 @@ def parse_bolt11(text): return - class ElectrumGui(BaseElectrumGui, EventListener): def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): From 0b0d58b289b2acc60efd33147c4c7a91751d3881 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Jul 2023 11:22:50 +0000 Subject: [PATCH 1050/1143] qml: fix "copy" and "share" buttons for channel backup dialogs The QR code was shown but the copy/share buttons did not do anything. --- electrum/gui/qml/components/GenericShareDialog.qml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/GenericShareDialog.qml b/electrum/gui/qml/components/GenericShareDialog.qml index aa85a12e2..423d6819c 100644 --- a/electrum/gui/qml/components/GenericShareDialog.qml +++ b/electrum/gui/qml/components/GenericShareDialog.qml @@ -10,7 +10,11 @@ ElDialog { property string text property string text_qr - // if text_qr is undefined text will be used + // If text is set, it is displayed as a string and also used as data in the QR code shown. + // text_qr can also be set if we want to show different data in the QR code. + // If only text_qr is set, the QR code is shown but the string itself is not, + // however the copy button still exposes the string. + property string text_help property int helpTextIconStyle: InfoTextArea.IconStyle.Info @@ -84,7 +88,7 @@ ElDialog { text: qsTr('Copy') icon.source: '../../icons/copy_bw.png' onClicked: { - AppController.textToClipboard(dialog.text) + AppController.textToClipboard(dialog.text ? dialog.text : dialog.text_qr) toaster.show(this, qsTr('Copied!')) } } @@ -95,7 +99,7 @@ ElDialog { text: qsTr('Share') icon.source: '../../icons/share.png' onClicked: { - AppController.doShare(dialog.text, dialog.title) + AppController.doShare(dialog.text ? dialog.text : dialog.text_qr, dialog.title) } } } From a572b9bf877bab57d22f569ca8cd6182e39b1de5 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 10 Jul 2023 14:21:36 +0200 Subject: [PATCH 1051/1143] lnchannel: add noop get_local_scid_alias for ChannelBackup --- electrum/lnchannel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index f84b79044..2bb70e08e 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -532,6 +532,9 @@ def get_capacity(self): def is_backup(self): return True + def get_local_scid_alias(self, *, create_new_if_needed: bool = False) -> Optional[bytes]: + return None + def get_remote_scid_alias(self) -> Optional[bytes]: return None From 86d79f3ec91f2ffbd738da821abae4a78da1ecd8 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 10 Jul 2023 14:45:36 +0200 Subject: [PATCH 1052/1143] qml: show backup type, as in qt --- electrum/gui/qml/components/ChannelDetails.qml | 15 +++++++++++++++ electrum/gui/qml/qechanneldetails.py | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index 28b00948e..a82cbf42f 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -97,6 +97,21 @@ Pane { text: channeldetails.channelType } + Label { + visible: channeldetails.isBackup + text: qsTr('Backup type') + color: Material.accentColor + } + + Label { + visible: channeldetails.isBackup + text: channeldetails.backupType == 'imported' + ? qsTr('imported') + : channeldetails.backupType == 'on-chain' + ? qsTr('on-chain') + : '?' + } + Label { text: qsTr('Remote node ID') Layout.columnSpan: 2 diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index af929dd0a..80d8851d8 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -190,6 +190,12 @@ def messageForceCloseBackup(self): def isBackup(self): return self._channel.is_backup() + @pyqtProperty(str, notify=channelChanged) + def backupType(self): + if not self.isBackup: + return '' + return 'imported' if self._channel.is_imported else 'on-chain' + @pyqtProperty(int, notify=channelChanged) def toSelfDelay(self): return self._channel.config[REMOTE].to_self_delay From 023e8ff0eb929e147f974dea606c92325f6f15e2 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 10 Jul 2023 18:10:15 +0200 Subject: [PATCH 1053/1143] qml: remember invoice/qr type on ReceiveDialog --- electrum/gui/qml/components/ReceiveDialog.qml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml index 3fa4ec432..8dea47d81 100644 --- a/electrum/gui/qml/components/ReceiveDialog.qml +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -117,7 +117,10 @@ ElDialog { MouseArea { anchors.fill: parent enabled: _bolt11 - onClicked: rootLayout.state = 'bolt11' + onClicked: { + rootLayout.state = 'bolt11' + Config.preferredRequestType = 'bolt11' + } } } Rectangle { @@ -133,7 +136,10 @@ ElDialog { MouseArea { anchors.fill: parent enabled: _bip21uri - onClicked: rootLayout.state = 'bip21uri' + onClicked: { + rootLayout.state = 'bip21uri' + Config.preferredRequestType = 'bip21uri' + } } } Rectangle { @@ -149,7 +155,10 @@ ElDialog { MouseArea { anchors.fill: parent enabled: _address - onClicked: rootLayout.state = 'address' + onClicked: { + rootLayout.state = 'address' + Config.preferredRequestType = 'address' + } } } } From 612a8e64246865d4412e2909d61112a721d7dadd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Jul 2023 17:50:53 +0000 Subject: [PATCH 1054/1143] qt: fix: bip70 pay reqs need x509 verification regression from https://github.com/spesmilo/electrum/pull/8462 - pr.verify() was called in qml, but not in qt gui - we now call pr.verify() in get_payment_request(), to make the API less error-prone - it is now ok to call pr.verify() multiple times, the result is cached --- electrum/contacts.py | 31 ++++++++++++++++++--------- electrum/gui/kivy/main_window.py | 2 +- electrum/gui/qml/qeinvoice.py | 2 +- electrum/gui/qt/main_window.py | 2 +- electrum/payment_identifier.py | 2 +- electrum/paymentrequest.py | 36 +++++++++++++++++++++----------- 6 files changed, 49 insertions(+), 26 deletions(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index febfcd57d..6630dd502 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -21,7 +21,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict, Any import dns import threading @@ -30,10 +30,13 @@ from . import bitcoin from . import dnssec from .util import read_json_file, write_json_file, to_string -from .logging import Logger +from .logging import Logger, get_logger from .util import trigger_callback +_logger = get_logger(__name__) + + class AliasNotFoundException(Exception): pass @@ -90,7 +93,13 @@ def resolve(self, k): 'address': addr, 'type': 'contact' } - out = self.resolve_openalias(k) + if openalias := self.resolve_openalias(k): + return openalias + raise AliasNotFoundException("Invalid Bitcoin address or alias", k) + + @classmethod + def resolve_openalias(cls, url: str) -> Dict[str, Any]: + out = cls._resolve_openalias(url) if out: address, name, validated = out return { @@ -99,7 +108,7 @@ def resolve(self, k): 'type': 'openalias', 'validated': validated } - raise AliasNotFoundException("Invalid Bitcoin address or alias", k) + return {} def by_name(self, name): for k in self.keys(): @@ -118,33 +127,35 @@ def fetch_openalias(self, config): if alias: alias = str(alias) def f(): - self.alias_info = self.resolve_openalias(alias) + self.alias_info = self._resolve_openalias(alias) trigger_callback('alias_received') t = threading.Thread(target=f) t.daemon = True t.start() - def resolve_openalias(self, url: str) -> Optional[Tuple[str, str, bool]]: + @classmethod + def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str, bool]]: # support email-style addresses, per the OA standard url = url.replace('@', '.') try: records, validated = dnssec.query(url, dns.rdatatype.TXT) except DNSException as e: - self.logger.info(f'Error resolving openalias: {repr(e)}') + _logger.info(f'Error resolving openalias: {repr(e)}') return None prefix = 'btc' for record in records: string = to_string(record.strings[0], 'utf8') if string.startswith('oa1:' + prefix): - address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') - name = self.find_regex(string, r'recipient_name=([^;]+)') + address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') + name = cls.find_regex(string, r'recipient_name=([^;]+)') if not name: name = address if not address: continue return address, name, validated - def find_regex(self, haystack, needle): + @staticmethod + def find_regex(haystack, needle): regex = re.compile(needle) try: return regex.search(haystack).groups()[0] diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 590e7b0fb..9697dcd04 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -459,7 +459,7 @@ def _on_pr(self, pr: 'PaymentRequest'): if not self.wallet: self.show_error(_('No wallet loaded.')) return - if pr.verify(self.wallet.contacts): + if pr.verify(): invoice = Invoice.from_bip70_payreq(pr, height=0) if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID: self.show_error("invoice already paid") diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 171959dbd..7a73ece05 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -473,7 +473,7 @@ def create_onchain_invoice(self, outputs, message, payment_request, uri): def _bip70_payment_request_resolved(self, pr: 'PaymentRequest'): self._logger.debug('resolved payment request') - if pr.verify(self._wallet.wallet.contacts): + if pr.verify(): invoice = Invoice.from_bip70_payreq(pr, height=0) if self._wallet.wallet.get_invoice_status(invoice) == PR_PAID: self.validationError.emit('unknown', _('Invoice already paid')) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index cf70c2f83..d79973bd4 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1438,7 +1438,7 @@ def show_onchain_invoice(self, invoice: Invoice): grid.addWidget(QLabel(format_time(invoice.exp + invoice.time)), 4, 1) if invoice.bip70: pr = paymentrequest.PaymentRequest(bytes.fromhex(invoice.bip70)) - pr.verify(self.contacts) + pr.verify() grid.addWidget(QLabel(_("Requestor") + ':'), 5, 0) grid.addWidget(QLabel(pr.get_requestor()), 5, 1) grid.addWidget(QLabel(_("Signature") + ':'), 6, 0) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index c601889a2..93a9191a6 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -340,7 +340,7 @@ async def _do_resolve(self, *, on_finished=None): elif self.bip70: from . import paymentrequest pr = await paymentrequest.get_payment_request(self.bip70) - if not pr.error: + if pr.verify(): self.bip70_data = pr self.set_state(PaymentIdentifierState.MERCHANT_NOTIFY) else: diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 73273dd7e..49082f422 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -39,13 +39,14 @@ sys.exit("Error: could not find paymentrequest_pb2.py. Create it with 'contrib/generate_payreqpb2.sh'") from . import bitcoin, constants, ecc, util, transaction, x509, rsakey -from .util import bfh, make_aiohttp_session, error_text_bytes_to_safe_str +from .util import bfh, make_aiohttp_session, error_text_bytes_to_safe_str, get_running_loop from .invoices import Invoice, get_id_from_onchain_outputs from .crypto import sha256 from .bitcoin import address_to_script from .transaction import PartialTxOutput from .network import Network from .logging import get_logger, Logger +from .contacts import Contacts if TYPE_CHECKING: from .simple_config import SimpleConfig @@ -104,6 +105,10 @@ async def get_payment_request(url: str) -> 'PaymentRequest': data = None error = f"Unknown scheme for payment request. URL: {url}" pr = PaymentRequest(data, error=error) + loop = get_running_loop() + # do x509/dnssec verification now (in separate thread, to avoid blocking event loop). + # we still expect the caller to at least check pr.error! + await loop.run_in_executor(None, pr.verify) return pr @@ -111,15 +116,17 @@ class PaymentRequest: def __init__(self, data: bytes, *, error=None): self.raw = data - self.error = error # FIXME overloaded and also used when 'verify' succeeds - self.parse(data) + self.error = error # type: Optional[str] + self._verified_success = None # caches result of _verify + self._verified_success_msg = None # type: Optional[str] + self._parse(data) self.requestor = None # known after verify self.tx = None def __str__(self): return str(self.raw) - def parse(self, r: bytes): + def _parse(self, r: bytes): self.outputs = [] # type: List[PartialTxOutput] if self.error: return @@ -147,8 +154,11 @@ def parse(self, r: bytes): self.memo = self.details.memo self.payment_url = self.details.payment_url - def verify(self, contacts): + def verify(self) -> bool: # FIXME: we should enforce that this method was called before we attempt payment + # note: this method might do network requests (at least for verify_dnssec) + if self._verified_success is True: + return True if self.error: return False if not self.raw: @@ -167,7 +177,7 @@ def verify(self, contacts): if pr.pki_type in ["x509+sha256", "x509+sha1"]: return self.verify_x509(pr) elif pr.pki_type in ["dnssec+btc", "dnssec+ecdsa"]: - return self.verify_dnssec(pr, contacts) + return self.verify_dnssec(pr) else: self.error = "ERROR: Unsupported PKI Type for Message Signature" return False @@ -209,13 +219,14 @@ def verify_x509(self, paymntreq): self.error = "ERROR: Invalid Signature for Payment Request Data" return False ### SIG Verified - self.error = 'Signed by Trusted CA: ' + ca.get_common_name() + self._verified_success_msg = 'Signed by Trusted CA: ' + ca.get_common_name() + self._verified_success = True return True - def verify_dnssec(self, pr, contacts): + def verify_dnssec(self, pr): sig = pr.signature alias = pr.pki_data - info = contacts.resolve(alias) + info = Contacts.resolve_openalias(alias) if info.get('validated') is not True: self.error = "Alias verification failed (DNSSEC)" return False @@ -225,7 +236,8 @@ def verify_dnssec(self, pr, contacts): pr.signature = b'' message = pr.SerializeToString() if ecc.verify_message_with_address(address, sig, message): - self.error = 'Verified with DNSSEC' + self._verified_success_msg = 'Verified with DNSSEC' + self._verified_success = True return True else: self.error = "verify failed" @@ -257,8 +269,8 @@ def get_address(self): def get_requestor(self): return self.requestor if self.requestor else self.get_address() - def get_verify_status(self): - return self.error if self.requestor else "No Signature" + def get_verify_status(self) -> str: + return (self.error or self._verified_success_msg) if self.requestor else "No Signature" def get_memo(self): return self.memo From f2dbf474130aef74b76c9094b02f60597078f3ff Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Jul 2023 17:59:56 +0000 Subject: [PATCH 1055/1143] payment_identifier.py: add some type hints, trivial clean-up --- electrum/payment_identifier.py | 61 ++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 93a9191a6..e740d4370 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -103,7 +103,7 @@ class PaymentIdentifier(Logger): * lightning address """ - def __init__(self, wallet: 'Abstract_Wallet', text): + def __init__(self, wallet: 'Abstract_Wallet', text: str): Logger.__init__(self) self._state = PaymentIdentifierState.EMPTY self.wallet = wallet @@ -139,7 +139,7 @@ def type(self): return self._type def set_state(self, state: 'PaymentIdentifierState'): - self.logger.debug(f'PI state {self._state} -> {state}') + self.logger.debug(f'PI state {self._state.name} -> {state.name}') self._state = state @property @@ -203,7 +203,7 @@ def is_error(self) -> bool: def get_error(self) -> str: return self.error - def parse(self, text): + def parse(self, text: str): # parse text, set self._type and self.error text = text.strip() if not text: @@ -290,13 +290,13 @@ def parse(self, text): self.error = f"Unknown payment identifier:\n{truncated_text}" self.set_state(PaymentIdentifierState.INVALID) - def resolve(self, *, on_finished: 'Callable'): + def resolve(self, *, on_finished: Callable[['PaymentIdentifier'], None]) -> None: assert self._state == PaymentIdentifierState.NEED_RESOLVE coro = self._do_resolve(on_finished=on_finished) asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) @log_exceptions - async def _do_resolve(self, *, on_finished=None): + async def _do_resolve(self, *, on_finished: Callable[['PaymentIdentifier'], None] = None): try: if self.emaillike or self.domainlike: # TODO: parallel lookup? @@ -356,19 +356,31 @@ async def _do_resolve(self, *, on_finished=None): return except Exception as e: self.error = str(e) - self.logger.error(repr(e)) + self.logger.error(f"_do_resolve() got error: {e!r}") self.set_state(PaymentIdentifierState.ERROR) finally: if on_finished: on_finished(self) - def finalize(self, *, amount_sat: int = 0, comment: str = None, on_finished: Callable = None): + def finalize( + self, + *, + amount_sat: int = 0, + comment: str = None, + on_finished: Callable[['PaymentIdentifier'], None] = None, + ): assert self._state == PaymentIdentifierState.LNURLP_FINALIZE coro = self._do_finalize(amount_sat, comment, on_finished=on_finished) asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) @log_exceptions - async def _do_finalize(self, amount_sat: int = None, comment: str = None, on_finished: Callable = None): + async def _do_finalize( + self, + *, + amount_sat: int = None, + comment: str = None, + on_finished: Callable[['PaymentIdentifier'], None] = None, + ): from .invoices import Invoice try: if not self.lnurl_data: @@ -402,20 +414,33 @@ async def _do_finalize(self, amount_sat: int = None, comment: str = None, on_fin self.set_state(PaymentIdentifierState.AVAILABLE) except Exception as e: self.error = str(e) - self.logger.error(repr(e)) + self.logger.error(f"_do_finalize() got error: {e!r}") self.set_state(PaymentIdentifierState.ERROR) finally: if on_finished: on_finished(self) - def notify_merchant(self, *, tx: 'Transaction' = None, refund_address: str = None, on_finished: 'Callable' = None): + def notify_merchant( + self, + *, + tx: 'Transaction', + refund_address: str, + on_finished: Callable[['PaymentIdentifier'], None] = None, + ): assert self._state == PaymentIdentifierState.MERCHANT_NOTIFY assert tx + assert refund_address coro = self._do_notify_merchant(tx, refund_address, on_finished=on_finished) asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) @log_exceptions - async def _do_notify_merchant(self, tx, refund_address, *, on_finished: 'Callable'): + async def _do_notify_merchant( + self, + tx: 'Transaction', + refund_address: str, + *, + on_finished: Callable[['PaymentIdentifier'], None] = None, + ): try: if not self.bip70_data: self.set_state(PaymentIdentifierState.ERROR) @@ -428,7 +453,7 @@ async def _do_notify_merchant(self, tx, refund_address, *, on_finished: 'Callabl self.set_state(PaymentIdentifierState.MERCHANT_ACK) except Exception as e: self.error = str(e) - self.logger.error(repr(e)) + self.logger.error(f"_do_notify_merchant() got error: {e!r}") self.set_state(PaymentIdentifierState.MERCHANT_ERROR) finally: if on_finished: @@ -448,7 +473,7 @@ def get_onchain_outputs(self, amount): else: raise Exception('not onchain') - def _parse_as_multiline(self, text): + def _parse_as_multiline(self, text: str): # filter out empty lines lines = text.split('\n') lines = [i for i in lines if i] @@ -473,7 +498,7 @@ def _parse_as_multiline(self, text): self.logger.debug(f'multiline: {outputs!r}, {self.error}') return outputs - def parse_address_and_amount(self, line) -> 'PartialTxOutput': + def parse_address_and_amount(self, line: str) -> 'PartialTxOutput': try: x, y = line.split(',') except ValueError: @@ -484,7 +509,7 @@ def parse_address_and_amount(self, line) -> 'PartialTxOutput': amount = self.parse_amount(y) return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount) - def parse_output(self, x) -> bytes: + def parse_output(self, x: str) -> bytes: try: address = self.parse_address(x) return bytes.fromhex(bitcoin.address_to_script(address)) @@ -498,7 +523,7 @@ def parse_output(self, x) -> bytes: # raise Exception("Invalid address or script.") - def parse_script(self, x): + def parse_script(self, x: str): script = '' for word in x.split(): if word[0:3] == 'OP_': @@ -509,7 +534,7 @@ def parse_script(self, x): script += construct_script([word]) return script - def parse_amount(self, x): + def parse_amount(self, x: str): x = x.strip() if not x: raise Exception("Amount is empty") @@ -521,7 +546,7 @@ def parse_amount(self, x): except InvalidOperation: raise Exception("Invalid amount") - def parse_address(self, line): + def parse_address(self, line: str): r = line.strip() m = re.match('^' + RE_ALIAS + '$', r) address = str(m.group(2) if m else r) From bb8c73cabdcf2865a804cf97d946d6a46cce62e1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 10 Jul 2023 18:16:56 +0000 Subject: [PATCH 1056/1143] qt: kind of fix bip70 notify_merchant logic by passing around PI ``` 229.18 | E | gui.qt.main_window.[test_segwit_2] | on_error Traceback (most recent call last): File "...\electrum\gui\qt\util.py", line 917, in run result = task.task() File "...\electrum\gui\qt\send_tab.py", line 681, in broadcast_thread if self.payto_e.payment_identifier.has_expired(): AttributeError: 'NoneType' object has no attribute 'has_expired' ``` In SendTab.broadcast_transaction.broadcast_thread, self.payto_e.payment_identifier was referenced - but do_clear() has already cleared it by then. E.g. consider SendTab.pay_onchain_dialog: it calls save_pending_invoice(), which calls do_clear(), and later (in sign_done), it calls window.broadcast_or_show, which will call SendTab.broadcast_transaction(). As there might be multiple independent transaction dialogs open simultaneously, the single shared state send_tab.payto_e.payment_identifier approach was problematic -- I think it is conceptually nicer to pass around the payment_identifiers as needed, as done with this change. However, this change is not a full proper fix, as it still somewhat relies on send_tab.payto_e.payment_identifier (e.g. in pay_onchain_dialog). Hence, e.g. when using the invoice_list context menu "Pay..." item, as payto_e.payment_identifier is not set, payment_identifier will be None in broadcast_transaction. but at least we handle PI being None gracefully -- before this change, broadcast_transaction expected PI to be set, and it was never set to the correct thing (as do_clear() already ran by then): depending on timing it was either None or a new empty PI. In the former case, producing the above traceback and hard failing (not only for bip70 stuff!), and in the latter, silently ignoring the logic bug. --- electrum/gui/qt/main_window.py | 22 ++++++++++++++-------- electrum/gui/qt/paytoedit.py | 4 ++-- electrum/gui/qt/send_tab.py | 26 ++++++++++++++++---------- electrum/gui/qt/transaction_dialog.py | 23 ++++++++++++++++++++--- 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d79973bd4..71f652417 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1062,8 +1062,14 @@ def show_channel_details(self, chan): from .channel_details import ChannelDetailsDialog ChannelDetailsDialog(self, chan).show() - def show_transaction(self, tx: Transaction, *, external_keypairs=None): - show_transaction(tx, parent=self, external_keypairs=external_keypairs) + def show_transaction( + self, + tx: Transaction, + *, + external_keypairs=None, + payment_identifier: PaymentIdentifier = None, + ): + show_transaction(tx, parent=self, external_keypairs=external_keypairs, payment_identifier=payment_identifier) def show_lightning_transaction(self, tx_item): from .lightning_tx_dialog import LightningTxDialog @@ -1208,18 +1214,18 @@ def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]: """ return self.utxo_list.get_spend_list() - def broadcast_or_show(self, tx: Transaction): + def broadcast_or_show(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): if not tx.is_complete(): - self.show_transaction(tx) + self.show_transaction(tx, payment_identifier=payment_identifier) return if not self.network: self.show_error(_("You can't broadcast a transaction without a live network connection.")) - self.show_transaction(tx) + self.show_transaction(tx, payment_identifier=payment_identifier) return - self.broadcast_transaction(tx) + self.broadcast_transaction(tx, payment_identifier=payment_identifier) - def broadcast_transaction(self, tx: Transaction): - self.send_tab.broadcast_transaction(tx) + def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): + self.send_tab.broadcast_transaction(tx, payment_identifier=payment_identifier) @protected def sign_tx(self, tx, *, callback, external_keypairs, password): diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 3fb8f659f..0ffe02314 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -173,7 +173,7 @@ def line_edit_changed(): self.edit_timer.setInterval(1000) self.edit_timer.timeout.connect(self._on_edit_timer) - self.payment_identifier = None + self.payment_identifier = None # type: Optional[PaymentIdentifier] @property def multiline(self): @@ -206,8 +206,8 @@ def setToolTip(self, tt: str) -> None: self.line_edit.setToolTip(tt) self.text_edit.setToolTip(tt) - '''set payment identifier only if valid, else exception''' def try_payment_identifier(self, text) -> None: + '''set payment identifier only if valid, else exception''' text = text.strip() pi = PaymentIdentifier(self.send_tab.wallet, text) if not pi.is_valid(): diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 2dfe8be09..b0d1f1f8f 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -17,7 +17,7 @@ from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.payment_identifier import PaymentIdentifierState, PaymentIdentifierType +from electrum.payment_identifier import PaymentIdentifierState, PaymentIdentifierType, PaymentIdentifier from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .paytoedit import InvalidPaymentIdentifier @@ -268,7 +268,8 @@ def spend_max(self): def pay_onchain_dialog( self, - outputs: List[PartialTxOutput], *, + outputs: List[PartialTxOutput], + *, nonlocal_only=False, external_keypairs=None, get_coins: Callable[..., Sequence[PartialTxInput]] = None, @@ -276,6 +277,9 @@ def pay_onchain_dialog( # trustedcoin requires this if run_hook('abort_send', self): return + # save current PI as local now. this is best-effort only... + # does not work e.g. when using InvoiceList context menu "pay" + payment_identifier = self.payto_e.payment_identifier is_sweep = bool(external_keypairs) # we call get_coins inside make_tx, so that inputs can be changed dynamically if get_coins is None: @@ -302,12 +306,12 @@ def pay_onchain_dialog( return is_preview = conf_dlg.is_preview if is_preview: - self.window.show_transaction(tx, external_keypairs=external_keypairs) + self.window.show_transaction(tx, external_keypairs=external_keypairs, payment_identifier=payment_identifier) return self.save_pending_invoice() def sign_done(success): if success: - self.window.broadcast_or_show(tx) + self.window.broadcast_or_show(tx, payment_identifier=payment_identifier) self.window.sign_tx( tx, callback=sign_done, @@ -527,7 +531,7 @@ def pay_multiple_invoices(self, invoices): outputs += invoice.outputs self.pay_onchain_dialog(outputs) - def do_edit_invoice(self, invoice: 'Invoice'): + def do_edit_invoice(self, invoice: 'Invoice'): # FIXME broken assert not bool(invoice.get_amount_sat()) text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address() self.payto_e._on_input_btn(text) @@ -674,11 +678,13 @@ def pay_lightning_invoice(self, invoice: Invoice): coro = lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat) self.window.run_coroutine_from_thread(coro, _('Sending payment')) - def broadcast_transaction(self, tx: Transaction): + def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): + # note: payment_identifier is explicitly passed as self.payto_e.payment_identifier might + # already be cleared or otherwise have changed. def broadcast_thread(): # non-GUI thread - if self.payto_e.payment_identifier.has_expired(): + if payment_identifier and payment_identifier.has_expired(): return False, _("Invoice has expired") try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) @@ -688,9 +694,9 @@ def broadcast_thread(): return False, repr(e) # success txid = tx.txid() - if self.payto_e.payment_identifier.need_merchant_notify(): + if payment_identifier and payment_identifier.need_merchant_notify(): refund_address = self.wallet.get_receiving_address() - self.payto_e.payment_identifier.notify_merchant( + payment_identifier.notify_merchant( tx=tx, refund_address=refund_address, on_finished=self.notify_merchant_done_signal.emit @@ -718,7 +724,7 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.window.on_error) - def on_notify_merchant_done(self, pi): + def on_notify_merchant_done(self, pi: PaymentIdentifier): if pi.is_error(): self.logger.debug(f'merchant notify error: {pi.get_error()}') else: diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 579a08e25..b6a483d24 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -70,6 +70,7 @@ if TYPE_CHECKING: from .main_window import ElectrumWindow from electrum.wallet import Abstract_Wallet + from electrum.payment_identifier import PaymentIdentifier _logger = get_logger(__name__) @@ -378,9 +379,16 @@ def show_transaction( parent: 'ElectrumWindow', prompt_if_unsaved: bool = False, external_keypairs=None, + payment_identifier: 'PaymentIdentifier' = None, ): try: - d = TxDialog(tx, parent=parent, prompt_if_unsaved=prompt_if_unsaved, external_keypairs=external_keypairs) + d = TxDialog( + tx, + parent=parent, + prompt_if_unsaved=prompt_if_unsaved, + external_keypairs=external_keypairs, + payment_identifier=payment_identifier, + ) except SerializationError as e: _logger.exception('unable to deserialize the transaction') parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) @@ -392,7 +400,15 @@ class TxDialog(QDialog, MessageBoxMixin): throttled_update_sig = pyqtSignal() # emit from thread to do update in main thread - def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsaved: bool, external_keypairs=None): + def __init__( + self, + tx: Transaction, + *, + parent: 'ElectrumWindow', + prompt_if_unsaved: bool, + external_keypairs=None, + payment_identifier: 'PaymentIdentifier' = None, + ): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' @@ -403,6 +419,7 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', prompt_if_unsav self.main_window = parent self.config = parent.config self.wallet = parent.wallet + self.payment_identifier = payment_identifier self.prompt_if_unsaved = prompt_if_unsaved self.saved = False self.desc = None @@ -537,7 +554,7 @@ def do_broadcast(self): self.main_window.push_top_level_window(self) self.main_window.send_tab.save_pending_invoice() try: - self.main_window.broadcast_transaction(self.tx) + self.main_window.broadcast_transaction(self.tx, payment_identifier=self.payment_identifier) finally: self.main_window.pop_top_level_window(self) self.saved = True From b6863b48540dcf03e0841ef29053fa1af48bf7d0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 12:51:37 +0200 Subject: [PATCH 1057/1143] qml: add LabelSync toggle --- electrum/gui/qml/__init__.py | 3 +- electrum/gui/qml/components/Preferences.qml | 20 +++++++++++++ electrum/gui/qml/qeapp.py | 14 ++++++++-- electrum/plugin.py | 1 + electrum/plugins/labels/labels.py | 14 ++++++---- electrum/plugins/labels/qml.py | 31 +++++++++++++-------- electrum/plugins/trustedcoin/qml.py | 9 +++--- 7 files changed, 68 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qml/__init__.py b/electrum/gui/qml/__init__.py index 9d36f369b..30f7d9d1a 100644 --- a/electrum/gui/qml/__init__.py +++ b/electrum/gui/qml/__init__.py @@ -41,6 +41,7 @@ def __init__(self, parent=None): def translate(self, context, source_text, disambiguation, n): return _(source_text, context=context) + class ElectrumGui(BaseElectrumGui, Logger): @profiler @@ -91,7 +92,7 @@ def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins') Exception_Hook.maybe_setup(config=config, slot=self.app.appController.crash) # Initialize any QML plugins - run_hook('init_qml', self) + run_hook('init_qml', self.app) self.app.engine.load('electrum/gui/qml/components/main.qml') def close(self): diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index c2cba8dd7..5ec5356be 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -217,6 +217,25 @@ Pane { } } + RowLayout { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.leftMargin: -constants.paddingSmall + spacing: 0 + Switch { + id: syncLabels + onCheckedChanged: { + if (activeFocus) + AppController.setPluginEnabled('labels', checked) + } + } + Label { + Layout.fillWidth: true + text: qsTr('Synchronize labels') + wrapMode: Text.Wrap + } + } + PrefsHeading { Layout.columnSpan: 2 text: qsTr('Wallet behavior') @@ -384,5 +403,6 @@ Pane { useFallbackAddress.checked = Config.useFallbackAddress enableDebugLogs.checked = Config.enableDebugLogs useRecoverableChannels.checked = Config.useRecoverableChannels + syncLabels.checked = AppController.isPluginEnabled('labels') } } diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py index bb2245c4f..20de321b0 100644 --- a/electrum/gui/qml/qeapp.py +++ b/electrum/gui/qml/qeapp.py @@ -19,6 +19,7 @@ from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue from electrum.network import Network +from electrum.plugin import run_hook from .qeconfig import QEConfig from .qedaemon import QEDaemon @@ -70,10 +71,11 @@ class QEAppController(BaseCrashReporter, QObject): sendingBugreportFailure = pyqtSignal(str) secureWindowChanged = pyqtSignal() - def __init__(self, qedaemon: 'QEDaemon', plugins: 'Plugins'): + def __init__(self, qeapp: 'ElectrumQmlApplication', qedaemon: 'QEDaemon', plugins: 'Plugins'): BaseCrashReporter.__init__(self, None, None, None) QObject.__init__(self) + self._app = qeapp self._qedaemon = qedaemon self._plugins = plugins self.config = qedaemon.daemon.config @@ -224,12 +226,18 @@ def plugins(self): return s @pyqtSlot(str, bool) - def setPluginEnabled(self, plugin, enabled): + def setPluginEnabled(self, plugin: str, enabled: bool): if enabled: self._plugins.enable(plugin) + # note: all enabled plugins will receive this hook: + run_hook('init_qml', self._app) else: self._plugins.disable(plugin) + @pyqtSlot(str, result=bool) + def isPluginEnabled(self, plugin: str): + return bool(self._plugins.get(plugin)) + @pyqtSlot(result=bool) def isAndroid(self): return 'ANDROID_DATA' in os.environ @@ -369,7 +377,7 @@ def __init__(self, args, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: ' self._qeconfig = QEConfig(config) self._qenetwork = QENetwork(daemon.network, self._qeconfig) self.daemon = QEDaemon(daemon) - self.appController = QEAppController(self.daemon, self.plugins) + self.appController = QEAppController(self, self.daemon, self.plugins) self._maxAmount = QEAmount(is_max=True) self.context.setContextProperty('AppController', self.appController) self.context.setContextProperty('Config', self._qeconfig) diff --git a/electrum/plugin.py b/electrum/plugin.py index 0194fb1a2..8879e15a7 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -218,6 +218,7 @@ def hook(func): hook_names.add(func.__name__) return func + def run_hook(name, *args): results = [] f_list = hooks.get(name, []) diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index a20cae8f4..9e392f6f9 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -134,9 +134,11 @@ async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool): response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id)) except Exception as e: raise ErrorConnectingServer(e) from e - if response["labels"] is None: + if response["labels"] is None or len(response["labels"]) == 0: self.logger.info('no new labels') return + self.logger.debug(f"labels received {response!r}") + self.logger.info(f'received {len(response["labels"])} labels') result = {} for label in response["labels"]: try: @@ -157,7 +159,6 @@ async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool): if force or not wallet._get_label(key): wallet._set_label(key, value) - self.logger.info(f"received {len(response)} labels") self.set_nonce(wallet, response["nonce"] + 1) self.on_pulled(wallet) @@ -173,15 +174,18 @@ async def pull_safe_thread(self, wallet: 'Abstract_Wallet', force: bool): self.logger.info(repr(e)) def pull(self, wallet: 'Abstract_Wallet', force: bool): - if not wallet.network: raise Exception(_('You are offline.')) + if not wallet.network: + raise Exception(_('You are offline.')) return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result() def push(self, wallet: 'Abstract_Wallet'): - if not wallet.network: raise Exception(_('You are offline.')) + if not wallet.network: + raise Exception(_('You are offline.')) return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result() def start_wallet(self, wallet: 'Abstract_Wallet'): - if not wallet.network: return # 'offline' mode + if not wallet.network: + return # 'offline' mode mpk = wallet.get_fingerprint() if not mpk: return diff --git a/electrum/plugins/labels/qml.py b/electrum/plugins/labels/qml.py index 6f95dca0e..4ad17263f 100644 --- a/electrum/plugins/labels/qml.py +++ b/electrum/plugins/labels/qml.py @@ -1,6 +1,6 @@ import threading -from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot +from PyQt5.QtCore import pyqtSignal, pyqtSlot from electrum.i18n import _ from electrum.plugin import hook @@ -10,6 +10,7 @@ from .labels import LabelsPlugin + class Plugin(LabelsPlugin): class QSignalObject(PluginQObject): @@ -63,6 +64,8 @@ def download_finished(self, result): def __init__(self, *args): LabelsPlugin.__init__(self, *args) + self._app = None + self.so = None @hook def load_wallet(self, wallet): @@ -77,9 +80,9 @@ def push_async(self): wallet = self._app.daemon.currentWallet.wallet - def push_thread(wallet): + def push_thread(_wallet): try: - self.push(wallet) + self.push(_wallet) self.so.upload_finished(True) self._app.appController.userNotify.emit(_('Labels uploaded')) except Exception as e: @@ -87,7 +90,7 @@ def push_thread(wallet): self.so.upload_finished(False) self._app.appController.userNotify.emit(repr(e)) - threading.Thread(target=push_thread,args=[wallet]).start() + threading.Thread(target=push_thread, args=[wallet]).start() def pull_async(self): if not self._app.daemon.currentWallet: @@ -96,9 +99,10 @@ def pull_async(self): return wallet = self._app.daemon.currentWallet.wallet - def pull_thread(wallet): + + def pull_thread(_wallet): try: - self.pull(wallet, True) + self.pull(_wallet, True) self.so.download_finished(True) self._app.appController.userNotify.emit(_('Labels downloaded')) except Exception as e: @@ -106,8 +110,7 @@ def pull_thread(wallet): self.so.download_finished(False) self._app.appController.userNotify.emit(repr(e)) - threading.Thread(target=pull_thread,args=[wallet]).start() - + threading.Thread(target=pull_thread, args=[wallet]).start() def on_pulled(self, wallet): self.logger.info('on pulled') @@ -117,9 +120,15 @@ def on_pulled(self, wallet): _wallet.labelsUpdated.emit() @hook - def init_qml(self, gui): - self.logger.debug(f'init_qml hook called, gui={str(type(gui))}') - self._app = gui.app + def init_qml(self, app): + self.logger.debug(f'init_qml hook called, gui={str(type(app))}') + self.logger.debug(f'app={self._app!r}, so={self.so!r}') + self._app = app # important: QSignalObject needs to be parented, as keeping a ref # in the plugin is not enough to avoid gc self.so = Plugin.QSignalObject(self, self._app) + + # If the user just enabled the plugin, the 'load_wallet' hook would not + # get called for already loaded wallets, hence we call it manually for those: + for wallet_name, wallet in app.daemon.daemon._wallets.items(): + self.load_wallet(wallet) diff --git a/electrum/plugins/trustedcoin/qml.py b/electrum/plugins/trustedcoin/qml.py index ea51db35c..962676ee0 100644 --- a/electrum/plugins/trustedcoin/qml.py +++ b/electrum/plugins/trustedcoin/qml.py @@ -19,9 +19,10 @@ TrustedCoinException, make_xpub) if TYPE_CHECKING: - from electrum.gui.qml import ElectrumGui + from electrum.gui.qml import ElectrumQmlApplication from electrum.wallet import Abstract_Wallet + class Plugin(TrustedCoinPlugin): class QSignalObject(PluginQObject): @@ -287,9 +288,9 @@ def load_wallet(self, wallet: 'Abstract_Wallet'): self.start_request_thread(wallet) @hook - def init_qml(self, gui: 'ElectrumGui'): - self.logger.debug(f'init_qml hook called, gui={str(type(gui))}') - self._app = gui.app + def init_qml(self, app: 'ElectrumQmlApplication'): + self.logger.debug(f'init_qml hook called, gui={str(type(app))}') + self._app = app # important: QSignalObject needs to be parented, as keeping a ref # in the plugin is not enough to avoid gc self.so = Plugin.QSignalObject(self, self._app) From d15050a2bf2c0342b1489f354f6e85b4978935ff Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 14:15:17 +0200 Subject: [PATCH 1058/1143] payment_identifier: fix _on_finalize params, fix lnurlp send comment instead of description, refactor payment_identifier.get_invoice to standalone invoice_from_payment_identifier --- electrum/gui/qt/send_tab.py | 10 +++++--- electrum/payment_identifier.py | 46 +++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index b0d1f1f8f..22e0a0297 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -17,7 +17,8 @@ from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed -from electrum.payment_identifier import PaymentIdentifierState, PaymentIdentifierType, PaymentIdentifier +from electrum.payment_identifier import PaymentIdentifierState, PaymentIdentifierType, PaymentIdentifier, \ + invoice_from_payment_identifier from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .paytoedit import InvalidPaymentIdentifier @@ -25,6 +26,7 @@ char_width_in_lineedit, get_iconname_camera, get_iconname_qrcode, read_QIcon, ColorScheme, icon_path) from .confirm_tx_dialog import ConfirmTxDialog +from .invoice_list import InvoiceList if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -161,7 +163,6 @@ def reset_max(text): self.fiat_send_e.textEdited.connect(reset_max) self.invoices_label = QLabel(_('Invoices')) - from .invoice_list import InvoiceList self.invoice_list = InvoiceList(self) self.toolbar, menu = self.invoice_list.create_toolbar_with_menu('') @@ -469,7 +470,8 @@ def read_invoice(self) -> Optional[Invoice]: self.show_error(_('No amount')) return - invoice = self.payto_e.payment_identifier.get_invoice(amount_sat, self.get_message()) + invoice = invoice_from_payment_identifier( + self.payto_e.payment_identifier, self.wallet, amount_sat, self.get_message()) if not invoice: self.show_error('error getting invoice' + self.payto_e.payment_identifier.error) return @@ -517,7 +519,7 @@ def do_pay_or_get_invoice(self): pi = self.payto_e.payment_identifier if pi.need_finalize(): self.prepare_for_send_tab_network_lookup() - pi.finalize(amount_sat=self.get_amount(), comment=self.message_e.text(), + pi.finalize(amount_sat=self.get_amount(), comment=self.comment_e.text(), on_finished=self.finalize_done_signal.emit) return self.pending_invoice = self.read_invoice() diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index e740d4370..68ba7dcaf 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -370,7 +370,7 @@ def finalize( on_finished: Callable[['PaymentIdentifier'], None] = None, ): assert self._state == PaymentIdentifierState.LNURLP_FINALIZE - coro = self._do_finalize(amount_sat, comment, on_finished=on_finished) + coro = self._do_finalize(amount_sat=amount_sat, comment=comment, on_finished=on_finished) asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) @log_exceptions @@ -665,22 +665,28 @@ def has_expired(self): return bool(expires) and expires < time.time() return False - def get_invoice(self, amount_sat, message): - # FIXME: this should not be a PI method - # ideally, PI should not have a reference to wallet. - if self.is_lightning(): - invoice = self.bolt11 - if not invoice: - return - if invoice.amount_msat is None: - invoice.amount_msat = int(amount_sat * 1000) - return invoice - else: - outputs = self.get_onchain_outputs(amount_sat) - message = self.bip21.get('message') if self.bip21 else message - bip70_data = self.bip70_data if self.bip70 else None - return self.wallet.create_invoice( - outputs=outputs, - message=message, - pr=bip70_data, - URI=self.bip21) + +def invoice_from_payment_identifier( + pi: 'PaymentIdentifier', + wallet: 'Abstract_Wallet', + amount_sat: int, + message: str = None +): + # FIXME: this should not be a PI method + # ideally, PI should not have a reference to wallet. + if pi.is_lightning(): + invoice = pi.bolt11 + if not invoice: + return + if invoice.amount_msat is None: + invoice.amount_msat = int(amount_sat * 1000) + return invoice + else: + outputs = pi.get_onchain_outputs(amount_sat) + message = pi.bip21.get('message') if pi.bip21 else message + bip70_data = self.bip70_data if self.bip70 else None + return wallet.create_invoice( + outputs=outputs, + message=message, + pr=bip70_data, + URI=pi.bip21) From d695dd51cd5030856cee31a6a006f0837c5ee2f3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 11 Jul 2023 12:17:40 +0000 Subject: [PATCH 1059/1143] build: include AppStream metainfo.xml in tarballs closes https://github.com/spesmilo/electrum/issues/8501 related https://github.com/spesmilo/electrum/pull/8149 --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 2a74487c0..f7db91710 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include README.md include electrum.desktop include *.py include run_electrum +include org.electrum.electrum.metainfo.xml recursive-include packages *.py recursive-include packages cacert.pem From 78a741e4a386ad216c17e5c7e2ed4a4144628c86 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 14:19:12 +0200 Subject: [PATCH 1060/1143] actually remove the FIXME --- electrum/payment_identifier.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 68ba7dcaf..935e589be 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -672,8 +672,6 @@ def invoice_from_payment_identifier( amount_sat: int, message: str = None ): - # FIXME: this should not be a PI method - # ideally, PI should not have a reference to wallet. if pi.is_lightning(): invoice = pi.bolt11 if not invoice: From 33acfd3d1c15cfb314a5c99695a4c16b83da8793 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 14:24:43 +0200 Subject: [PATCH 1061/1143] followup d15050a2bf2c0342b1489f354f6e85b4978935ff --- electrum/payment_identifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 935e589be..11c84f8f4 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -682,7 +682,7 @@ def invoice_from_payment_identifier( else: outputs = pi.get_onchain_outputs(amount_sat) message = pi.bip21.get('message') if pi.bip21 else message - bip70_data = self.bip70_data if self.bip70 else None + bip70_data = pi.bip70_data if pi.bip70 else None return wallet.create_invoice( outputs=outputs, message=message, From ad4b431738f1460a6c3daa3f76de7b9b73688cd0 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 15:24:03 +0200 Subject: [PATCH 1062/1143] payment_identifier: fix setting self.bolt11 to invoice in bip21 LN alt case --- electrum/payment_identifier.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 11c84f8f4..0b62d5f42 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -253,13 +253,9 @@ def parse(self, text: str): bolt11 = out.get('lightning') if bolt11: try: - lndecode(bolt11) - # if we get here, we have a usable bolt11 - self.bolt11 = bolt11 - except LnInvoiceException as e: - self.logger.debug(_("Error parsing Lightning invoice") + f":\n{e}") - except IncompatibleOrInsaneFeatures as e: - self.logger.debug(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}") + self.bolt11 = Invoice.from_bech32(bolt11) + except InvoiceError as e: + self.logger.debug(self._get_error_from_invoiceerror(e)) self.set_state(PaymentIdentifierState.AVAILABLE) elif scriptpubkey := self.parse_output(text): self._type = PaymentIdentifierType.SPK @@ -671,7 +667,7 @@ def invoice_from_payment_identifier( wallet: 'Abstract_Wallet', amount_sat: int, message: str = None -): +) -> Optional[Invoice]: if pi.is_lightning(): invoice = pi.bolt11 if not invoice: From b1b2190f0ab332d92629601618eb5349c938a324 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 16:43:49 +0200 Subject: [PATCH 1063/1143] payment_identifier: add payment_identifier_from_invoice function to reconstruct a PI from bip70 invoice --- electrum/payment_identifier.py | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 0b62d5f42..05aa3c3b3 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -19,6 +19,7 @@ from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnutil import IncompatibleOrInsaneFeatures from .bip21 import parse_bip21_URI, InvalidBitcoinURI, LIGHTNING_URI_SCHEME, BITCOIN_BIP21_URI_SCHEME +from . import paymentrequest if TYPE_CHECKING: from .wallet import Abstract_Wallet @@ -334,7 +335,6 @@ async def _do_resolve(self, *, on_finished: Callable[['PaymentIdentifier'], None else: self.set_state(PaymentIdentifierState.NOT_FOUND) elif self.bip70: - from . import paymentrequest pr = await paymentrequest.get_payment_request(self.bip70) if pr.verify(): self.bip70_data = pr @@ -668,6 +668,7 @@ def invoice_from_payment_identifier( amount_sat: int, message: str = None ) -> Optional[Invoice]: + assert pi.state in [PaymentIdentifierState.AVAILABLE, PaymentIdentifierState.MERCHANT_NOTIFY] if pi.is_lightning(): invoice = pi.bolt11 if not invoice: @@ -684,3 +685,43 @@ def invoice_from_payment_identifier( message=message, pr=bip70_data, URI=pi.bip21) + + +# Note: this is only really used for bip70 to handle MECHANT_NOTIFY state from +# a saved bip70 invoice. +# TODO: reflect bip70-only in function name, or implement other types as well. +def payment_identifier_from_invoice( + wallet: 'Abstract_Wallet', + invoice: Invoice +) -> Optional[PaymentIdentifier]: + if not invoice: + return + pi = PaymentIdentifier(wallet, '') + if invoice.bip70: + pi._type = PaymentIdentifierType.BIP70 + pi.bip70_data = paymentrequest.PaymentRequest(bytes.fromhex(invoice.bip70)) + pi.set_state(PaymentIdentifierState.MERCHANT_NOTIFY) + return pi + # else: + # if invoice.outputs: + # if len(invoice.outputs) > 1: + # pi._type = PaymentIdentifierType.MULTILINE + # pi.multiline_outputs = invoice.outputs + # pi.set_state(PaymentIdentifierState.AVAILABLE) + # else: + # pi._type = PaymentIdentifierType.BIP21 + # params = {} + # if invoice.exp: + # params['exp'] = str(invoice.exp) + # if invoice.time: + # params['time'] = str(invoice.time) + # pi.bip21 = create_bip21_uri(invoice.outputs[0].address, invoice.get_amount_sat(), invoice.message, + # extra_query_params=params) + # pi.set_state(PaymentIdentifierState.AVAILABLE) + # elif invoice.is_lightning(): + # pi._type = PaymentIdentifierType.BOLT11 + # pi.bolt11 = invoice + # pi.set_state(PaymentIdentifierState.AVAILABLE) + # else: + # return None + # return pi From 5b9b6161465e7843ce1bcc918ff7d722035aba7f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 11 Jul 2023 14:50:09 +0000 Subject: [PATCH 1064/1143] simple_config: allow deepcopy-ing ConfigVars Was getting error: ``` >>> import copy >>> from electrum.simple_config import SimpleConfig >>> copy.deepcopy(SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS) Traceback (most recent call last): File "", line 1, in File "...\Python\Python310\lib\copy.py", line 161, in deepcopy rv = reductor(4) TypeError: cannot pickle 'ConfigVar' object ``` This is useful in tests/test_lnpeer.py, to deepcopy the GRAPH_DEFINITIONS dict. --- electrum/simple_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index dedb93b4d..43f835a5e 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -89,6 +89,10 @@ def get_default_value(self) -> Any: def __repr__(self): return f"" + def __deepcopy__(self, memo): + cv = ConfigVar(self._key, default=self._default, type_=self._type) + return cv + class ConfigVarWithConfig: From 40f15b158cabf1111662b97d87b39f00a0a240de Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 17:13:57 +0200 Subject: [PATCH 1065/1143] payment_identifier: reconstruct PI for bip70 invoice in pay_onchain_dialog instead of taking the send_tab PI --- electrum/gui/qt/send_tab.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 22e0a0297..7ee00ed4e 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -18,7 +18,7 @@ from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.payment_identifier import PaymentIdentifierState, PaymentIdentifierType, PaymentIdentifier, \ - invoice_from_payment_identifier + invoice_from_payment_identifier, payment_identifier_from_invoice from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .paytoedit import InvalidPaymentIdentifier @@ -267,6 +267,9 @@ def spend_max(self): msg += "\n" + _("Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)").format(frozen_bal) QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg) + # TODO: instead of passing outputs, use an invoice instead (like pay_lightning_invoice) + # so we have more context (we cannot rely on send_tab field contents or payment identifier + # as this method is called from other places as well). def pay_onchain_dialog( self, outputs: List[PartialTxOutput], @@ -274,13 +277,18 @@ def pay_onchain_dialog( nonlocal_only=False, external_keypairs=None, get_coins: Callable[..., Sequence[PartialTxInput]] = None, + invoice: Optional[Invoice] = None ) -> None: # trustedcoin requires this if run_hook('abort_send', self): return # save current PI as local now. this is best-effort only... # does not work e.g. when using InvoiceList context menu "pay" - payment_identifier = self.payto_e.payment_identifier + + payment_identifier = None + if invoice and invoice.bip70: + payment_identifier = payment_identifier_from_invoice(self.wallet, invoice) + is_sweep = bool(external_keypairs) # we call get_coins inside make_tx, so that inputs can be changed dynamically if get_coins is None: @@ -475,6 +483,7 @@ def read_invoice(self) -> Optional[Invoice]: if not invoice: self.show_error('error getting invoice' + self.payto_e.payment_identifier.error) return + if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): self.show_error(_('Lightning is disabled')) if self.wallet.get_invoice_status(invoice) == PR_PAID: @@ -548,7 +557,7 @@ def do_pay_invoice(self, invoice: 'Invoice'): if invoice.is_lightning(): self.pay_lightning_invoice(invoice) else: - self.pay_onchain_dialog(invoice.outputs) + self.pay_onchain_dialog(invoice.outputs, invoice=invoice) def read_amount(self) -> List[PartialTxOutput]: amount = '!' if self.max_button.isChecked() else self.get_amount() From 0d7ff92c437e05794a24dd1a681aebca069e3feb Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Tue, 11 Jul 2023 19:54:07 +0200 Subject: [PATCH 1066/1143] send_tab: remove payment_identifier comments in send_tab --- electrum/gui/qt/send_tab.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 7ee00ed4e..8de66f2e3 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -282,8 +282,6 @@ def pay_onchain_dialog( # trustedcoin requires this if run_hook('abort_send', self): return - # save current PI as local now. this is best-effort only... - # does not work e.g. when using InvoiceList context menu "pay" payment_identifier = None if invoice and invoice.bip70: From 4900d01344449eafa0614a94de0760c96cef410a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 13 Jul 2023 18:51:17 +0200 Subject: [PATCH 1067/1143] allow more time to detect tor --- electrum/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index 16396462b..b9cdeb13f 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1470,7 +1470,7 @@ def detect_tor_socks_proxy() -> Optional[Tuple[str, int]]: def is_tor_socks_port(host: str, port: int) -> bool: try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(0.1) + s.settimeout(1.0) s.connect((host, port)) # mimic "tor-resolve 0.0.0.0". # see https://github.com/spesmilo/electrum/issues/7317#issuecomment-1369281075 From 2cf4cc197856ac113ea972fb720a72be082b652c Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 11 May 2023 13:23:23 +0200 Subject: [PATCH 1068/1143] qml: render reserved channel capacity in a darker tone, take frozen into account --- .../gui/qml/components/ChannelDetails.qml | 4 ++ electrum/gui/qml/components/Constants.qml | 4 +- .../qml/components/controls/ChannelBar.qml | 50 +++++++++++++++---- .../components/controls/ChannelDelegate.qml | 4 ++ electrum/gui/qml/qechannellistmodel.py | 10 ++-- 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index a82cbf42f..dbe81e152 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -170,6 +170,10 @@ Pane { capacity: channeldetails.capacity localCapacity: channeldetails.localCapacity remoteCapacity: channeldetails.remoteCapacity + canSend: channeldetails.canSend + canReceive: channeldetails.canReceive + frozenForSending: channeldetails.frozenForSending + frozenForReceiving: channeldetails.frozenForReceiving } Label { diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index e66f61183..84c935de6 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -41,8 +41,10 @@ Item { property color colorProgress: '#ffffff80' property color colorDone: '#ff80ff80' - property color colorLightningLocal: "blue" + property color colorLightningLocal: "#6060ff" + property color colorLightningLocalReserve: "#0000a0" property color colorLightningRemote: "yellow" + property color colorLightningRemoteReserve: Qt.darker(colorLightningRemote, 1.5) property color colorChannelOpen: "#ff80ff80" property color colorPiechartTotal: Material.accentColor diff --git a/electrum/gui/qml/components/controls/ChannelBar.qml b/electrum/gui/qml/components/controls/ChannelBar.qml index 34378158a..0a3d3ee73 100644 --- a/electrum/gui/qml/components/controls/ChannelBar.qml +++ b/electrum/gui/qml/components/controls/ChannelBar.qml @@ -9,6 +9,10 @@ Item { property Amount capacity property Amount localCapacity property Amount remoteCapacity + property Amount canSend + property Amount canReceive + property bool frozenForSending: false + property bool frozenForReceiving: false height: 10 implicitWidth: 100 @@ -16,32 +20,56 @@ Item { onWidthChanged: { var cap = capacity.satsInt * 1000 var twocap = cap * 2 - b1.width = width * (cap - localCapacity.msatsInt) / twocap - b2.width = width * localCapacity.msatsInt / twocap - b3.width = width * remoteCapacity.msatsInt / twocap - b4.width = width * (cap - remoteCapacity.msatsInt) / twocap + l1.width = width * (cap - localCapacity.msatsInt) / twocap + if (frozenForSending) { + l2.width = width * localCapacity.msatsInt / twocap + l3.width = 0 + } else { + l2.width = width * (localCapacity.msatsInt - canSend.msatsInt) / twocap + l3.width = width * canSend.msatsInt / twocap + } + if (frozenForReceiving) { + r3.width = 0 + r2.width = width * remoteCapacity.msatsInt / twocap + } else { + r3.width = width * canReceive.msatsInt / twocap + r2.width = width * (remoteCapacity.msatsInt - canReceive.msatsInt) / twocap + } + r1.width = width * (cap - remoteCapacity.msatsInt) / twocap } Rectangle { - id: b1 + id: l1 x: 0 height: parent.height color: 'gray' } Rectangle { - id: b2 - anchors.left: b1.right + id: l2 + anchors.left: l1.right + height: parent.height + color: constants.colorLightningLocalReserve + } + Rectangle { + id: l3 + anchors.left: l2.right height: parent.height color: constants.colorLightningLocal } Rectangle { - id: b3 - anchors.left: b2.right + id: r3 + anchors.left: l3.right height: parent.height color: constants.colorLightningRemote } Rectangle { - id: b4 - anchors.left: b3.right + id: r2 + anchors.left: r3.right + height: parent.height + color: constants.colorLightningRemoteReserve + } + Rectangle { + id: r1 + anchors.left: r2.right height: parent.height color: 'gray' } diff --git a/electrum/gui/qml/components/controls/ChannelDelegate.qml b/electrum/gui/qml/components/controls/ChannelDelegate.qml index bf54d0a76..299b7433b 100644 --- a/electrum/gui/qml/components/controls/ChannelDelegate.qml +++ b/electrum/gui/qml/components/controls/ChannelDelegate.qml @@ -111,6 +111,10 @@ ItemDelegate { capacity: model.capacity localCapacity: model.local_capacity remoteCapacity: model.remote_capacity + canSend: model.can_send + canReceive: model.can_receive + frozenForSending: model.send_frozen + frozenForReceiving: model.receive_frozen } Item { diff --git a/electrum/gui/qml/qechannellistmodel.py b/electrum/gui/qml/qechannellistmodel.py index a58019f8f..1036a7f31 100644 --- a/electrum/gui/qml/qechannellistmodel.py +++ b/electrum/gui/qml/qechannellistmodel.py @@ -14,9 +14,9 @@ class QEChannelListModel(QAbstractListModel, QtEventListener): _logger = get_logger(__name__) # define listmodel rolemap - _ROLE_NAMES=('cid','state','state_code','initiator','capacity','can_send', - 'can_receive','l_csv_delay','r_csv_delay','send_frozen','receive_frozen', - 'type','node_id','node_alias','short_cid','funding_tx','is_trampoline', + _ROLE_NAMES=('cid', 'state', 'state_code', 'initiator', 'capacity', 'can_send', + 'can_receive', 'l_csv_delay', 'r_csv_delay', 'send_frozen', 'receive_frozen', + 'type', 'node_id', 'node_alias', 'short_cid', 'funding_tx', 'is_trampoline', 'is_backup', 'is_imported', 'local_capacity', 'remote_capacity') _ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) @@ -93,12 +93,16 @@ def channel_to_model(self, lnc): item['can_receive'] = QEAmount() item['local_capacity'] = QEAmount() item['remote_capacity'] = QEAmount() + item['send_frozen'] = True + item['receive_frozen'] = True item['is_imported'] = lnc.is_imported else: item['can_send'] = QEAmount(amount_msat=lnc.available_to_spend(LOCAL)) item['can_receive'] = QEAmount(amount_msat=lnc.available_to_spend(REMOTE)) item['local_capacity'] = QEAmount(amount_msat=lnc.balance(LOCAL)) item['remote_capacity'] = QEAmount(amount_msat=lnc.balance(REMOTE)) + item['send_frozen'] = lnc.is_frozen_for_sending() + item['receive_frozen'] = lnc.is_frozen_for_receiving() item['is_imported'] = False return item From 45944d280d08c883c030d9b38ac4bf79eb112ecc Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 11 May 2023 13:59:41 +0200 Subject: [PATCH 1069/1143] qml: update channelbar when conditions change --- .../qml/components/controls/ChannelBar.qml | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qml/components/controls/ChannelBar.qml b/electrum/gui/qml/components/controls/ChannelBar.qml index 0a3d3ee73..606a25934 100644 --- a/electrum/gui/qml/components/controls/ChannelBar.qml +++ b/electrum/gui/qml/components/controls/ChannelBar.qml @@ -17,7 +17,7 @@ Item { height: 10 implicitWidth: 100 - onWidthChanged: { + function update() { var cap = capacity.satsInt * 1000 var twocap = cap * 2 l1.width = width * (cap - localCapacity.msatsInt) / twocap @@ -37,6 +37,31 @@ Item { } r1.width = width * (cap - remoteCapacity.msatsInt) / twocap } + + onWidthChanged: update() + onFrozenForSendingChanged: update() + onFrozenForReceivingChanged: update() + + Connections { + target: localCapacity + function onMsatsIntChanged() { update() } + } + + Connections { + target: remoteCapacity + function onMsatsIntChanged() { update() } + } + + Connections { + target: canSend + function onMsatsIntChanged() { update() } + } + + Connections { + target: canReceive + function onMsatsIntChanged() { update() } + } + Rectangle { id: l1 x: 0 From 0428fc7c0acdff04834ae1aaa86519adea516a63 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Wed, 31 May 2023 11:11:54 +0200 Subject: [PATCH 1070/1143] qml: add explanatory infomessage when sending capacity is significantly less than local balance. Show message when attempting unfreeze of gossip channel in trampoline mode --- .../gui/qml/components/ChannelDetails.qml | 29 ++++++++++++++++--- electrum/gui/qml/qechanneldetails.py | 2 ++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qml/components/ChannelDetails.qml b/electrum/gui/qml/components/ChannelDetails.qml index dbe81e152..e2b2ee060 100644 --- a/electrum/gui/qml/components/ChannelDetails.qml +++ b/electrum/gui/qml/components/ChannelDetails.qml @@ -135,9 +135,10 @@ Pane { icon.source: '../../icons/share.png' icon.color: 'transparent' onClicked: { - var dialog = app.genericShareDialog.createObject(root, - { title: qsTr('Channel node ID'), text: channeldetails.pubkey } - ) + var dialog = app.genericShareDialog.createObject(root, { + title: qsTr('Channel node ID'), + text: channeldetails.pubkey + }) dialog.open() } } @@ -159,6 +160,18 @@ Pane { columns: 2 rowSpacing: constants.paddingSmall + InfoTextArea { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingMedium + visible: channeldetails.canSend.msatsInt < 0.5 * channeldetails.localCapacity.msatsInt + && channeldetails.localCapacity.msatsInt > 0.2 * channeldetails.capacity.msatsInt + iconStyle: InfoTextArea.IconStyle.Warn + compact: true + text: [qsTr('The amount available for sending is considerably lower than the local balance.'), + qsTr('This can occur when mempool fees are high.')].join(' ') + } + ChannelBar { Layout.columnSpan: 2 Layout.fillWidth: true @@ -222,7 +235,7 @@ Pane { } Label { - text: qsTr('Can Receive') + text: qsTr('Can receive') color: Material.accentColor } @@ -324,6 +337,14 @@ Pane { id: channeldetails wallet: Daemon.currentWallet channelid: root.channelid + onTrampolineFrozenInGossipMode: { + var dialog = app.messageDialog.createObject(root, { + title: qsTr('Cannot unfreeze channel'), + text: [qsTr('Non-Trampoline channels cannot be used for sending while in trampoline mode.'), + qsTr('Disable trampoline mode to enable sending from this channel.')].join(' ') + }) + dialog.open() + } } Component { diff --git a/electrum/gui/qml/qechanneldetails.py b/electrum/gui/qml/qechanneldetails.py index 80d8851d8..5094f8c85 100644 --- a/electrum/gui/qml/qechanneldetails.py +++ b/electrum/gui/qml/qechanneldetails.py @@ -27,6 +27,7 @@ class State: # subset, only ones we currently need in UI channelCloseSuccess = pyqtSignal() channelCloseFailed = pyqtSignal([str], arguments=['message']) isClosingChanged = pyqtSignal() + trampolineFrozenInGossipMode = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) @@ -214,6 +215,7 @@ def freezeForSending(self): self.channelChanged.emit() else: self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP) + self.trampolineFrozenInGossipMode.emit() @pyqtSlot() def freezeForReceiving(self): From 60e007862bd048f8e6ac65aa78849943da751d91 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 13 Jul 2023 19:08:23 +0200 Subject: [PATCH 1071/1143] qml: defer updates, fixes listitems not updating --- electrum/gui/qml/components/controls/ChannelBar.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qml/components/controls/ChannelBar.qml b/electrum/gui/qml/components/controls/ChannelBar.qml index 606a25934..9dcf66c0c 100644 --- a/electrum/gui/qml/components/controls/ChannelBar.qml +++ b/electrum/gui/qml/components/controls/ChannelBar.qml @@ -18,6 +18,10 @@ Item { implicitWidth: 100 function update() { + Qt.callLater(do_update) + } + + function do_update() { var cap = capacity.satsInt * 1000 var twocap = cap * 2 l1.width = width * (cap - localCapacity.msatsInt) / twocap From 6b52aad3c8c69e4e90203d9e12faf7ff04a3f46e Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 14 Jul 2023 10:28:50 +0200 Subject: [PATCH 1072/1143] qml: silence some null deref errors at shutdown --- electrum/gui/qml/components/main.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/electrum/gui/qml/components/main.qml b/electrum/gui/qml/components/main.qml index e49f88ec9..13ad78acc 100644 --- a/electrum/gui/qml/components/main.qml +++ b/electrum/gui/qml/components/main.qml @@ -121,7 +121,8 @@ ApplicationWindow MouseArea { anchors.fill: parent - enabled: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) + enabled: Daemon.currentWallet && + (!stack.currentItem || !stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) onClicked: { stack.getRoot().menu.open() // open wallet-menu stack.getRoot().menu.y = toolbar.height @@ -139,14 +140,15 @@ ApplicationWindow Image { Layout.preferredWidth: constants.iconSizeSmall Layout.preferredHeight: constants.iconSizeSmall - visible: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) + visible: Daemon.currentWallet && + (!stack.currentItem || !stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name) source: '../../icons/wallet.png' } Label { Layout.fillWidth: true Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height) - text: stack.currentItem.title + text: stack.currentItem && stack.currentItem.title ? stack.currentItem.title : Daemon.currentWallet.name elide: Label.ElideRight From b04ade5d7d891ca4afdb0001a96d434f3c32ed53 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Jul 2023 13:44:56 +0000 Subject: [PATCH 1073/1143] tests: add failing test for sweeping chan after local fclose using cb --- electrum/tests/regtest.py | 3 +++ electrum/tests/regtest/regtest.sh | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py index 53eaa3e11..806a9efc9 100644 --- a/electrum/tests/regtest.py +++ b/electrum/tests/regtest.py @@ -50,6 +50,9 @@ def test_collaborative_close(self): def test_backup(self): self.run_shell(['backup']) + def test_backup_local_forceclose(self): + self.run_shell(['backup_local_forceclose']) + def test_breach(self): self.run_shell(['breach']) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 38df96509..a47d3d56e 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -158,6 +158,31 @@ if [[ $1 == "backup" ]]; then fi +if [[ $1 == "backup_local_forceclose" ]]; then + # Alice does a local-force-close, and then restores from seed before sweeping CSV-locked coins + wait_for_balance alice 1 + echo "alice opens channel" + bob_node=$($bob nodeid) + $alice setconfig use_recoverable_channels False + channel=$($alice open_channel $bob_node 0.15) + new_blocks 3 + wait_until_channel_open alice + backup=$($alice export_channel_backup $channel) + echo "local force close $channel" + $alice close_channel $channel --force + sleep 0.5 + seed=$($alice getseed) + $alice stop + mv /tmp/alice/regtest/wallets/default_wallet /tmp/alice/regtest/wallets/default_wallet.old + new_blocks 150 + $alice -o restore "$seed" + $alice daemon -d + $alice load_wallet + $alice import_channel_backup $backup + wait_for_balance alice 0.998 +fi + + if [[ $1 == "collaborative_close" ]]; then wait_for_balance alice 1 echo "alice opens channel" From 1a46460d11533918503b90886adb944a2a9bfd4c Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Jul 2023 14:21:50 +0000 Subject: [PATCH 1074/1143] fix sweeping chan after local force-close using cb scenario: - user opens a lightning channel and exports an "imported channel backup" - user closes channel via local-force-close - local ctx is published, to_local output has user's funds and they are CSV-locked for days - user restores wallet file from seed and imports channel backup - new wallet file should be able to sweep coins from to_local output (after CSV expires) This was not working previously, as the local_payment_basepoint was not included in the imported channel backups, and the code was interpreting the lack of this as the channel not having option_static_remotekey enabled. This resulted in lnutil.extract_ctn_from_tx using an incorrect funder_payment_basepoint, and lnsweep not recognising the ctx due to the garbage ctn value. The imported channel backup serialisation format is slightly changed to include the previously missing field, and its version number is bumped (0->1). We allow importing both version 0 and version 1 backups, however v0 backups cannot handle the above described scenario (they can only be used to request a remote-force-close). Note that we were/are setting the missing local_payment_basepoint to the pubkey of one of the wallet change addresses, which is bruteforceable if necessary, but I think it is not worth the complexity to add this bruteforce logic. Also note that the bruteforcing could only be done after the local-force-close was broadcast. Ideally people with existing channels and already exported v0 backups should re-export v1 backups... Not sure how to handle this. closes https://github.com/spesmilo/electrum/issues/8516 --- electrum/lnchannel.py | 23 ++++++++--- electrum/lnsweep.py | 2 + electrum/lnutil.py | 72 +++++++++++++++++++++++++---------- electrum/lnworker.py | 6 ++- electrum/tests/test_lnutil.py | 30 ++++++++++++++- 5 files changed, 103 insertions(+), 30 deletions(-) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 2bb70e08e..bf162fa9c 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -184,6 +184,7 @@ class AbstractChannel(Logger, ABC): funding_outpoint: Outpoint node_id: bytes # note that it might not be the full 33 bytes; for OCB it is only the prefix _state: ChannelState + sweep_address: str def set_short_channel_id(self, short_id: ShortChannelID) -> None: self.short_channel_id = short_id @@ -289,10 +290,10 @@ def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]: if self._sweep_info.get(txid) is None: our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx) their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx) - if our_sweep_info is not None: + if our_sweep_info: self._sweep_info[txid] = our_sweep_info self.logger.info(f'we (local) force closed') - elif their_sweep_info is not None: + elif their_sweep_info: self._sweep_info[txid] = their_sweep_info self.logger.info(f'they (remote) force closed.') else: @@ -300,6 +301,12 @@ def sweep_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]: self.logger.info(f'not sure who closed.') return self._sweep_info[txid] + def maybe_sweep_revoked_htlc(self, ctx: Transaction, htlc_tx: Transaction) -> Optional[SweepInfo]: + return None + + def extract_preimage_from_htlc_txin(self, txin: TxInput) -> None: + return + def update_onchain_state(self, *, funding_txid: str, funding_height: TxMinedInfo, closing_txid: str, closing_height: TxMinedInfo, keep_watching: bool) -> None: # note: state transitions are irreversible, but @@ -479,15 +486,21 @@ def __init__(self, cb: ChannelBackupStorage, *, lnworker=None): Logger.__init__(self) self.config = {} if self.is_imported: + assert isinstance(cb, ImportedChannelBackupStorage) self.init_config(cb) self.unconfirmed_closing_txid = None # not a state, only for GUI - def init_config(self, cb): + def init_config(self, cb: ImportedChannelBackupStorage): + local_payment_pubkey = cb.local_payment_pubkey + if local_payment_pubkey is None: + self.logger.warning( + f"local_payment_pubkey missing from (old-type) channel backup. " + f"You should export and re-import a newer backup.") self.config[LOCAL] = LocalConfig.from_seed( channel_seed=cb.channel_seed, to_self_delay=cb.local_delay, + static_remotekey=local_payment_pubkey, # dummy values - static_remotekey=None, dust_limit_sat=None, max_htlc_value_in_flight_msat=None, max_accepted_htlcs=None, @@ -580,8 +593,6 @@ def is_frozen_for_receiving(self) -> bool: @property def sweep_address(self) -> str: - # Since channel backups do not save the static_remotekey, payment_basepoint in - # their local config is not static) return self.lnworker.wallet.get_new_sweep_address_for_channel() def get_local_pubkey(self) -> bytes: diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 10151c33c..91b672899 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -206,6 +206,7 @@ def create_sweeptxs_for_our_ctx( to_local_witness_script = make_commitment_output_to_local_witness_script( their_revocation_pubkey, to_self_delay, our_localdelayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script) + to_remote_address = None # test if this is our_ctx found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address)) if not chan.is_backup(): @@ -359,6 +360,7 @@ def create_sweeptxs_for_their_ctx( witness_script = make_commitment_output_to_local_witness_script( our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey).hex() to_local_address = redeem_script_to_address('p2wsh', witness_script) + to_remote_address = None # test if this is their ctx found_to_local = bool(ctx.get_output_idxs_from_address(to_local_address)) if not chan.is_backup(): diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 2c3b6b1bb..0447034bf 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -31,6 +31,7 @@ from .lnaddr import lndecode from .bip32 import BIP32Node, BIP32_PRIME from .transaction import BCDataStream, OPPushDataGeneric +from .logging import get_logger if TYPE_CHECKING: @@ -39,6 +40,9 @@ from .lnonion import OnionRoutingFailure +_logger = get_logger(__name__) + + # defined in BOLT-03: HTLC_TIMEOUT_WEIGHT = 663 HTLC_SUCCESS_WEIGHT = 703 @@ -192,7 +196,7 @@ class LocalConfig(ChannelConfig): per_commitment_secret_seed = attr.ib(type=bytes, converter=hex_to_bytes) @classmethod - def from_seed(self, **kwargs): + def from_seed(cls, **kwargs): channel_seed = kwargs['channel_seed'] static_remotekey = kwargs.pop('static_remotekey') node = BIP32Node.from_rootseed(channel_seed, xtype='standard') @@ -202,7 +206,11 @@ def from_seed(self, **kwargs): kwargs['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE) kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE) kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE) - kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) if static_remotekey else keypair_generator(LnKeyFamily.PAYMENT_BASE) + if static_remotekey: + kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) + else: + # we expect all our channels to use option_static_remotekey, so ending up here likely indicates an issue... + kwargs['payment_basepoint'] = keypair_generator(LnKeyFamily.PAYMENT_BASE) return LocalConfig(**kwargs) def validate_params(self, *, funding_sat: int) -> None: @@ -236,7 +244,9 @@ class ChannelConstraints(StoredObject): funding_txn_minimum_depth = attr.ib(type=int) -CHANNEL_BACKUP_VERSION = 0 +CHANNEL_BACKUP_VERSION_LATEST = 1 +KNOWN_CHANNEL_BACKUP_VERSIONS = (0, 1,) +assert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS @attr.s class ChannelBackupStorage(StoredObject): @@ -255,13 +265,13 @@ def channel_id(self): @stored_in('onchain_channel_backups') @attr.s class OnchainChannelBackupStorage(ChannelBackupStorage): - node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes) + node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes) # remote node pubkey @stored_in('imported_channel_backups') @attr.s class ImportedChannelBackupStorage(ChannelBackupStorage): - node_id = attr.ib(type=bytes, converter=hex_to_bytes) - privkey = attr.ib(type=bytes, converter=hex_to_bytes) + node_id = attr.ib(type=bytes, converter=hex_to_bytes) # remote node pubkey + privkey = attr.ib(type=bytes, converter=hex_to_bytes) # local node privkey host = attr.ib(type=str) port = attr.ib(type=int, converter=int) channel_seed = attr.ib(type=bytes, converter=hex_to_bytes) @@ -269,10 +279,11 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): remote_delay = attr.ib(type=int, converter=int) remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) + local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes] def to_bytes(self) -> bytes: vds = BCDataStream() - vds.write_uint16(CHANNEL_BACKUP_VERSION) + vds.write_uint16(CHANNEL_BACKUP_VERSION_LATEST) vds.write_boolean(self.is_initiator) vds.write_bytes(self.privkey, 32) vds.write_bytes(self.channel_seed, 32) @@ -286,6 +297,7 @@ def to_bytes(self) -> bytes: vds.write_uint16(self.remote_delay) vds.write_string(self.host) vds.write_uint16(self.port) + vds.write_bytes(self.local_payment_pubkey, 33) return bytes(vds.input) @staticmethod @@ -293,22 +305,40 @@ def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": vds = BCDataStream() vds.write(s) version = vds.read_uint16() - if version != CHANNEL_BACKUP_VERSION: + if version not in KNOWN_CHANNEL_BACKUP_VERSIONS: raise Exception(f"unknown version for channel backup: {version}") + is_initiator = vds.read_boolean() + privkey = vds.read_bytes(32) + channel_seed = vds.read_bytes(32) + node_id = vds.read_bytes(33) + funding_txid = vds.read_bytes(32).hex() + funding_index = vds.read_uint16() + funding_address = vds.read_string() + remote_payment_pubkey = vds.read_bytes(33) + remote_revocation_pubkey = vds.read_bytes(33) + local_delay = vds.read_uint16() + remote_delay = vds.read_uint16() + host = vds.read_string() + port = vds.read_uint16() + if version >= 1: + local_payment_pubkey = vds.read_bytes(33) + else: + local_payment_pubkey = None return ImportedChannelBackupStorage( - is_initiator=vds.read_boolean(), - privkey=vds.read_bytes(32), - channel_seed=vds.read_bytes(32), - node_id=vds.read_bytes(33), - funding_txid=vds.read_bytes(32).hex(), - funding_index=vds.read_uint16(), - funding_address=vds.read_string(), - remote_payment_pubkey=vds.read_bytes(33), - remote_revocation_pubkey=vds.read_bytes(33), - local_delay=vds.read_uint16(), - remote_delay=vds.read_uint16(), - host=vds.read_string(), - port=vds.read_uint16(), + is_initiator=is_initiator, + privkey=privkey, + channel_seed=channel_seed, + node_id=node_id, + funding_txid=funding_txid, + funding_index=funding_index, + funding_address=funding_address, + remote_payment_pubkey=remote_payment_pubkey, + remote_revocation_pubkey=remote_revocation_pubkey, + local_delay=local_delay, + remote_delay=remote_delay, + host=host, + port=port, + local_payment_pubkey=local_payment_pubkey, ) @staticmethod diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 10f71ae8d..3914decfe 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2537,7 +2537,7 @@ def current_feerate_per_kw(self): feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE return max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4) - def create_channel_backup(self, channel_id): + def create_channel_backup(self, channel_id: bytes): chan = self._channels[channel_id] # do not backup old-style channels assert chan.is_static_remotekey_enabled() @@ -2556,7 +2556,9 @@ def create_channel_backup(self, channel_id): local_delay = chan.config[LOCAL].to_self_delay, remote_delay = chan.config[REMOTE].to_self_delay, remote_revocation_pubkey = chan.config[REMOTE].revocation_basepoint.pubkey, - remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey) + remote_payment_pubkey = chan.config[REMOTE].payment_basepoint.pubkey, + local_payment_pubkey=chan.config[LOCAL].payment_basepoint.pubkey, + ) def export_channel_backup(self, channel_id): xpub = self.wallet.get_fingerprint() diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index 30926bcb3..592b9f7a2 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -921,7 +921,7 @@ def test_channel_type(self): self.assertEqual(ChannelType(0b10000000001000000000000), channel_type) @as_testnet - async def test_decode_imported_channel_backup(self): + async def test_decode_imported_channel_backup_v0(self): encrypted_cb = "channel_backup:Adn87xcGIs9H2kfp4VpsOaNKWCHX08wBoqq37l1cLYKGlJamTeoaLEwpJA81l1BXF3GP/mRxqkY+whZG9l51G8izIY/kmMSvnh0DOiZEdwaaT/1/MwEHfsEomruFqs+iW24SFJPHbMM7f80bDtIxcLfZkKmgcKBAOlcqtq+dL3U3yH74S8BDDe2L4snaxxpCjF0JjDMBx1UR/28D+QlIi+lbvv1JMaCGXf+AF1+3jLQf8+lVI+rvFdyArws6Ocsvjf+ANQeSGUwW6Nb2xICQcMRgr1DO7bO4pgGu408eYRr2v3ayJBVtnKwSwd49gF5SDSjTDAO4CCM0uj9H5RxyzH7fqotkd9J80MBr84RiBXAeXKz+Ap8608/FVqgQ9BOcn6LhuAQdE5zXpmbQyw5jUGkPvHuseR+rzthzncy01odUceqTNg==" config = SimpleConfig({'electrum_path': self.electrum_path}) d = restore_wallet_from_text("9dk", path=None, gap_limit=2, config=config) @@ -942,6 +942,34 @@ async def test_decode_imported_channel_backup(self): remote_delay=720, remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), + local_payment_pubkey=None, + ), + decoded_cb, + ) + + @as_testnet + async def test_decode_imported_channel_backup_v1(self): + encrypted_cb = "channel_backup:AVYIedu0qSLfY2M2bBxF6dA4RAxcmobp+3h9mxALWWsv5X7hhNg0XYOKNd11FE6BJOZgZnIZ4CCAlHtLNj0/9S5GbNhbNZiQXxeHMwC1lHvtjawkwSejIJyOI52DkDFHBAGZRd4fJjaPJRHnUizWfySVR4zjd08lTinpoIeL7C7tXBW1N6YqceqV7RpeoywlBXJtFfCCuw0hnUKgq3SMlBKapkNAIgGrg15aIHNcYeENxCxr5FD1s7DIwFSECqsBVnu/Ogx2oii8BfuxqJq8vuGq4Ib/BVaSVtdb2E1wklAor/CG0p9Fg9mFWND98JD+64nz9n/knPFFyHxTXErn+ct3ZcStsLYynWKUIocgu38PtzCJ7r5ivqOw4O49fbbzdjcgMUGklPYxjuinETneCo+dCPa1uepOGTqeOYmnjVYtYZYXOlWV1F5OtNoM7MwwJjAbz84=" + config = SimpleConfig({'electrum_path': self.electrum_path}) + d = restore_wallet_from_text("9dk", path=None, gap_limit=2, config=config) + wallet1 = d['wallet'] # type: Standard_Wallet + decoded_cb = ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet1.get_fingerprint()) + self.assertEqual( + ImportedChannelBackupStorage( + funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe', + funding_index=1, + funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp', + is_initiator=True, + node_id=bfh('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f'), + privkey=bfh('7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772'), + host='195.201.207.61', + port=9739, + channel_seed=bfh('ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a'), + local_delay=1008, + remote_delay=720, + remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), + remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), + local_payment_pubkey=bfh('0308d686712782a44b0cef220485ad83dae77853a5bf8501a92bb79056c9dcb25a'), ), decoded_cb, ) From 1767d26de92fd61726b28a9035abb6b5b7f2d0d4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Jul 2023 15:18:36 +0000 Subject: [PATCH 1075/1143] tests: make regtest tests somewhat faster by faster polling in e-x using https://github.com/spesmilo/electrumx/commit/4e66804dc0d668cd6bd4602b547e2f5b2e227e97 on my machine, before-after: Ran 9 tests in 495.865s Ran 9 tests in 376.183s --- .cirrus.yml | 4 ++-- electrum/tests/regtest/run_electrumx.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 981fffe35..1a4c8af96 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -105,8 +105,8 @@ task: - apt-get update - apt-get -y install libsecp256k1-dev curl jq bc - pip3 install .[tests] - # install e-x some commits after 1.16.0 tag, where it uses same aiorpcx as electrum - - pip3 install git+https://github.com/spesmilo/electrumx.git@c8d2cc0d5cf9e549a90ca876d85fed9a90b8c4ed + # install e-x some commits after 1.16.0 tag + - pip3 install git+https://github.com/spesmilo/electrumx.git@4e66804dc0d668cd6bd4602b547e2f5b2e227e97 - "BITCOIND_VERSION=$(curl https://bitcoincore.org/en/download/ | grep -E -i --only-matching 'Latest version: [0-9\\.]+' | grep -E --only-matching '[0-9\\.]+')" - BITCOIND_FILENAME=bitcoin-$BITCOIND_VERSION-x86_64-linux-gnu.tar.gz - BITCOIND_PATH=/tmp/bitcoind/$BITCOIND_FILENAME diff --git a/electrum/tests/regtest/run_electrumx.sh b/electrum/tests/regtest/run_electrumx.sh index e53cc1688..c1a821611 100755 --- a/electrum/tests/regtest/run_electrumx.sh +++ b/electrum/tests/regtest/run_electrumx.sh @@ -4,4 +4,4 @@ set -eux pipefail cd rm -rf $HOME/electrumx_db mkdir $HOME/electrumx_db -COST_SOFT_LIMIT=0 COST_HARD_LIMIT=0 COIN=BitcoinSegwit SERVICES=tcp://:51001,rpc:// NET=regtest DAEMON_URL=http://doggman:donkey@127.0.0.1:18554 DB_DIRECTORY=$HOME/electrumx_db electrumx_server +COST_SOFT_LIMIT=0 COST_HARD_LIMIT=0 COIN=BitcoinSegwit SERVICES=tcp://:51001,rpc:// NET=regtest DAEMON_URL=http://doggman:donkey@127.0.0.1:18554 DB_DIRECTORY=$HOME/electrumx_db DAEMON_POLL_INTERVAL_BLOCKS=100 DAEMON_POLL_INTERVAL_MEMPOOL=100 electrumx_server From 6977be8e1b48e336dbba5a4fce53e91f396109b3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 17 Jul 2023 15:10:15 +0000 Subject: [PATCH 1076/1143] util.CallbackManager: keep strong references for running futures This clears up log spam for regtest tests. related: - https://bugs.python.org/issue44665 - https://github.com/python/cpython/issues/88831 - https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/ - https://github.com/python/cpython/issues/91887#issuecomment-1434816045 - "Task was destroyed but it is pending!" Perhaps we should inspect all our usages of - asyncio.create_task - loop.create_task - asyncio.ensure_future - asyncio.run_coroutine_threadsafe ? Example log for running a regtest test: ``` $ python3 -m unittest electrum.tests.regtest.TestLightningAB.test_collaborative_close ***** test_collaborative_close ****** initializing alice --- Logging error --- Traceback (most recent call last): File "/usr/lib/python3.10/logging/__init__.py", line 1100, in emit msg = self.format(record) File "/usr/lib/python3.10/logging/__init__.py", line 943, in format return fmt.format(record) File "/home/user/wspace/electrum/electrum/logging.py", line 44, in format record = copy.copy(record) # avoid mutating arg File "/usr/lib/python3.10/copy.py", line 92, in copy rv = reductor(4) ImportError: sys.meta_path is None, Python is likely shutting down Call stack: File "/usr/lib/python3.10/asyncio/base_events.py", line 1781, in call_exception_handler self._exception_handler(self, context) File "/home/user/wspace/electrum/electrum/util.py", line 1535, in on_exception loop.default_exception_handler(context) File "/usr/lib/python3.10/asyncio/base_events.py", line 1744, in default_exception_handler logger.error('\n'.join(log_lines), exc_info=exc_info) Message: "Task was destroyed but it is pending!\ntask: wait_for= cb=[_chain_future.._call_set_state() at /usr/lib/python3.10/asyncio/futures.py:392]>" Arguments: () [--- SNIP --- more of the same --- SNIP ---] --- Logging error --- Traceback (most recent call last): File "/usr/lib/python3.10/logging/__init__.py", line 1100, in emit msg = self.format(record) File "/usr/lib/python3.10/logging/__init__.py", line 943, in format return fmt.format(record) File "/home/user/wspace/electrum/electrum/logging.py", line 44, in format record = copy.copy(record) # avoid mutating arg File "/usr/lib/python3.10/copy.py", line 92, in copy rv = reductor(4) ImportError: sys.meta_path is None, Python is likely shutting down Call stack: File "/usr/lib/python3.10/asyncio/base_events.py", line 1781, in call_exception_handler self._exception_handler(self, context) File "/home/user/wspace/electrum/electrum/util.py", line 1535, in on_exception loop.default_exception_handler(context) File "/usr/lib/python3.10/asyncio/base_events.py", line 1744, in default_exception_handler logger.error('\n'.join(log_lines), exc_info=exc_info) Message: "Task was destroyed but it is pending!\ntask: wait_for=._call_check_cancel() at /usr/lib/python3.10/asyncio/futures.py:385, Task.task_wakeup()]> cb=[_chain_future.._call_set_state() at /usr/lib/python3.10/asyncio/futures.py:392]>" Arguments: () true true true true funding alice ``` --- electrum/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/electrum/util.py b/electrum/util.py index b9cdeb13f..53fe4fc7c 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1706,6 +1706,7 @@ class CallbackManager: def __init__(self): self.callback_lock = threading.Lock() self.callbacks = defaultdict(list) # note: needs self.callback_lock + self._running_cb_futs = set() def register_callback(self, func, events): with self.callback_lock: @@ -1730,7 +1731,10 @@ def trigger_callback(self, event, *args): for callback in callbacks: # FIXME: if callback throws, we will lose the traceback if asyncio.iscoroutinefunction(callback): - asyncio.run_coroutine_threadsafe(callback(*args), loop) + fut = asyncio.run_coroutine_threadsafe(callback(*args), loop) + # keep strong references around to avoid GC issues: + self._running_cb_futs.add(fut) + fut.add_done_callback(lambda fut_: self._running_cb_futs.remove(fut_)) elif get_running_loop() == loop: # run callback immediately, so that it is guaranteed # to have been executed when this method returns From 1e3b3b82d55763e490f94dc17b363ca0857ee04a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Jul 2023 09:42:01 +0200 Subject: [PATCH 1077/1143] test_lnpeer: deepcopy graph definitions in test setup --- electrum/tests/test_lnpeer.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index e291f8e8a..caf5d9b45 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -1,5 +1,6 @@ import asyncio import shutil +import copy import tempfile from decimal import Decimal import os @@ -366,7 +367,7 @@ class PeerInTests(Peer): 'remote_fee_rate_millionths': 1, } -GRAPH_DEFINITIONS = { +_GRAPH_DEFINITIONS = { 'square_graph': { 'alice': { 'channels': { @@ -420,6 +421,7 @@ def setUpClass(cls): def setUp(self): super().setUp() + self.GRAPH_DEFINITIONS = copy.deepcopy(_GRAPH_DEFINITIONS) self._lnworkers_created = [] # type: List[MockLNWallet] async def asyncTearDown(self): @@ -923,7 +925,7 @@ async def many_payments(): @needs_test_with_all_chacha20_implementations async def test_payment_multihop(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) peers = graph.peers.values() async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash)) @@ -945,7 +947,7 @@ async def f(): @needs_test_with_all_chacha20_implementations async def test_payment_multihop_with_preselected_path(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) peers = graph.peers.values() async def pay(pay_req): with self.subTest(msg="bad path: edges do not chain together"): @@ -990,7 +992,7 @@ async def f(): @needs_test_with_all_chacha20_implementations async def test_payment_multihop_temp_node_failure(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) graph.workers['bob'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True peers = graph.peers.values() @@ -1017,7 +1019,7 @@ async def f(): async def test_payment_multihop_route_around_failure(self): # Alice will pay Dave. Alice first tries A->C->D route, due to lower fees, but Carol # will fail the htlc and get blacklisted. Alice will then try A->B->D and succeed. - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True peers = graph.peers.values() async def pay(lnaddr, pay_req): @@ -1054,7 +1056,7 @@ async def f(): @needs_test_with_all_chacha20_implementations async def test_payment_with_temp_channel_failure_and_liquidity_hints(self): # prepare channels such that a temporary channel failure happens at c->d - graph_definition = GRAPH_DEFINITIONS['square_graph'].copy() + graph_definition = self.GRAPH_DEFINITIONS['square_graph'] graph_definition['alice']['channels']['carol']['local_balance_msat'] = 200_000_000 graph_definition['alice']['channels']['carol']['remote_balance_msat'] = 200_000_000 graph_definition['carol']['channels']['dave']['local_balance_msat'] = 50_000_000 @@ -1170,12 +1172,12 @@ async def f(kwargs): @needs_test_with_all_chacha20_implementations async def test_payment_multipart_with_timeout(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) await self._run_mpp(graph, {'bob_forwarding': False}, {'bob_forwarding': True}) @needs_test_with_all_chacha20_implementations async def test_payment_multipart(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) await self._run_mpp(graph, {'mpp_invoice': False}, {'mpp_invoice': True}) async def _run_trampoline_payment(self, is_legacy, direct, drop_dave=None): @@ -1208,7 +1210,7 @@ async def f(): do_drop_dave(p) await group.spawn(pay(lnaddr, pay_req)) - graph_definition = GRAPH_DEFINITIONS['square_graph'].copy() + graph_definition = self.GRAPH_DEFINITIONS['square_graph'] if not direct: # deplete channel from alice to carol graph_definition['alice']['channels']['carol'] = depleted_channel @@ -1250,7 +1252,7 @@ async def test_payment_trampoline_e2e_indirect(self): @needs_test_with_all_chacha20_implementations async def test_payment_multipart_trampoline_e2e(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey), graph.workers['carol'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey), @@ -1266,7 +1268,7 @@ async def test_payment_multipart_trampoline_e2e(self): @needs_test_with_all_chacha20_implementations async def test_payment_multipart_trampoline_legacy(self): - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey), graph.workers['carol'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['carol'].node_keypair.pubkey), @@ -1283,7 +1285,7 @@ async def test_fail_pending_htlcs_on_shutdown(self): Dave shuts down (stops wallet). We test if Dave fails the pending HTLCs during shutdown. """ - graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) self.assertEqual(500_000_000_000, graph.channels[('alice', 'bob')].balance(LOCAL)) self.assertEqual(500_000_000_000, graph.channels[('alice', 'carol')].balance(LOCAL)) amount_to_pay = 600_000_000_000 From aeaf9c71df5a86bb07a7e39246e3d6613cc497e6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 20 Jun 2023 14:42:24 +0200 Subject: [PATCH 1078/1143] Add unit test for trampoline MPP consolidation This tests that a trampoline waits until all incoming HTLCs are received, and fail or succeed them together --- electrum/lnworker.py | 2 ++ electrum/simple_config.py | 1 + electrum/tests/test_lnpeer.py | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 10f71ae8d..b021e05cf 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1648,6 +1648,8 @@ async def create_routes_for_payment( is_mpp = sum(len(x) for x in list(sc.config.values())) > 1 if is_mpp and not invoice_features.supports(LnFeatures.BASIC_MPP_OPT): continue + if not is_mpp and self.config.TEST_FORCE_MPP: + continue self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}") routes = [] try: diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 43f835a5e..b0feab9e5 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -893,6 +893,7 @@ def get_swapserver_url(self): EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool) TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool) TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool) + TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool) TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int) TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None) TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index caf5d9b45..490be6f9c 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -1180,7 +1180,7 @@ async def test_payment_multipart(self): graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) await self._run_mpp(graph, {'mpp_invoice': False}, {'mpp_invoice': True}) - async def _run_trampoline_payment(self, is_legacy, direct, drop_dave=None): + async def _run_trampoline_payment(self, is_legacy, direct, drop_dave=None, test_mpp_consolidation=False): if drop_dave is None: drop_dave = [] async def pay(lnaddr, pay_req): @@ -1217,7 +1217,16 @@ async def f(): # insert a channel from bob to carol graph_definition['bob']['channels']['carol'] = high_fee_channel + if test_mpp_consolidation: + # deplete alice to carol so that all htlcs go through bob + graph_definition['alice']['channels']['carol'] = depleted_channel + graph = self.prepare_chans_and_peers_in_graph(graph_definition) + + if test_mpp_consolidation: + graph.workers['dave'].features |= LnFeatures.BASIC_MPP_OPT + graph.workers['alice'].network.config.TEST_FORCE_MPP = True + peers = graph.peers.values() if is_legacy: # turn off trampoline features in invoice @@ -1231,6 +1240,11 @@ async def f(): await f() + @needs_test_with_all_chacha20_implementations + async def test_trampoline_mpp_consolidation(self): + with self.assertRaises(PaymentDone): + await self._run_trampoline_payment(is_legacy=True, direct=False, test_mpp_consolidation=True) + @needs_test_with_all_chacha20_implementations async def test_payment_trampoline_legacy(self): with self.assertRaises(PaymentDone): From e124ff7ee73923686437bed91907c7fabd21567c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 11 Jul 2023 11:23:12 +0200 Subject: [PATCH 1079/1143] Trampoline MPP consolidation: - fix parameters passed to maybe_forward_trampoline - use lnworker.trampoline_forwardings as a semaphore for ongoing trampoline payments - if a trampoline payment fails, fail all received HTLCs --- electrum/lnpeer.py | 38 ++++++++++++++++++++++++++--------- electrum/lnworker.py | 1 + electrum/tests/test_lnpeer.py | 1 + 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index c0532800c..8c3c2bbb9 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1670,8 +1670,9 @@ def log_fail_reason(reason: str): def maybe_forward_trampoline( self, *, - chan: Channel, - htlc: UpdateAddHtlc, + payment_hash: bytes, + cltv_expiry: int, + outer_onion: ProcessedOnionPacket, trampoline_onion: ProcessedOnionPacket): forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS @@ -1679,9 +1680,7 @@ def maybe_forward_trampoline( if not (forwarding_enabled and forwarding_trampoline_enabled): self.logger.info(f"trampoline forwarding is disabled. failing htlc.") raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') - payload = trampoline_onion.hop_data.payload - payment_hash = htlc.payment_hash payment_data = payload.get('payment_data') if payment_data: # legacy case payment_secret = payment_data['payment_secret'] @@ -1709,8 +1708,10 @@ def maybe_forward_trampoline( # these are the fee/cltv paid by the sender # pay_to_node will raise if they are not sufficient - trampoline_cltv_delta = htlc.cltv_expiry - cltv_from_onion - trampoline_fee = htlc.amount_msat - amt_to_forward + trampoline_cltv_delta = cltv_expiry - cltv_from_onion + total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"] + trampoline_fee = total_msat - amt_to_forward + self.logger.info(f'trampoline cltv and fee: {trampoline_cltv_delta, trampoline_fee}') @log_exceptions async def forward_trampoline_payment(): @@ -1735,6 +1736,14 @@ async def forward_trampoline_payment(): error_reason = OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') self.lnworker.trampoline_forwarding_failures[payment_hash] = error_reason + # remove from list of payments, so that another attempt can be initiated + self.lnworker.trampoline_forwardings.remove(payment_hash) + + # add to list of ongoing payments + self.lnworker.trampoline_forwardings.add(payment_hash) + # clear previous failures + self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None) + # start payment asyncio.ensure_future(forward_trampoline_payment()) def maybe_fulfill_htlc( @@ -2335,12 +2344,13 @@ def process_unfulfilled_htlc( chan=chan, htlc=htlc, processed_onion=processed_onion) + if trampoline_onion_packet: # trampoline- recipient or forwarding if not forwarding_info: trampoline_onion = self.process_onion_packet( trampoline_onion_packet, - payment_hash=htlc.payment_hash, + payment_hash=payment_hash, onion_packet_bytes=onion_packet_bytes, is_trampoline=True) if trampoline_onion.are_we_final: @@ -2354,16 +2364,24 @@ def process_unfulfilled_htlc( # trampoline- HTLC we are supposed to forward, but haven't forwarded yet if not self.lnworker.enable_htlc_forwarding: return None, None, None + + if payment_hash in self.lnworker.trampoline_forwardings: + self.logger.info(f"we are already forwarding this.") + # we are already forwarding this payment + return None, True, None + self.maybe_forward_trampoline( - chan=chan, - htlc=htlc, + payment_hash=payment_hash, + cltv_expiry=htlc.cltv_expiry, # TODO: use max or enforce same value across mpp parts + outer_onion=processed_onion, trampoline_onion=trampoline_onion) # return True so that this code gets executed only once return None, True, None else: # trampoline- HTLC we are supposed to forward, and have already forwarded preimage = self.lnworker.get_preimage(payment_hash) - error_reason = self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None) + # get (and not pop) failure because the incoming payment might be multi-part + error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_hash) if error_reason: self.logger.info(f'trampoline forwarding failure: {error_reason.code_name()}') raise error_reason diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b021e05cf..fcb142b72 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -682,6 +682,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): for payment_hash in self.get_payments(status='inflight').keys(): self.set_invoice_status(payment_hash.hex(), PR_INFLIGHT) + self.trampoline_forwardings = set() self.trampoline_forwarding_failures = {} # todo: should be persisted # map forwarded htlcs (fw_info=(scid_hex, htlc_id)) to originating peer pubkeys self.downstream_htlc_to_upstream_peer_map = {} # type: Dict[Tuple[str, int], bytes] diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 490be6f9c..86c1c0e42 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -169,6 +169,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.sent_htlcs = defaultdict(asyncio.Queue) self.sent_htlcs_info = dict() self.sent_buckets = defaultdict(set) + self.trampoline_forwardings = set() self.trampoline_forwarding_failures = {} self.inflight_payments = set() self.preimages = {} From 827792c14cb11cfca7c8c91af2724c8b15fa2df4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 19 Jul 2023 15:38:07 +0200 Subject: [PATCH 1080/1143] lnpeer: simplify maybe_fulfill_htlc --- electrum/lnpeer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 8c3c2bbb9..e92a02bea 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1751,10 +1751,10 @@ def maybe_fulfill_htlc( chan: Channel, htlc: UpdateAddHtlc, processed_onion: ProcessedOnionPacket, - is_trampoline: bool = False) -> Tuple[Optional[bytes], Optional[OnionPacket]]: + is_trampoline: bool = False) -> Optional[bytes]: """As a final recipient of an HTLC, decide if we should fulfill it. - Return (preimage, trampoline_onion_packet) with at most a single element not None + Return preimage or None """ def log_fail_reason(reason: str): self.logger.info(f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. " @@ -1812,7 +1812,7 @@ def log_fail_reason(reason: str): payment_status = self.lnworker.check_received_htlc(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) if payment_status is None: - return None, None + return None elif payment_status is False: log_fail_reason(f"MPP_TIMEOUT") raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') @@ -1822,7 +1822,7 @@ def log_fail_reason(reason: str): # if there is a trampoline_onion, maybe_fulfill_htlc will be called again if processed_onion.trampoline_onion_packet: # TODO: we should check that all trampoline_onions are the same - return None, processed_onion.trampoline_onion_packet + return None # TODO don't accept payments twice for same invoice # TODO check invoice expiry @@ -1845,7 +1845,7 @@ def log_fail_reason(reason: str): if preimage: self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") self.lnworker.set_request_status(htlc.payment_hash, PR_PAID) - return preimage, None + return preimage def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") @@ -2340,22 +2340,22 @@ def process_unfulfilled_htlc( onion_packet_bytes=onion_packet_bytes) if processed_onion.are_we_final: # either we are final recipient; or if trampoline, see cases below - preimage, trampoline_onion_packet = self.maybe_fulfill_htlc( + preimage = self.maybe_fulfill_htlc( chan=chan, htlc=htlc, processed_onion=processed_onion) - if trampoline_onion_packet: + if processed_onion.trampoline_onion_packet: # trampoline- recipient or forwarding if not forwarding_info: trampoline_onion = self.process_onion_packet( - trampoline_onion_packet, + processed_onion.trampoline_onion_packet, payment_hash=payment_hash, onion_packet_bytes=onion_packet_bytes, is_trampoline=True) if trampoline_onion.are_we_final: # trampoline- we are final recipient of HTLC - preimage, _ = self.maybe_fulfill_htlc( + preimage = self.maybe_fulfill_htlc( chan=chan, htlc=htlc, processed_onion=trampoline_onion, From 017186d10771070cd1730027e0bffac15202d120 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Jul 2023 11:55:59 +0200 Subject: [PATCH 1081/1143] Refactor trampoline forwarding and hold invoices. - maybe_fulfill_htlc returns a forwarding callback that covers both cases. - previously, the callback of hold invoices was called as a side-effect of lnworker.check_mpp_status. - the same data structures (lnworker.trampoline_forwardings, lnworker.trampoline_forwarding_errors) are used for both trampoline forwardings and hold invoices. - maybe_fulfill_htlc still recursively calls itself to perform checks on trampoline onion. This is ugly, but ugliness is now contained to that method. --- electrum/lnpeer.py | 161 +++++++++++++++++++--------------- electrum/lnworker.py | 26 ++---- electrum/tests/test_lnpeer.py | 4 +- 3 files changed, 98 insertions(+), 93 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index e92a02bea..356a2ed1a 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -9,7 +9,7 @@ import asyncio import os import time -from typing import Tuple, Dict, TYPE_CHECKING, Optional, Union, Set +from typing import Tuple, Dict, TYPE_CHECKING, Optional, Union, Set, Callable from datetime import datetime import functools @@ -1668,7 +1668,8 @@ def log_fail_reason(reason: str): next_peer.maybe_send_commitment(next_chan) return next_chan_scid, next_htlc.htlc_id - def maybe_forward_trampoline( + @log_exceptions + async def maybe_forward_trampoline( self, *, payment_hash: bytes, cltv_expiry: int, @@ -1713,48 +1714,34 @@ def maybe_forward_trampoline( trampoline_fee = total_msat - amt_to_forward self.logger.info(f'trampoline cltv and fee: {trampoline_cltv_delta, trampoline_fee}') - @log_exceptions - async def forward_trampoline_payment(): - try: - await self.lnworker.pay_to_node( - node_pubkey=outgoing_node_id, - payment_hash=payment_hash, - payment_secret=payment_secret, - amount_to_pay=amt_to_forward, - min_cltv_expiry=cltv_from_onion, - r_tags=[], - invoice_features=invoice_features, - fwd_trampoline_onion=next_trampoline_onion, - fwd_trampoline_fee=trampoline_fee, - fwd_trampoline_cltv_delta=trampoline_cltv_delta, - attempts=1) - except OnionRoutingFailure as e: - # FIXME: cannot use payment_hash as key - self.lnworker.trampoline_forwarding_failures[payment_hash] = e - except PaymentFailure as e: - # FIXME: adapt the error code - error_reason = OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') - self.lnworker.trampoline_forwarding_failures[payment_hash] = error_reason - - # remove from list of payments, so that another attempt can be initiated - self.lnworker.trampoline_forwardings.remove(payment_hash) - - # add to list of ongoing payments - self.lnworker.trampoline_forwardings.add(payment_hash) - # clear previous failures - self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None) - # start payment - asyncio.ensure_future(forward_trampoline_payment()) + try: + await self.lnworker.pay_to_node( + node_pubkey=outgoing_node_id, + payment_hash=payment_hash, + payment_secret=payment_secret, + amount_to_pay=amt_to_forward, + min_cltv_expiry=cltv_from_onion, + r_tags=[], + invoice_features=invoice_features, + fwd_trampoline_onion=next_trampoline_onion, + fwd_trampoline_fee=trampoline_fee, + fwd_trampoline_cltv_delta=trampoline_cltv_delta, + attempts=1) + except OnionRoutingFailure as e: + raise + except PaymentFailure as e: + # FIXME: adapt the error code + raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') def maybe_fulfill_htlc( self, *, chan: Channel, htlc: UpdateAddHtlc, processed_onion: ProcessedOnionPacket, - is_trampoline: bool = False) -> Optional[bytes]: - + onion_packet_bytes: bytes, + is_trampoline: bool = False) -> Tuple[Optional[bytes], Optional[Callable]]: """As a final recipient of an HTLC, decide if we should fulfill it. - Return preimage or None + Return (preimage, forwarding_callback) with at most a single element not None """ def log_fail_reason(reason: str): self.logger.info(f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. " @@ -1810,19 +1797,55 @@ def log_fail_reason(reason: str): log_fail_reason(f"'payment_secret' missing from onion") raise exc_incorrect_or_unknown_pd - payment_status = self.lnworker.check_received_htlc(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) + payment_status = self.lnworker.check_mpp_status(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) if payment_status is None: - return None + return None, None elif payment_status is False: log_fail_reason(f"MPP_TIMEOUT") raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') else: assert payment_status is True + payment_hash = htlc.payment_hash + preimage = self.lnworker.get_preimage(payment_hash) + hold_invoice_callback = self.lnworker.hold_invoice_callbacks.get(payment_hash) + if not preimage and hold_invoice_callback: + if preimage: + return preimage, None + else: + # for hold invoices, trigger callback + cb, timeout = hold_invoice_callback + if int(time.time()) < timeout: + return None, lambda: cb(payment_hash) + else: + raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') + # if there is a trampoline_onion, maybe_fulfill_htlc will be called again if processed_onion.trampoline_onion_packet: # TODO: we should check that all trampoline_onions are the same - return None + + trampoline_onion = self.process_onion_packet( + processed_onion.trampoline_onion_packet, + payment_hash=payment_hash, + onion_packet_bytes=onion_packet_bytes, + is_trampoline=True) + if trampoline_onion.are_we_final: + # trampoline- we are final recipient of HTLC + preimage, cb = self.maybe_fulfill_htlc( + chan=chan, + htlc=htlc, + processed_onion=trampoline_onion, + onion_packet_bytes=onion_packet_bytes, + is_trampoline=True) + assert cb is None + return preimage, None + else: + callback = lambda: self.maybe_forward_trampoline( + payment_hash=payment_hash, + cltv_expiry=htlc.cltv_expiry, # TODO: use max or enforce same value across mpp parts + outer_onion=processed_onion, + trampoline_onion=trampoline_onion) + return None, callback # TODO don't accept payments twice for same invoice # TODO check invoice expiry @@ -1845,7 +1868,7 @@ def log_fail_reason(reason: str): if preimage: self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") self.lnworker.set_request_status(htlc.payment_hash, PR_PAID) - return preimage + return preimage, None def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") @@ -2340,42 +2363,36 @@ def process_unfulfilled_htlc( onion_packet_bytes=onion_packet_bytes) if processed_onion.are_we_final: # either we are final recipient; or if trampoline, see cases below - preimage = self.maybe_fulfill_htlc( + preimage, forwarding_callback = self.maybe_fulfill_htlc( chan=chan, htlc=htlc, - processed_onion=processed_onion) + processed_onion=processed_onion, + onion_packet_bytes=onion_packet_bytes) - if processed_onion.trampoline_onion_packet: - # trampoline- recipient or forwarding + if forwarding_callback: if not forwarding_info: - trampoline_onion = self.process_onion_packet( - processed_onion.trampoline_onion_packet, - payment_hash=payment_hash, - onion_packet_bytes=onion_packet_bytes, - is_trampoline=True) - if trampoline_onion.are_we_final: - # trampoline- we are final recipient of HTLC - preimage = self.maybe_fulfill_htlc( - chan=chan, - htlc=htlc, - processed_onion=trampoline_onion, - is_trampoline=True) + # trampoline- HTLC we are supposed to forward, but haven't forwarded yet + if not self.lnworker.enable_htlc_forwarding: + pass + elif payment_hash in self.lnworker.trampoline_forwardings: + # we are already forwarding this payment + self.logger.info(f"we are already forwarding this.") else: - # trampoline- HTLC we are supposed to forward, but haven't forwarded yet - if not self.lnworker.enable_htlc_forwarding: - return None, None, None - - if payment_hash in self.lnworker.trampoline_forwardings: - self.logger.info(f"we are already forwarding this.") - # we are already forwarding this payment - return None, True, None - - self.maybe_forward_trampoline( - payment_hash=payment_hash, - cltv_expiry=htlc.cltv_expiry, # TODO: use max or enforce same value across mpp parts - outer_onion=processed_onion, - trampoline_onion=trampoline_onion) - # return True so that this code gets executed only once + # add to list of ongoing payments + self.lnworker.trampoline_forwardings.add(payment_hash) + # clear previous failures + self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None) + async def wrapped_callback(): + forwarding_coro = forwarding_callback() + try: + await forwarding_coro + except Exception as e: + # FIXME: cannot use payment_hash as key + self.lnworker.trampoline_forwarding_failures[payment_hash] = e + finally: + # remove from list of payments, so that another attempt can be initiated + self.lnworker.trampoline_forwardings.remove(payment_hash) + asyncio.ensure_future(wrapped_callback()) return None, True, None else: # trampoline- HTLC we are supposed to forward, and have already forwarded diff --git a/electrum/lnworker.py b/electrum/lnworker.py index fcb142b72..caa4bf852 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1922,16 +1922,16 @@ def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> if write_to_disk: self.wallet.save_db() - def check_received_htlc( - self, payment_secret: bytes, - short_channel_id: ShortChannelID, - htlc: UpdateAddHtlc, - expected_msat: int, + + def check_mpp_status( + self, payment_secret: bytes, + short_channel_id: ShortChannelID, + htlc: UpdateAddHtlc, + expected_msat: int, ) -> Optional[bool]: """ return MPP status: True (accepted), False (expired) or None (waiting) """ payment_hash = htlc.payment_hash - self.update_mpp_with_received_htlc(payment_secret, short_channel_id, htlc, expected_msat) is_expired, is_accepted = self.get_mpp_status(payment_secret) if not is_accepted and not is_expired: @@ -1944,19 +1944,7 @@ def check_received_htlc( elif self.stopping_soon: is_expired = True # try to time out pending HTLCs before shutting down elif all([self.is_mpp_amount_reached(x) for x in payment_secrets]): - preimage = self.get_preimage(payment_hash) - hold_invoice_callback = self.hold_invoice_callbacks.get(payment_hash) - if not preimage and hold_invoice_callback: - # for hold invoices, trigger callback - cb, timeout = hold_invoice_callback - if int(time.time()) < timeout: - cb(payment_hash) - else: - is_expired = True - else: - # note: preimage will be None for outer trampoline onion - is_accepted = True - + is_accepted = True elif time.time() - first_timestamp > self.MPP_EXPIRY: is_expired = True diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 86c1c0e42..7b8a805d7 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -251,7 +251,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln set_request_status = LNWallet.set_request_status set_payment_status = LNWallet.set_payment_status get_payment_status = LNWallet.get_payment_status - check_received_htlc = LNWallet.check_received_htlc + check_mpp_status = LNWallet.check_mpp_status htlc_fulfilled = LNWallet.htlc_fulfilled htlc_failed = LNWallet.htlc_failed save_preimage = LNWallet.save_preimage @@ -764,7 +764,7 @@ async def pay(lnaddr, pay_req): if test_hold_invoice: payment_hash = lnaddr.paymenthash preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) - def cb(payment_hash): + async def cb(payment_hash): if not test_hold_timeout: w2.save_preimage(payment_hash, preimage) timeout = 1 if test_hold_timeout else 60 From 0e5cb194085297421cfc3bc70dcb2b70bf8999e8 Mon Sep 17 00:00:00 2001 From: 3rd Iteration Date: Fri, 21 Jul 2023 11:14:56 -0400 Subject: [PATCH 1082/1143] Add Vendor/Device IDs for CH340 based DIY Jade devices. (#8546) * Add Vendor/Device IDs for CH340 based DIY Jade devices. * Add device descriptions to hardwareIDs --- electrum/plugins/jade/jade.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/plugins/jade/jade.py b/electrum/plugins/jade/jade.py index f4638eafd..c29004109 100644 --- a/electrum/plugins/jade/jade.py +++ b/electrum/plugins/jade/jade.py @@ -350,7 +350,10 @@ def show_address_multi(self, multisig_name, paths): class JadePlugin(HW_PluginBase): keystore_class = Jade_KeyStore minimum_library = (0, 0, 1) - DEVICE_IDS = [(0x10c4, 0xea60), (0x1a86, 0x55d4), (0x0403, 0x6001)] + DEVICE_IDS = [(0x10c4, 0xea60), # Development Jade device + (0x1a86, 0x55d4), # Retail Blockstream Jade (And some DIY devices) + (0x0403, 0x6001), # DIY FTDI Based Devices (Eg: M5StickC-Plus) + (0x1a86, 0x7523)] # DIY CH340 Based devices (Eg: ESP32-Wrover) SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') MIN_SUPPORTED_FW_VERSION = (0, 1, 32) From 8630030bd92ac4468fcbfb71a6c782f89a8e7abd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 21 Jul 2023 19:32:38 +0200 Subject: [PATCH 1083/1143] Restrict exception type in trampoline_forwarding_failures (follow-up 017186d10771070cd1730027e0bffac15202d120) --- electrum/lnpeer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 356a2ed1a..39c36a9bb 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -2386,7 +2386,7 @@ async def wrapped_callback(): forwarding_coro = forwarding_callback() try: await forwarding_coro - except Exception as e: + except OnionRoutingFailure as e: # FIXME: cannot use payment_hash as key self.lnworker.trampoline_forwarding_failures[payment_hash] = e finally: From 1296d3361de50ae37d48f485714c74d57532d4ea Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 11 Jul 2023 15:43:21 +0200 Subject: [PATCH 1084/1143] use payment_secret instead of payment_hash --- electrum/lnpeer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 39c36a9bb..f29a64773 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -2369,36 +2369,37 @@ def process_unfulfilled_htlc( processed_onion=processed_onion, onion_packet_bytes=onion_packet_bytes) + payment_secret = processed_onion.hop_data.payload["payment_data"]["payment_secret"] + payment_key = payment_hash + payment_secret if forwarding_callback: if not forwarding_info: # trampoline- HTLC we are supposed to forward, but haven't forwarded yet if not self.lnworker.enable_htlc_forwarding: pass - elif payment_hash in self.lnworker.trampoline_forwardings: + elif payment_key in self.lnworker.trampoline_forwardings: # we are already forwarding this payment self.logger.info(f"we are already forwarding this.") else: # add to list of ongoing payments - self.lnworker.trampoline_forwardings.add(payment_hash) + self.lnworker.trampoline_forwardings.add(payment_key) # clear previous failures - self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None) + self.lnworker.trampoline_forwarding_failures.pop(payment_key, None) async def wrapped_callback(): forwarding_coro = forwarding_callback() try: await forwarding_coro except OnionRoutingFailure as e: - # FIXME: cannot use payment_hash as key - self.lnworker.trampoline_forwarding_failures[payment_hash] = e + self.lnworker.trampoline_forwarding_failures[payment_key] = e finally: # remove from list of payments, so that another attempt can be initiated - self.lnworker.trampoline_forwardings.remove(payment_hash) + self.lnworker.trampoline_forwardings.remove(payment_key) asyncio.ensure_future(wrapped_callback()) return None, True, None else: # trampoline- HTLC we are supposed to forward, and have already forwarded preimage = self.lnworker.get_preimage(payment_hash) # get (and not pop) failure because the incoming payment might be multi-part - error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_hash) + error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_key) if error_reason: self.logger.info(f'trampoline forwarding failure: {error_reason.code_name()}') raise error_reason From 141cd524bc4881cee21dc5d265c4a80ffe4b033d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 23 Jul 2023 09:40:39 +0200 Subject: [PATCH 1085/1143] lnpeer: do not run maybe_fulfill_htlc more than once, if it triggered a payment forwarding. Final onions may trigger a payment forwarding, through the callback returned by maybe_fulfill_htlc. In that case, we should not fail the HTLC later; doing so might result in fund loss. Remove test_simple_payment_with_hold_invoice_timing_out: once we have accepted to forward a payment HTLC with a hold invoice, we do not want to time it out, for the same reason. --- electrum/lnpeer.py | 67 ++++++++++++++++++----------------- electrum/tests/test_lnpeer.py | 12 ++----- 2 files changed, 36 insertions(+), 43 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index f29a64773..da0aa356c 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1744,9 +1744,9 @@ def maybe_fulfill_htlc( Return (preimage, forwarding_callback) with at most a single element not None """ def log_fail_reason(reason: str): - self.logger.info(f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. " - f"{reason}. htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}") - + self.logger.info( + f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. " + f"{reason}. htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}") try: amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"] except Exception: @@ -1809,7 +1809,7 @@ def log_fail_reason(reason: str): payment_hash = htlc.payment_hash preimage = self.lnworker.get_preimage(payment_hash) hold_invoice_callback = self.lnworker.hold_invoice_callbacks.get(payment_hash) - if not preimage and hold_invoice_callback: + if hold_invoice_callback: if preimage: return preimage, None else: @@ -1818,12 +1818,11 @@ def log_fail_reason(reason: str): if int(time.time()) < timeout: return None, lambda: cb(payment_hash) else: - raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') + raise exc_incorrect_or_unknown_pd # if there is a trampoline_onion, maybe_fulfill_htlc will be called again if processed_onion.trampoline_onion_packet: # TODO: we should check that all trampoline_onions are the same - trampoline_onion = self.process_onion_packet( processed_onion.trampoline_onion_packet, payment_hash=payment_hash, @@ -1849,15 +1848,18 @@ def log_fail_reason(reason: str): # TODO don't accept payments twice for same invoice # TODO check invoice expiry - info = self.lnworker.get_payment_info(htlc.payment_hash) + info = self.lnworker.get_payment_info(payment_hash) if info is None: log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}") raise exc_incorrect_or_unknown_pd - preimage = self.lnworker.get_preimage(htlc.payment_hash) + + preimage = self.lnworker.get_preimage(payment_hash) + if not preimage: + self.logger.info(f"missing callback {payment_hash.hex()}") + return None, None + expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)] - if preimage: - # legacy secret for old invoices - expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) + expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) # legacy secret for old invoices if payment_secret_from_onion not in expected_payment_secrets: log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}') raise exc_incorrect_or_unknown_pd @@ -1865,9 +1867,7 @@ def log_fail_reason(reason: str): if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat): log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}") raise exc_incorrect_or_unknown_pd - if preimage: - self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") - self.lnworker.set_request_status(htlc.payment_hash, PR_PAID) + self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") return preimage, None def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): @@ -2301,6 +2301,7 @@ async def htlc_switch(self): self.lnworker.downstream_htlc_to_upstream_peer_map[fw_info] = self.pubkey elif preimage or error_reason or error_bytes: if preimage: + self.lnworker.set_request_status(htlc.payment_hash, PR_PAID) if not self.lnworker.enable_htlc_settle: continue self.fulfill_htlc(chan, htlc.htlc_id, preimage) @@ -2363,16 +2364,15 @@ def process_unfulfilled_htlc( onion_packet_bytes=onion_packet_bytes) if processed_onion.are_we_final: # either we are final recipient; or if trampoline, see cases below - preimage, forwarding_callback = self.maybe_fulfill_htlc( - chan=chan, - htlc=htlc, - processed_onion=processed_onion, - onion_packet_bytes=onion_packet_bytes) - - payment_secret = processed_onion.hop_data.payload["payment_data"]["payment_secret"] - payment_key = payment_hash + payment_secret - if forwarding_callback: - if not forwarding_info: + if not forwarding_info: + preimage, forwarding_callback = self.maybe_fulfill_htlc( + chan=chan, + htlc=htlc, + processed_onion=processed_onion, + onion_packet_bytes=onion_packet_bytes) + if forwarding_callback: + payment_secret = processed_onion.hop_data.payload["payment_data"]["payment_secret"] + payment_key = payment_hash + payment_secret # trampoline- HTLC we are supposed to forward, but haven't forwarded yet if not self.lnworker.enable_htlc_forwarding: pass @@ -2394,15 +2394,16 @@ async def wrapped_callback(): # remove from list of payments, so that another attempt can be initiated self.lnworker.trampoline_forwardings.remove(payment_key) asyncio.ensure_future(wrapped_callback()) - return None, True, None - else: - # trampoline- HTLC we are supposed to forward, and have already forwarded - preimage = self.lnworker.get_preimage(payment_hash) - # get (and not pop) failure because the incoming payment might be multi-part - error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_key) - if error_reason: - self.logger.info(f'trampoline forwarding failure: {error_reason.code_name()}') - raise error_reason + return None, payment_key, None + else: + payment_key = forwarding_info + # trampoline- HTLC we are supposed to forward, and have already forwarded + preimage = self.lnworker.get_preimage(payment_hash) + # get (and not pop) failure because the incoming payment might be multi-part + error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_key) + if error_reason: + self.logger.info(f'trampoline forwarding failure: {error_reason.code_name()}') + raise error_reason elif not forwarding_info: # HTLC we are supposed to forward, but haven't forwarded yet diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 7b8a805d7..d2eafa6f2 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -745,7 +745,6 @@ async def _test_simple_payment( self, test_trampoline: bool, test_hold_invoice=False, - test_hold_timeout=False, test_bundle=False, test_bundle_timeout=False ): @@ -765,10 +764,8 @@ async def pay(lnaddr, pay_req): payment_hash = lnaddr.paymenthash preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) async def cb(payment_hash): - if not test_hold_timeout: - w2.save_preimage(payment_hash, preimage) - timeout = 1 if test_hold_timeout else 60 - w2.register_callback_for_hold_invoice(payment_hash, cb, timeout) + w2.save_preimage(payment_hash, preimage) + w2.register_callback_for_hold_invoice(payment_hash, cb, 60) if test_bundle: lnaddr2, pay_req2 = self.prepare_invoice(w2) @@ -817,11 +814,6 @@ async def test_simple_payment_with_hold_invoice(self): with self.assertRaises(PaymentDone): await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True) - async def test_simple_payment_with_hold_invoice_timing_out(self): - for test_trampoline in [False, True]: - with self.assertRaises(PaymentFailure): - await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True, test_hold_timeout=True) - @needs_test_with_all_chacha20_implementations async def test_payment_race(self): """Alice and Bob pay each other simultaneously. From 3bb5ebf137674c0a06d76d5be9f105c3e9525d22 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 24 Jul 2023 16:39:28 +0200 Subject: [PATCH 1086/1143] simplify check_mpp_status (the distinction that was between is_accepted and is_expired does not seem to be useful) --- electrum/lnworker.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index caa4bf852..aa1c44f22 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1922,7 +1922,6 @@ def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> if write_to_disk: self.wallet.save_db() - def check_mpp_status( self, payment_secret: bytes, short_channel_id: ShortChannelID, @@ -1948,11 +1947,7 @@ def check_mpp_status( elif time.time() - first_timestamp > self.MPP_EXPIRY: is_expired = True - if is_accepted: - # accept only the current part of a bundle - self.set_mpp_status(payment_secret, is_expired, is_accepted) - elif is_expired: - # .. but expire all parts + if is_accepted or is_expired: for x in payment_secrets: if x in self.received_mpp_htlcs: self.set_mpp_status(x, is_expired, is_accepted) From 49b5bf99ae4daa1f8d36b9928228af9c294b82b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 24 Jul 2023 17:22:44 +0200 Subject: [PATCH 1087/1143] fw_info: use hex value of payment_key, as this is persisted --- electrum/lnpeer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index da0aa356c..b5df86830 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -2394,10 +2394,11 @@ async def wrapped_callback(): # remove from list of payments, so that another attempt can be initiated self.lnworker.trampoline_forwardings.remove(payment_key) asyncio.ensure_future(wrapped_callback()) - return None, payment_key, None + fw_info = payment_key.hex() + return None, fw_info, None else: - payment_key = forwarding_info # trampoline- HTLC we are supposed to forward, and have already forwarded + payment_key = bytes.fromhex(forwarding_info) preimage = self.lnworker.get_preimage(payment_hash) # get (and not pop) failure because the incoming payment might be multi-part error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_key) From 4d847690968683d895e3e2a2a723d7fdea2c5ef6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 24 Jul 2023 17:56:12 +0200 Subject: [PATCH 1088/1143] Qt: Show notification instead of popup if a lightning payment fails. --- electrum/gui/qt/main_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 71f652417..9b3c31724 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1195,10 +1195,11 @@ def on_event_payment_failed(self, wallet, key, reason): return invoice = self.wallet.get_invoice(key) if invoice and invoice.is_lightning() and invoice.get_address(): + # fixme: we should display this popup only if the user initiated the payment if self.question(_('Payment failed') + '\n\n' + reason + '\n\n'+ 'Fallback to onchain payment?'): self.send_tab.pay_onchain_dialog(invoice.get_outputs()) else: - self.show_error(_('Payment failed') + '\n\n' + reason) + self.notify(_('Payment failed') + '\n\n' + reason) def get_coins(self, **kwargs) -> Sequence[PartialTxInput]: coins = self.get_manually_selected_coins() From e5ac521d38735117c583b0eba1b10ed2500fbc84 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 26 Jul 2023 18:40:38 +0200 Subject: [PATCH 1089/1143] maybe_fulfill_htlc: check trampoline before hold invoice order is important: if we receive a trampoline onion for a hold invoice, we need to peel the onion through the recursive call. --- electrum/lnpeer.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index b5df86830..da7530499 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1807,20 +1807,11 @@ def log_fail_reason(reason: str): assert payment_status is True payment_hash = htlc.payment_hash - preimage = self.lnworker.get_preimage(payment_hash) - hold_invoice_callback = self.lnworker.hold_invoice_callbacks.get(payment_hash) - if hold_invoice_callback: - if preimage: - return preimage, None - else: - # for hold invoices, trigger callback - cb, timeout = hold_invoice_callback - if int(time.time()) < timeout: - return None, lambda: cb(payment_hash) - else: - raise exc_incorrect_or_unknown_pd + # detect callback # if there is a trampoline_onion, maybe_fulfill_htlc will be called again + # order is important: if we receive a trampoline onion for a hold invoice, we need to peel the onion first. + if processed_onion.trampoline_onion_packet: # TODO: we should check that all trampoline_onions are the same trampoline_onion = self.process_onion_packet( @@ -1836,8 +1827,10 @@ def log_fail_reason(reason: str): processed_onion=trampoline_onion, onion_packet_bytes=onion_packet_bytes, is_trampoline=True) - assert cb is None - return preimage, None + if preimage: + return preimage, None + else: + return cb, None else: callback = lambda: self.maybe_forward_trampoline( payment_hash=payment_hash, @@ -1846,6 +1839,19 @@ def log_fail_reason(reason: str): trampoline_onion=trampoline_onion) return None, callback + preimage = self.lnworker.get_preimage(payment_hash) + hold_invoice_callback = self.lnworker.hold_invoice_callbacks.get(payment_hash) + if hold_invoice_callback: + if preimage: + return preimage, None + else: + # for hold invoices, trigger callback + cb, timeout = hold_invoice_callback + if int(time.time()) < timeout: + return None, lambda: cb(payment_hash) + else: + raise exc_incorrect_or_unknown_pd + # TODO don't accept payments twice for same invoice # TODO check invoice expiry info = self.lnworker.get_payment_info(payment_hash) From 83dcc5e4cc4c776adb732d8ac97ba8c352a1d38a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 26 Jul 2023 18:52:12 +0200 Subject: [PATCH 1090/1143] payment bundles: fix bundle detection for trampoline This feels a bit like workaround; it might be better to represent payment bundles objects using payment secrets rather than payment hashes. --- electrum/lnworker.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index aa1c44f22..33acb0d00 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1935,8 +1935,14 @@ def check_mpp_status( is_expired, is_accepted = self.get_mpp_status(payment_secret) if not is_accepted and not is_expired: bundle = self.get_payment_bundle(payment_hash) - payment_hashes = bundle or [payment_hash] - payment_secrets = [self.get_payment_secret(h) for h in bundle] if bundle else [payment_secret] + if bundle: + payment_secrets = [self.get_payment_secret(h) for h in bundle] + if payment_secret not in payment_secrets: + # outer trampoline onion secret differs from inner onion + # the latter, not the former, might be part of a bundle + payment_secrets = [payment_secret] + else: + payment_secrets = [payment_secret] first_timestamp = min([self.get_first_timestamp_of_mpp(x) for x in payment_secrets]) if self.get_payment_status(payment_hash) == PR_PAID: is_accepted = True From 8bd1292e9ab43d68b8010b6e5b3fd165c29a8121 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 26 Jul 2023 19:20:02 +0200 Subject: [PATCH 1091/1143] follow-up e5ac521d38735117c583b0eba1b10ed2500fbc84 --- electrum/lnpeer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index da7530499..2ba240e97 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1830,7 +1830,7 @@ def log_fail_reason(reason: str): if preimage: return preimage, None else: - return cb, None + return None, cb else: callback = lambda: self.maybe_forward_trampoline( payment_hash=payment_hash, From 098c65d73244bdffbcae4d4051656d41cbec46b8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 20 Oct 2022 13:40:01 +0200 Subject: [PATCH 1092/1143] submarine swap server plugin: - hold invoices - uses the same web API as the Boltz backend --- electrum/plugins/swapserver/__init__.py | 6 + electrum/plugins/swapserver/cmdline.py | 31 ++++ electrum/plugins/swapserver/qt.py | 31 ++++ electrum/plugins/swapserver/server.py | 138 ++++++++++++++++ electrum/plugins/swapserver/swapserver.py | 58 +++++++ electrum/simple_config.py | 4 +- electrum/submarine_swaps.py | 187 +++++++++++++++++++--- electrum/tests/regtest.py | 3 + electrum/tests/regtest/regtest.sh | 25 ++- 9 files changed, 455 insertions(+), 28 deletions(-) create mode 100644 electrum/plugins/swapserver/__init__.py create mode 100644 electrum/plugins/swapserver/cmdline.py create mode 100644 electrum/plugins/swapserver/qt.py create mode 100644 electrum/plugins/swapserver/server.py create mode 100644 electrum/plugins/swapserver/swapserver.py diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py new file mode 100644 index 000000000..867fb643e --- /dev/null +++ b/electrum/plugins/swapserver/__init__.py @@ -0,0 +1,6 @@ +from electrum.i18n import _ + +fullname = _('SwapServer') +description = '' + +available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/swapserver/cmdline.py b/electrum/plugins/swapserver/cmdline.py new file mode 100644 index 000000000..7add7a386 --- /dev/null +++ b/electrum/plugins/swapserver/cmdline.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from .swapserver import SwapServerPlugin + +class Plugin(SwapServerPlugin): + pass + diff --git a/electrum/plugins/swapserver/qt.py b/electrum/plugins/swapserver/qt.py new file mode 100644 index 000000000..7add7a386 --- /dev/null +++ b/electrum/plugins/swapserver/qt.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from .swapserver import SwapServerPlugin + +class Plugin(SwapServerPlugin): + pass + diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py new file mode 100644 index 000000000..6a2f2ad70 --- /dev/null +++ b/electrum/plugins/swapserver/server.py @@ -0,0 +1,138 @@ +import os +import asyncio +import attr +import random +from collections import defaultdict + +from aiohttp import ClientResponse +from aiohttp import web, client_exceptions +from aiorpcx import timeout_after, TaskTimeout, ignore_after +from aiorpcx import NetAddress + + +from electrum.util import log_exceptions, ignore_exceptions +from electrum.logging import Logger +from electrum.util import EventListener, event_listener +from electrum.invoices import PR_PAID, PR_EXPIRED + + +class SwapServer(Logger, EventListener): + """ + public API: + - getpairs + - createswap + """ + + WWW_DIR = os.path.join(os.path.dirname(__file__), 'www') + + def __init__(self, config, wallet): + Logger.__init__(self) + self.config = config + self.wallet = wallet + self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS) + self.register_callbacks() # eventlistener + + self.pending = defaultdict(asyncio.Event) + self.pending_msg = {} + + @ignore_exceptions + @log_exceptions + async def run(self): + self.root = '/root' + app = web.Application() + app.add_routes([web.get('/api/getpairs', self.get_pairs)]) + app.add_routes([web.post('/api/createswap', self.create_swap)]) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context()) + await site.start() + self.logger.info(f"now running and listening. addr={self.addr}") + + async def get_pairs(self, r): + sm = self.wallet.lnworker.swap_manager + sm.init_pairs() + pairs = { + "info": [], + "warnings": [], + "pairs": { + "BTC/BTC": { + "hash": "dfe692a026d6964601bfd79703611af333d1d5aa49ef5fedd288f5a620fced60", + "rate": 1, + "limits": { + "maximal": sm._max_amount, + "minimal": sm._min_amount, + "maximalZeroConf": { + "baseAsset": 0, + "quoteAsset": 0 + } + }, + "fees": { + "percentage": 0.5, + "minerFees": { + "baseAsset": { + "normal": sm.normal_fee, + "reverse": { + "claim": sm.claim_fee, + "lockup": sm.lockup_fee + } + }, + "quoteAsset": { + "normal": sm.normal_fee, + "reverse": { + "claim": sm.claim_fee, + "lockup": sm.lockup_fee + } + } + } + } + } + } + } + return web.json_response(pairs) + + async def create_swap(self, r): + sm = self.wallet.lnworker.swap_manager + sm.init_pairs() + request = await r.json() + req_type = request['type'] + assert request['pairId'] == 'BTC/BTC' + if req_type == 'reversesubmarine': + lightning_amount_sat=request['invoiceAmount'] + payment_hash=bytes.fromhex(request['preimageHash']) + their_pubkey=bytes.fromhex(request['claimPublicKey']) + assert len(payment_hash) == 32 + assert len(their_pubkey) == 33 + swap, payment_hash, invoice = sm.add_server_swap( + lightning_amount_sat=lightning_amount_sat, + payment_hash=payment_hash, + their_pubkey=their_pubkey + ) + response = { + 'id': payment_hash.hex(), + 'invoice': invoice, + 'minerFeeInvoice': None, + 'lockupAddress': swap.lockup_address, + 'redeemScript': swap.redeem_script.hex(), + 'timeoutBlockHeight': swap.locktime, + "onchainAmount": swap.onchain_amount, + } + elif req_type == 'submarine': + their_invoice=request['invoice'] + their_pubkey=bytes.fromhex(request['refundPublicKey']) + assert len(their_pubkey) == 33 + swap, payment_hash, invoice = sm.add_server_swap( + invoice=their_invoice, + their_pubkey=their_pubkey + ) + response = { + "id": payment_hash.hex(), + "acceptZeroConf": False, + "expectedAmount": swap.onchain_amount, + "timeoutBlockHeight": swap.locktime, + "address": swap.lockup_address, + "redeemScript": swap.redeem_script.hex() + } + else: + raise Exception('unsupported request type:' + req_type) + return web.json_response(response) diff --git a/electrum/plugins/swapserver/swapserver.py b/electrum/plugins/swapserver/swapserver.py new file mode 100644 index 000000000..d061c8bc1 --- /dev/null +++ b/electrum/plugins/swapserver/swapserver.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import asyncio +import os +import random +from electrum.plugin import BasePlugin, hook +from electrum.util import log_exceptions, ignore_exceptions +from electrum import ecc + +from .server import SwapServer + + +class SwapServerPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.config = config + self.server = None + + @hook + def daemon_wallet_loaded(self, daemon, wallet): + # we use the first wallet loaded + if self.server is not None: + return + if self.config.get('offline'): + return + + self.server = SwapServer(self.config, wallet) + sm = wallet.lnworker.swap_manager + jobs = [ + sm.pay_pending_invoices(), + self.server.run(), + ] + asyncio.run_coroutine_threadsafe(daemon._run(jobs=jobs), daemon.asyncio_loop) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index b0feab9e5..92ed68396 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -966,7 +966,7 @@ def get_swapserver_url(self): # submarine swap server SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str) SWAPSERVER_URL_TESTNET = ConfigVar('swapserver_url_testnet', default='https://swaps.electrum.org/testnet', type_=str) - SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='https://localhost/api', type_=str) + SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='http://localhost:5455/api', type_=str) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) @@ -981,6 +981,8 @@ def get_swapserver_url(self): PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str) PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool) + SWAPSERVER_ADDRESS = ConfigVar('swapserver_address', default='localhost:5455', type_=str) + PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 4e7c7fe8e..32dda9eae 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -33,7 +33,10 @@ +CLAIM_FEE_SIZE = 136 +LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs +MIN_LOCKTIME_DELTA = 60 WITNESS_TEMPLATE_SWAP = [ opcodes.OP_HASH160, @@ -102,14 +105,11 @@ class SwapData(StoredObject): is_redeemed = attr.ib(type=bool) _funding_prevout = None # type: Optional[TxOutpoint] # for RBF - __payment_hash = None + _payment_hash = None @property def payment_hash(self) -> bytes: - if self.__payment_hash is None: - self.__payment_hash = sha256(self.preimage) - return self.__payment_hash - + return self._payment_hash def create_claim_tx( *, @@ -139,6 +139,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): Logger.__init__(self) self.normal_fee = 0 self.lockup_fee = 0 + self.claim_fee = 0 # part of the boltz prococol, not used by Electrum self.percentage = 0 self._min_amount = None self._max_amount = None @@ -149,6 +150,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): self._swaps_by_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData] self._swaps_by_lockup_address = {} # type: Dict[str, SwapData] for payment_hash, swap in self.swaps.items(): + swap._payment_hash = bytes.fromhex(payment_hash) self._add_or_reindex_swap(swap) self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash @@ -171,6 +173,30 @@ def start_network(self, *, network: 'Network', lnwatcher: 'LNWalletWatcher'): continue self.add_lnwatcher_callback(swap) + async def pay_pending_invoices(self): + # for server + self.invoices_to_pay = set() + while True: + await asyncio.sleep(1) + for key in list(self.invoices_to_pay): + swap = self.swaps.get(key) + if not swap: + continue + invoice = self.wallet.get_invoice(key) + if not invoice: + continue + current_height = self.network.get_local_height() + delta = swap.locktime - current_height + if delta <= MIN_LOCKTIME_DELTA: + # fixme: should consider cltv of ln payment + self.logger.info(f'locktime too close {key}') + continue + success, log = await self.lnworker.pay_invoice(invoice.lightning_invoice, attempts=1) + if not success: + self.logger.info(f'failed to pay invoice {key}') + continue + self.invoices_to_pay.remove(key) + @log_exceptions async def _claim_swap(self, swap: SwapData) -> None: assert self.network @@ -187,9 +213,31 @@ async def _claim_swap(self, swap: SwapData) -> None: swap.funding_txid = txin.prevout.txid.hex() swap._funding_prevout = txin.prevout self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint + funding_conf = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex()).conf spent_height = txin.spent_height + + if swap.is_reverse and swap.preimage is None: + if funding_conf <= 0: + continue + preimage = self.lnworker.get_preimage(swap.payment_hash) + if preimage is None: + self.invoices_to_pay.add(swap.payment_hash.hex()) + continue + swap.preimage = preimage + if spent_height is not None: swap.spending_txid = txin.spent_txid + if not swap.is_reverse and swap.preimage is None: + # we need to extract the preimage, add it to lnwatcher + # + tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) + preimage = tx.inputs()[0].witness_elements()[1] + assert swap.payment_hash == sha256(preimage) + swap.preimage = preimage + self.logger.info(f'found preimage: {preimage.hex()}') + self.lnworker.preimages[swap.payment_hash.hex()] = preimage.hex() + # note: we must check the payment secret before we broadcast the funding tx + if spent_height > 0: if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: self.logger.info(f'stop watching swap {swap.lockup_address}') @@ -205,6 +253,10 @@ async def _claim_swap(self, swap: SwapData) -> None: if not swap.is_reverse and delta < 0: # too early for refund return + # + if swap.is_reverse and swap.preimage is None: + self.logger.info('preimage not available yet') + continue try: tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) except BelowDustLimit: @@ -215,11 +267,14 @@ async def _claim_swap(self, swap: SwapData) -> None: swap.spending_txid = tx.txid() def get_claim_fee(self): - return self._get_claim_fee(config=self.wallet.config) + return self.get_fee(CLAIM_FEE_SIZE) + + def get_fee(self, size): + return self._get_fee(size=size, config=self.wallet.config) @classmethod - def _get_claim_fee(cls, *, config: 'SimpleConfig'): - return config.estimate_fee(136, allow_fallback_to_static_rates=True) + def _get_fee(cls, *, size, config: 'SimpleConfig'): + return config.estimate_fee(size, allow_fallback_to_static_rates=True) def get_swap(self, payment_hash: bytes) -> Optional[SwapData]: # for history @@ -234,6 +289,73 @@ def add_lnwatcher_callback(self, swap: SwapData) -> None: callback = lambda: self._claim_swap(swap) self.lnwatcher.add_callback(swap.lockup_address, callback) + async def hold_invoice_callback(self, payment_hash): + key = payment_hash.hex() + if key in self.swaps: + swap = self.swaps[key] + if swap.funding_txid is None: + await self.start_normal_swap(swap, None, None) + + def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoice=None, their_pubkey=None): + from .bitcoin import construct_script + from .crypto import ripemd + from .lnaddr import lndecode + from .invoices import Invoice + + locktime = self.network.get_local_height() + 140 + privkey = os.urandom(32) + our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) + is_reverse_for_server = (invoice is not None) + if is_reverse_for_server: + # client is doing a normal swap + lnaddr = lndecode(invoice) + payment_hash = lnaddr.paymenthash + lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int + onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) + redeem_script = construct_script( + WITNESS_TEMPLATE_SWAP, + {1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey} + ) + self.wallet.save_invoice(Invoice.from_bech32(invoice)) + else: + onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) + lnaddr, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=payment_hash, + amount_msat=lightning_amount_sat * 1000, + message='Submarine swap', + expiry=3600 * 24, + fallback_address=None, + channels=None, + ) + # add payment info to lnworker + self.lnworker.add_payment_info_for_hold_invoice(payment_hash, lightning_amount_sat) + self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback, 60*60*24) + redeem_script = construct_script( + WITNESS_TEMPLATE_REVERSE_SWAP, + {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} + ) + lockup_address = script_to_p2wsh(redeem_script) + receive_address = self.wallet.get_receiving_address() + swap = SwapData( + redeem_script = bytes.fromhex(redeem_script), + locktime = locktime, + privkey = privkey, + preimage = None, + prepay_hash = None, + lockup_address = lockup_address, + onchain_amount = onchain_amount_sat, + receive_address = receive_address, + lightning_amount = lightning_amount_sat, + is_reverse = is_reverse_for_server, + is_redeemed = False, + funding_txid = None, + spending_txid = None, + ) + swap._payment_hash = payment_hash + self._add_or_reindex_swap(swap) + self.add_lnwatcher_callback(swap) + return swap, payment_hash, invoice + async def normal_swap( self, *, @@ -304,18 +426,6 @@ async def normal_swap( # verify that they are not locking up funds for more than a day if locktime - self.network.get_local_height() >= 144: raise Exception("fswap check failed: locktime too far in future") - # create funding tx - # note: rbf must not decrease payment - # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output - funding_output = PartialTxOutput.from_address_and_value(lockup_address, onchain_amount) - if tx is None: - tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password) - else: - dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), expected_onchain_amount_sat) - tx.outputs().remove(dummy_output) - tx.add_outputs([funding_output]) - tx.set_rbf(True) - self.wallet.sign_transaction(tx, password) # save swap data in wallet in case we need a refund receive_address = self.wallet.get_receiving_address() swap = SwapData( @@ -325,7 +435,7 @@ async def normal_swap( preimage = preimage, prepay_hash = None, lockup_address = lockup_address, - onchain_amount = expected_onchain_amount_sat, + onchain_amount = onchain_amount, receive_address = receive_address, lightning_amount = lightning_amount_sat, is_reverse = False, @@ -333,10 +443,28 @@ async def normal_swap( funding_txid = None, spending_txid = None, ) + swap._payment_hash = payment_hash self._add_or_reindex_swap(swap) self.add_lnwatcher_callback(swap) + return await self.start_normal_swap(swap, tx, password) + + @log_exceptions + async def start_normal_swap(self, swap, tx, password): + # create funding tx + # note: rbf must not decrease payment + # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output + funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount) + if tx is None: + tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password) + else: + dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), swap.onchain_amount) + tx.outputs().remove(dummy_output) + tx.add_outputs([funding_output]) + tx.set_rbf(True) + self.wallet.sign_transaction(tx, password) await self.network.broadcast_transaction(tx) - return tx.txid() + swap.funding_txid = tx.txid() + return swap.funding_txid async def reverse_swap( self, @@ -401,7 +529,7 @@ async def reverse_swap( raise Exception(f"rswap check failed: onchain_amount is less than what we expected: " f"{onchain_amount} < {expected_onchain_amount_sat}") # verify that we will have enough time to get our tx confirmed - if locktime - self.network.get_local_height() <= 60: + if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA: raise Exception("rswap check failed: locktime too close") # verify invoice preimage_hash lnaddr = self.lnworker._check_invoice(invoice) @@ -435,6 +563,7 @@ async def reverse_swap( funding_txid = None, spending_txid = None, ) + swap._payment_hash = preimage_hash self._add_or_reindex_swap(swap) # add callback to lnwatcher self.add_lnwatcher_callback(swap) @@ -459,6 +588,15 @@ def _add_or_reindex_swap(self, swap: SwapData) -> None: self._swaps_by_funding_outpoint[swap._funding_prevout] = swap self._swaps_by_lockup_address[swap.lockup_address] = swap + def init_pairs(self) -> None: + """ for server """ + self.percentage = 0.5 + self._min_amount = 20000 + self._max_amount = 10000000 + self.normal_fee = self.get_fee(CLAIM_FEE_SIZE) + self.lockup_fee = self.get_fee(LOCKUP_FEE_SIZE) + self.claim_fee = self.get_fee(CLAIM_FEE_SIZE) + async def get_pairs(self) -> None: """Might raise SwapServerError.""" from .network import Network @@ -479,6 +617,7 @@ async def get_pairs(self) -> None: self.percentage = fees['percentage'] self.normal_fee = fees['minerFees']['baseAsset']['normal'] self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] + self.claim_fee = fees['minerFees']['baseAsset']['reverse']['claim'] limits = pairs['pairs']['BTC/BTC']['limits'] self._min_amount = limits['minimal'] self._max_amount = limits['maximal'] @@ -650,7 +789,7 @@ def _create_and_sign_claim_tx( ) -> PartialTransaction: # FIXME the mining fee should depend on swap.is_reverse. # the txs are not the same size... - amount_sat = txin.value_sats() - cls._get_claim_fee(config=config) + amount_sat = txin.value_sats() - cls._get_fee(size=CLAIM_FEE_SIZE, config=config) if amount_sat < dust_threshold(): raise BelowDustLimit() if swap.is_reverse: # successful reverse swap diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py index 53eaa3e11..090425c60 100644 --- a/electrum/tests/regtest.py +++ b/electrum/tests/regtest.py @@ -47,6 +47,9 @@ class TestLightningAB(TestLightning): def test_collaborative_close(self): self.run_shell(['collaborative_close']) + def test_submarine_swap(self): + self.run_shell(['reverse_swap']) + def test_backup(self): self.run_shell(['backup']) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 38df96509..288127816 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -83,10 +83,11 @@ if [[ $1 == "init" ]]; then # alice is funded, bob is listening if [[ $2 == "bob" ]]; then $bob setconfig --offline lightning_listen localhost:9735 - else - echo "funding $2" - $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 + $bob setconfig --offline use_swapserver true + #else fi + echo "funding $2" + $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 fi @@ -170,6 +171,24 @@ if [[ $1 == "collaborative_close" ]]; then fi +if [[ $1 == "reverse_swap" ]]; then + wait_for_balance alice 1 + echo "alice opens channel" + bob_node=$($bob nodeid) + channel=$($alice open_channel $bob_node 0.15) + new_blocks 3 + wait_until_channel_open alice + echo "alice initiates swap" + dryrun=$($alice reverse_swap 0.02 dryrun) + echo $dryrun | jq + onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") + $alice reverse_swap 0.02 $onchain_amount + new_blocks 1 + sleep 1 + new_blocks 1 +fi + + if [[ $1 == "extract_preimage" ]]; then # instead of settling bob will broadcast $bob enable_htlc_settle false From 351ff1e4b568ccbe43edf66fd8078c5f2d4a0660 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 15 Jun 2023 12:04:36 +0200 Subject: [PATCH 1093/1143] swapserver: support prepayment of fees --- electrum/plugins/swapserver/server.py | 6 +++--- electrum/submarine_swaps.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 6a2f2ad70..da961af1c 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -103,7 +103,7 @@ async def create_swap(self, r): their_pubkey=bytes.fromhex(request['claimPublicKey']) assert len(payment_hash) == 32 assert len(their_pubkey) == 33 - swap, payment_hash, invoice = sm.add_server_swap( + swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap( lightning_amount_sat=lightning_amount_sat, payment_hash=payment_hash, their_pubkey=their_pubkey @@ -111,7 +111,7 @@ async def create_swap(self, r): response = { 'id': payment_hash.hex(), 'invoice': invoice, - 'minerFeeInvoice': None, + 'minerFeeInvoice': prepay_invoice, 'lockupAddress': swap.lockup_address, 'redeemScript': swap.redeem_script.hex(), 'timeoutBlockHeight': swap.locktime, @@ -121,7 +121,7 @@ async def create_swap(self, r): their_invoice=request['invoice'] their_pubkey=bytes.fromhex(request['refundPublicKey']) assert len(their_pubkey) == 33 - swap, payment_hash, invoice = sm.add_server_swap( + swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap( invoice=their_invoice, their_pubkey=their_pubkey ) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 32dda9eae..47ef82b54 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -317,19 +317,32 @@ def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoi {1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey} ) self.wallet.save_invoice(Invoice.from_bech32(invoice)) + prepay_invoice = None else: onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) + prepay_amount_sat = self.get_claim_fee() * 2 + main_amount_sat = lightning_amount_sat - prepay_amount_sat lnaddr, invoice = self.lnworker.get_bolt11_invoice( payment_hash=payment_hash, - amount_msat=lightning_amount_sat * 1000, + amount_msat=main_amount_sat * 1000, message='Submarine swap', expiry=3600 * 24, fallback_address=None, channels=None, ) # add payment info to lnworker - self.lnworker.add_payment_info_for_hold_invoice(payment_hash, lightning_amount_sat) + self.lnworker.add_payment_info_for_hold_invoice(payment_hash, main_amount_sat) self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback, 60*60*24) + prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) + _, prepay_invoice = self.lnworker.get_bolt11_invoice( + payment_hash=prepay_hash, + amount_msat=prepay_amount_sat * 1000, + message='prepay', + expiry=3600 * 24, + fallback_address=None, + channels=None, + ) + self.lnworker.bundle_payments([payment_hash, prepay_hash]) redeem_script = construct_script( WITNESS_TEMPLATE_REVERSE_SWAP, {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} @@ -354,7 +367,7 @@ def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoi swap._payment_hash = payment_hash self._add_or_reindex_swap(swap) self.add_lnwatcher_callback(swap) - return swap, payment_hash, invoice + return swap, payment_hash, invoice, prepay_invoice async def normal_swap( self, From 1411b75584002647931e82972df84f10e339c575 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 28 Jun 2023 14:23:32 +0200 Subject: [PATCH 1094/1143] swapserver: add test for refund path --- electrum/commands.py | 8 +++--- electrum/lnworker.py | 5 ++++ electrum/simple_config.py | 1 + electrum/submarine_swaps.py | 24 +++++++++++------ electrum/tests/regtest.py | 7 +++-- electrum/tests/regtest/regtest.sh | 43 ++++++++++++++++++++++++++++--- 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 77838f22e..2d55c4bb0 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1318,22 +1318,22 @@ async def reverse_swap(self, lightning_amount, onchain_amount, wallet: Abstract_ await sm.get_pairs() lightning_amount_sat = satoshis(lightning_amount) onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True) - success = None + funding_txid = None elif lightning_amount == 'dryrun': await sm.get_pairs() onchain_amount_sat = satoshis(onchain_amount) lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True) - success = None + funding_txid = None else: lightning_amount_sat = satoshis(lightning_amount) claim_fee = sm.get_claim_fee() onchain_amount_sat = satoshis(onchain_amount) + claim_fee - success = await wallet.lnworker.swap_manager.reverse_swap( + funding_txid = await wallet.lnworker.swap_manager.reverse_swap( lightning_amount_sat=lightning_amount_sat, expected_onchain_amount_sat=onchain_amount_sat, ) return { - 'success': success, + 'funding_txid': funding_txid, 'lightning_amount': format_satoshis(lightning_amount_sat), 'onchain_amount': format_satoshis(onchain_amount_sat), } diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 33acb0d00..f928bd6e2 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2704,3 +2704,8 @@ def maybe_add_backup_from_tx(self, tx): self._channel_backups[bfh(channel_id)] = cb util.trigger_callback('channels_updated', self.wallet) self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) + + def fail_trampoline_forwarding(self, payment_key): + """ use this to fail htlcs received for hold invoices""" + e = OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') + self.trampoline_forwarding_failures[payment_key] = e diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 92ed68396..0ccfafb14 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -967,6 +967,7 @@ def get_swapserver_url(self): SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str) SWAPSERVER_URL_TESTNET = ConfigVar('swapserver_url_testnet', default='https://swaps.electrum.org/testnet', type_=str) SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='http://localhost:5455/api', type_=str) + TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 47ef82b54..122b2e07f 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -232,11 +232,18 @@ async def _claim_swap(self, swap: SwapData) -> None: # tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) preimage = tx.inputs()[0].witness_elements()[1] - assert swap.payment_hash == sha256(preimage) - swap.preimage = preimage - self.logger.info(f'found preimage: {preimage.hex()}') - self.lnworker.preimages[swap.payment_hash.hex()] = preimage.hex() - # note: we must check the payment secret before we broadcast the funding tx + if sha256(preimage) == swap.payment_hash: + swap.preimage = preimage + self.logger.info(f'found preimage: {preimage.hex()}') + self.lnworker.preimages[swap.payment_hash.hex()] = preimage.hex() + # note: we must check the payment secret before we broadcast the funding tx + else: + # refund tx + if spent_height > 0: + self.logger.info(f'found confirmed refund') + payment_secret = self.lnworker.get_payment_secret(swap.payment_hash) + payment_key = swap.payment_hash + payment_secret + self.lnworker.fail_trampoline_forwarding(payment_key) if spent_height > 0: if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: @@ -257,6 +264,8 @@ async def _claim_swap(self, swap: SwapData) -> None: if swap.is_reverse and swap.preimage is None: self.logger.info('preimage not available yet') continue + if swap.is_reverse and self.network.config.TEST_SWAPSERVER_REFUND: + continue try: tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) except BelowDustLimit: @@ -586,13 +595,12 @@ async def reverse_swap( asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10)) # we return if we detect funding async def wait_for_funding(swap): - while swap.spending_txid is None: + while swap.funding_txid is None: await asyncio.sleep(1) # initiate main payment tasks = [asyncio.create_task(self.lnworker.pay_invoice(invoice, attempts=10, channels=channels)), asyncio.create_task(wait_for_funding(swap))] await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - success = swap.spending_txid is not None - return success + return swap.funding_txid def _add_or_reindex_swap(self, swap: SwapData) -> None: if swap.payment_hash.hex() not in self.swaps: diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py index 090425c60..263b79663 100644 --- a/electrum/tests/regtest.py +++ b/electrum/tests/regtest.py @@ -47,8 +47,11 @@ class TestLightningAB(TestLightning): def test_collaborative_close(self): self.run_shell(['collaborative_close']) - def test_submarine_swap(self): - self.run_shell(['reverse_swap']) + def test_swapserver_success(self): + self.run_shell(['swapserver_success']) + + def test_swapserver_refund(self): + self.run_shell(['swapserver_refund']) def test_backup(self): self.run_shell(['backup']) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 288127816..363d7c585 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -15,6 +15,19 @@ function new_blocks() $bitcoin_cli generatetoaddress $1 $($bitcoin_cli getnewaddress) > /dev/null } +function wait_until_htlcs_settled() +{ + msg="wait until $1's local_unsettled_sent is zero" + cmd="./run_electrum --regtest -D /tmp/$1" + while unsettled=$($alice list_channels | jq '.[] | .local_unsettled_sent') && [ $unsettled != "0" ]; do + sleep 1 + msg="$msg." + printf "$msg\r" + done + printf "\n" +} + + function wait_for_balance() { msg="wait until $1's balance reaches $2" @@ -171,7 +184,7 @@ if [[ $1 == "collaborative_close" ]]; then fi -if [[ $1 == "reverse_swap" ]]; then +if [[ $1 == "swapserver_success" ]]; then wait_for_balance alice 1 echo "alice opens channel" bob_node=$($bob nodeid) @@ -180,12 +193,34 @@ if [[ $1 == "reverse_swap" ]]; then wait_until_channel_open alice echo "alice initiates swap" dryrun=$($alice reverse_swap 0.02 dryrun) - echo $dryrun | jq onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") - $alice reverse_swap 0.02 $onchain_amount + swap=$($alice reverse_swap 0.02 $onchain_amount) + echo $swap | jq + funding_txid=$(echo $swap| jq -r ".funding_txid") new_blocks 1 - sleep 1 + wait_until_spent $funding_txid 0 + wait_until_htlcs_settled alice +fi + + +if [[ $1 == "swapserver_refund" ]]; then + $alice setconfig test_swapserver_refund true + wait_for_balance alice 1 + echo "alice opens channel" + bob_node=$($bob nodeid) + channel=$($alice open_channel $bob_node 0.15) + new_blocks 3 + wait_until_channel_open alice + echo "alice initiates swap" + dryrun=$($alice reverse_swap 0.02 dryrun) + onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") + swap=$($alice reverse_swap 0.02 $onchain_amount) + echo $swap | jq + funding_txid=$(echo $swap| jq -r ".funding_txid") + new_blocks 140 + wait_until_spent $funding_txid 0 new_blocks 1 + wait_until_htlcs_settled alice fi From 69a1242ea8e7a168c6162d78e05589735d67c9af Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 25 Jul 2023 14:52:47 +0200 Subject: [PATCH 1095/1143] restructure submarine_swaps._claim_swap --- electrum/submarine_swaps.py | 67 +++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 122b2e07f..bd4074dcb 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -215,21 +215,25 @@ async def _claim_swap(self, swap: SwapData) -> None: self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint funding_conf = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex()).conf spent_height = txin.spent_height - - if swap.is_reverse and swap.preimage is None: - if funding_conf <= 0: - continue - preimage = self.lnworker.get_preimage(swap.payment_hash) - if preimage is None: - self.invoices_to_pay.add(swap.payment_hash.hex()) - continue - swap.preimage = preimage - if spent_height is not None: swap.spending_txid = txin.spent_txid - if not swap.is_reverse and swap.preimage is None: - # we need to extract the preimage, add it to lnwatcher - # + if spent_height > 0: + if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: + self.logger.info(f'stop watching swap {swap.lockup_address}') + self.lnwatcher.remove_callback(swap.lockup_address) + swap.is_redeemed = True + elif spent_height == TX_HEIGHT_LOCAL: + if txin.block_height > 0 or self.wallet.config.LIGHTNING_ALLOW_INSTANT_SWAPS: + tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) + self.logger.info(f'broadcasting tx {txin.spent_txid}') + await self.network.broadcast_transaction(tx) + else: + # spending tx is in mempool + pass + + if not swap.is_reverse: + if swap.preimage is None and spent_height is not None: + # extract the preimage, add it to lnwatcher tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) preimage = tx.inputs()[0].witness_elements()[1] if sha256(preimage) == swap.payment_hash: @@ -245,26 +249,23 @@ async def _claim_swap(self, swap: SwapData) -> None: payment_key = swap.payment_hash + payment_secret self.lnworker.fail_trampoline_forwarding(payment_key) - if spent_height > 0: - if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: - self.logger.info(f'stop watching swap {swap.lockup_address}') - self.lnwatcher.remove_callback(swap.lockup_address) - swap.is_redeemed = True - elif spent_height == TX_HEIGHT_LOCAL: - if txin.block_height > 0 or self.wallet.config.LIGHTNING_ALLOW_INSTANT_SWAPS: - tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) - self.logger.info(f'broadcasting tx {txin.spent_txid}') - await self.network.broadcast_transaction(tx) - # already in mempool - continue - if not swap.is_reverse and delta < 0: - # too early for refund - return - # - if swap.is_reverse and swap.preimage is None: - self.logger.info('preimage not available yet') - continue - if swap.is_reverse and self.network.config.TEST_SWAPSERVER_REFUND: + if delta < 0: + # too early for refund + continue + else: + if swap.preimage is None: + if funding_conf <= 0: + continue + preimage = self.lnworker.get_preimage(swap.payment_hash) + if preimage is None: + self.invoices_to_pay.add(swap.payment_hash.hex()) + continue + swap.preimage = preimage + if self.network.config.TEST_SWAPSERVER_REFUND: + # for testing: do not create claim tx + continue + + if spent_height is not None: continue try: tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) From 1b14692f304e10c2f50468ccb79cad7a3ebaf8ee Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 25 Jul 2023 15:28:28 +0200 Subject: [PATCH 1096/1143] swapserver: cleanup, add description --- electrum/plugins/swapserver/__init__.py | 11 ++++++++++- electrum/plugins/swapserver/server.py | 2 -- electrum/tests/regtest/regtest.sh | 1 - 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py index 867fb643e..d757b7956 100644 --- a/electrum/plugins/swapserver/__init__.py +++ b/electrum/plugins/swapserver/__init__.py @@ -1,6 +1,15 @@ from electrum.i18n import _ fullname = _('SwapServer') -description = '' +description = """ +Submarine swap server for an Electrum daemon. + +Example setup: + + electrum -o setconfig use_swapserver True + electrum -o setconfig swapserver_address localhost:5455 + electrum daemon -v + +""" available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index da961af1c..d5fee453b 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -38,7 +38,6 @@ def __init__(self, config, wallet): @ignore_exceptions @log_exceptions async def run(self): - self.root = '/root' app = web.Application() app.add_routes([web.get('/api/getpairs', self.get_pairs)]) app.add_routes([web.post('/api/createswap', self.create_swap)]) @@ -57,7 +56,6 @@ async def get_pairs(self, r): "warnings": [], "pairs": { "BTC/BTC": { - "hash": "dfe692a026d6964601bfd79703611af333d1d5aa49ef5fedd288f5a620fced60", "rate": 1, "limits": { "maximal": sm._max_amount, diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 363d7c585..0bfc65a1f 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -97,7 +97,6 @@ if [[ $1 == "init" ]]; then if [[ $2 == "bob" ]]; then $bob setconfig --offline lightning_listen localhost:9735 $bob setconfig --offline use_swapserver true - #else fi echo "funding $2" $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 From 583afefe33d7999fd3dfbcc5821be840e8dc1b4b Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 13 Jul 2023 16:19:37 +0200 Subject: [PATCH 1097/1143] qml: add deadzones on the edge of the screen to work around android back gesture unintended click events --- electrum/gui/qml/components/Addresses.qml | 2 +- electrum/gui/qml/components/Channels.qml | 2 +- electrum/gui/qml/components/History.qml | 14 +------ electrum/gui/qml/components/Invoices.qml | 2 +- electrum/gui/qml/components/Preferences.qml | 10 ----- .../gui/qml/components/ReceiveRequests.qml | 2 +- electrum/gui/qml/components/Wallets.qml | 2 +- .../qml/components/controls/ElListView.qml | 38 +++++++++++++++++++ .../qml/components/controls/ServerConfig.qml | 2 +- 9 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 electrum/gui/qml/components/controls/ElListView.qml diff --git a/electrum/gui/qml/components/Addresses.qml b/electrum/gui/qml/components/Addresses.qml index 91d7e20be..48ef0cf1b 100644 --- a/electrum/gui/qml/components/Addresses.qml +++ b/electrum/gui/qml/components/Addresses.qml @@ -17,7 +17,7 @@ Pane { id: layout anchors.fill: parent - ListView { + ElListView { id: listview Layout.fillWidth: true diff --git a/electrum/gui/qml/components/Channels.qml b/electrum/gui/qml/components/Channels.qml index dbe3d7888..5609ff324 100644 --- a/electrum/gui/qml/components/Channels.qml +++ b/electrum/gui/qml/components/Channels.qml @@ -75,7 +75,7 @@ Pane { spacing: 0 anchors.fill: parent - ListView { + ElListView { id: listview Layout.preferredWidth: parent.width Layout.fillHeight: true diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index 4134d9a1a..c1e9bd73e 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -18,7 +18,7 @@ Pane { color: constants.darkerBackground } - ListView { + ElListView { id: listview width: parent.width height: parent.height @@ -107,18 +107,6 @@ Pane { } } - MouseArea { - // cover list items, make left side insensitive to clicks - // this helps with the back gesture on newer androids - id: left_backgesture_hack - anchors { - top: listview.top - left: listview.left - bottom: listview.bottom - } - width: constants.fingerWidth - } - Rectangle { id: dragb anchors.right: vdragscroll.left diff --git a/electrum/gui/qml/components/Invoices.qml b/electrum/gui/qml/components/Invoices.qml index 473fe8f2b..5403a7baa 100644 --- a/electrum/gui/qml/components/Invoices.qml +++ b/electrum/gui/qml/components/Invoices.qml @@ -39,7 +39,7 @@ Pane { Layout.fillHeight: true Layout.fillWidth: true - ListView { + ElListView { id: listview anchors.fill: parent clip: true diff --git a/electrum/gui/qml/components/Preferences.qml b/electrum/gui/qml/components/Preferences.qml index 5ec5356be..893333e13 100644 --- a/electrum/gui/qml/components/Preferences.qml +++ b/electrum/gui/qml/components/Preferences.qml @@ -79,7 +79,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: thousands @@ -96,7 +95,6 @@ Pane { } RowLayout { - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: fiatEnable @@ -125,7 +123,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: historicRates @@ -162,7 +159,6 @@ Pane { RowLayout { Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: usePin @@ -220,7 +216,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: syncLabels @@ -243,7 +238,6 @@ Pane { RowLayout { Layout.columnSpan: 2 - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: spendUnconfirmed @@ -267,7 +261,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: useTrampolineRouting @@ -303,7 +296,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: useRecoverableChannels @@ -338,7 +330,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: useFallbackAddress @@ -362,7 +353,6 @@ Pane { RowLayout { Layout.columnSpan: 2 Layout.fillWidth: true - Layout.leftMargin: -constants.paddingSmall spacing: 0 Switch { id: enableDebugLogs diff --git a/electrum/gui/qml/components/ReceiveRequests.qml b/electrum/gui/qml/components/ReceiveRequests.qml index 78f3bd4b3..879e3b928 100644 --- a/electrum/gui/qml/components/ReceiveRequests.qml +++ b/electrum/gui/qml/components/ReceiveRequests.qml @@ -42,7 +42,7 @@ Pane { Layout.fillHeight: true Layout.fillWidth: true - ListView { + ElListView { id: listview anchors.fill: parent clip: true diff --git a/electrum/gui/qml/components/Wallets.qml b/electrum/gui/qml/components/Wallets.qml index f651fa33c..7d911a854 100644 --- a/electrum/gui/qml/components/Wallets.qml +++ b/electrum/gui/qml/components/Wallets.qml @@ -47,7 +47,7 @@ Pane { horizontalPadding: 0 background: PaneInsetBackground {} - ListView { + ElListView { id: listview anchors.fill: parent clip: true diff --git a/electrum/gui/qml/components/controls/ElListView.qml b/electrum/gui/qml/components/controls/ElListView.qml new file mode 100644 index 000000000..9ae1c121e --- /dev/null +++ b/electrum/gui/qml/components/controls/ElListView.qml @@ -0,0 +1,38 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.0 +import QtQuick.Controls.Material 2.0 + +ListView { + id: root + + property int width_left_exclusion_zone: 0 + property int width_right_exclusion_zone: 0 + + MouseArea { + anchors {top: root.top; left: root.left; bottom: root.bottom } + visible: width_left_exclusion_zone > 0 + width: width_left_exclusion_zone + } + + MouseArea { + anchors { top: root.top; right: root.right; bottom: root.bottom } + visible: width_right_exclusion_zone > 0 + width: width_right_exclusion_zone + } + + // determine distance from sides of window and reserve some + // space using noop mouseareas in order to not emit clicks when + // android back gesture is used + function layoutExclusionZones() { + var reserve = constants.fingerWidth / 2 + var p = root.mapToGlobal(0, 0) + width_left_exclusion_zone = Math.max(0, reserve - p.x) + p = root.mapToGlobal(width, 0) + width_right_exclusion_zone = Math.max(0, reserve - (app.width - p.x)) + } + + Component.onCompleted: { + layoutExclusionZones() + } +} diff --git a/electrum/gui/qml/components/controls/ServerConfig.qml b/electrum/gui/qml/components/controls/ServerConfig.qml index 5991a9fdf..a18056869 100644 --- a/electrum/gui/qml/components/controls/ServerConfig.qml +++ b/electrum/gui/qml/components/controls/ServerConfig.qml @@ -57,7 +57,7 @@ Item { Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - ListView { + ElListView { id: serversListView anchors.fill: parent model: Network.serverListModel From f4f88f42942e7cc3d4bc67ba4fa8a24bbb996e83 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 31 Jul 2023 11:43:15 +0200 Subject: [PATCH 1098/1143] qml: fix wizard text for keystore, wrap text --- .../gui/qml/components/wizard/WCKeystoreType.qml | 12 +++++++++--- electrum/gui/qml/components/wizard/WCWalletType.qml | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/electrum/gui/qml/components/wizard/WCKeystoreType.qml b/electrum/gui/qml/components/wizard/WCKeystoreType.qml index 30cb41a9d..a9368167b 100644 --- a/electrum/gui/qml/components/wizard/WCKeystoreType.qml +++ b/electrum/gui/qml/components/wizard/WCKeystoreType.qml @@ -1,3 +1,4 @@ +import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.1 @@ -12,9 +13,14 @@ WizardComponent { id: keystoregroup } - GridLayout { - columns: 1 - Label { text: qsTr('What kind of wallet do you want to create?') } + ColumnLayout { + width: parent.width + + Label { + Layout.fillWidth: true + wrapMode: Text.Wrap + text: qsTr('Do you want to create a new seed, restore using an existing seed, or restore from master key?') + } RadioButton { ButtonGroup.group: keystoregroup property string keystoretype: 'createseed' diff --git a/electrum/gui/qml/components/wizard/WCWalletType.qml b/electrum/gui/qml/components/wizard/WCWalletType.qml index 8f7c3b3ac..ef625619e 100644 --- a/electrum/gui/qml/components/wizard/WCWalletType.qml +++ b/electrum/gui/qml/components/wizard/WCWalletType.qml @@ -1,3 +1,4 @@ +import QtQuick 2.6 import QtQuick.Layouts 1.0 import QtQuick.Controls 2.1 @@ -17,9 +18,14 @@ WizardComponent { id: wallettypegroup } - GridLayout { - columns: 1 - Label { text: qsTr('What kind of wallet do you want to create?') } + ColumnLayout { + width: parent.width + + Label { + Layout.fillWidth: true + text: qsTr('What kind of wallet do you want to create?') + wrapMode: Text.Wrap + } RadioButton { ButtonGroup.group: wallettypegroup property string wallettype: 'standard' From d9d281338cef699b70e2e816e1724a34586ef778 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 11:40:01 +0000 Subject: [PATCH 1099/1143] tests: fix logic bug in some regtests follow-up https://github.com/spesmilo/electrum/pull/8489 --- electrum/tests/regtest/regtest.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index b3d7ebfca..836380daa 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -99,6 +99,7 @@ if [[ $1 == "init" ]]; then $bob setconfig --offline use_swapserver true fi echo "funding $2" + # note: changing the funding amount affects all tests, as they rely on "wait_for_balance" $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 fi @@ -141,7 +142,7 @@ if [[ $1 == "breach" ]]; then new_blocks 1 wait_until_channel_closed bob new_blocks 1 - wait_for_balance bob 0.14 + wait_for_balance bob 1.14 $bob getbalance fi @@ -347,7 +348,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then fi echo "alice breaches with old ctx" $bitcoin_cli sendrawtransaction $ctx - wait_for_balance bob 0.14 + wait_for_balance bob 1.14 fi @@ -402,7 +403,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then $bob daemon -d sleep 1 $bob load_wallet - wait_for_balance bob 0.039 + wait_for_balance bob 1.039 $bob getbalance fi From c068b80d780b076f13417b96abcd5ddffe0d7163 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Thu, 3 Aug 2023 17:17:06 +0200 Subject: [PATCH 1100/1143] qml: fix issues with 2fa, simplify terms and conditions retrieval code --- electrum/plugins/trustedcoin/qml.py | 40 +++++++------------ .../trustedcoin/qml/ShowConfirmOTP.qml | 6 +-- electrum/plugins/trustedcoin/qml/Terms.qml | 23 +++++++++-- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/electrum/plugins/trustedcoin/qml.py b/electrum/plugins/trustedcoin/qml.py index 962676ee0..bb9412cc0 100644 --- a/electrum/plugins/trustedcoin/qml.py +++ b/electrum/plugins/trustedcoin/qml.py @@ -28,10 +28,8 @@ class Plugin(TrustedCoinPlugin): class QSignalObject(PluginQObject): canSignWithoutServerChanged = pyqtSignal() _canSignWithoutServer = False - termsAndConditionsChanged = pyqtSignal() - _termsAndConditions = '' - termsAndConditionsErrorChanged = pyqtSignal() - _termsAndConditionsError = '' + termsAndConditionsRetrieved = pyqtSignal([str], arguments=['message']) + termsAndConditionsError = pyqtSignal([str], arguments=['message']) otpError = pyqtSignal([str], arguments=['message']) otpSuccess = pyqtSignal() disclaimerChanged = pyqtSignal() @@ -76,14 +74,6 @@ def shortId(self): def otpSubmit(self, otp): self._plugin.on_otp(otp) - @pyqtProperty(str, notify=termsAndConditionsChanged) - def termsAndConditions(self): - return self._termsAndConditions - - @pyqtProperty(str, notify=termsAndConditionsErrorChanged) - def termsAndConditionsError(self): - return self._termsAndConditionsError - @pyqtProperty(str, notify=remoteKeyStateChanged) def remoteKeyState(self): return self._remoteKeyState @@ -122,14 +112,11 @@ def fetch_task(): self.plugin.logger.debug('TOS') tos = server.get_terms_of_service() except ErrorConnectingServer as e: - self._termsAndConditionsError = _('Error connecting to server') - self.termsAndConditionsErrorChanged.emit() + self.termsAndConditionsError.emit(_('Error connecting to server')) except Exception as e: - self._termsAndConditionsError = '%s: %s' % (_('Error'), repr(e)) - self.termsAndConditionsErrorChanged.emit() + self.termsAndConditionsError.emit('%s: %s' % (_('Error'), repr(e))) else: - self._termsAndConditions = tos - self.termsAndConditionsChanged.emit() + self.termsAndConditionsRetrieved.emit(tos) finally: self._busy = False self.busyChanged.emit() @@ -143,7 +130,11 @@ def fetch_task(): @pyqtSlot(str) def createKeystore(self, email): self.remoteKeyState = '' + self._otpSecret = '' + self.otpSecretChanged.emit() + xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys() + def create_remote_key_task(): try: self.plugin.logger.debug('create remote key') @@ -179,6 +170,7 @@ def create_remote_key_task(): self.logger.error("unexpected trustedcoin xpub3: expected {}, received {}".format(xpub3, _xpub3)) self.remoteKeyError.emit('Unexpected trustedcoin xpub3') return + self.remoteKeyState = 'new' self._otpSecret = otp_secret self.otpSecretChanged.emit() self._shortId = short_id @@ -198,12 +190,15 @@ def create_remote_key_task(): def resetOtpSecret(self): self.remoteKeyState = '' xprv1, xpub1, xprv2, xpub2, xpub3, short_id = self.plugin.create_keys() + def reset_otp_task(): try: + # TODO: move reset request to UI agnostic plugin section self.plugin.logger.debug('reset_otp') r = server.get_challenge(short_id) challenge = r.get('challenge') message = 'TRUSTEDCOIN CHALLENGE: ' + challenge + def f(xprv): rootnode = BIP32Node.from_xkey(xprv) key = rootnode.subkey_at_private_derivation((0, 0)).eckey @@ -220,6 +215,7 @@ def f(xprv): self.remoteKeyState = 'error' self.remoteKeyError.emit(f'Error: {str(e)}') else: + self.remoteKeyState = 'reset' self._otpSecret = otp_secret self.otpSecretChanged.emit() finally: @@ -260,7 +256,6 @@ def check_otp_task(): t = threading.Thread(target=check_otp_task, daemon=True) t.start() - def __init__(self, *args): super().__init__(*args) @@ -269,7 +264,6 @@ def load_wallet(self, wallet: 'Abstract_Wallet'): if not isinstance(wallet, self.wallet_class): return self.logger.debug(f'plugin enabled for wallet "{str(wallet)}"') - #wallet.handler_2fa = HandlerTwoFactor(self, window) if wallet.can_sign_without_server(): self.so._canSignWithoutServer = True self.so.canSignWithoutServerChanged.emit() @@ -279,12 +273,6 @@ def load_wallet(self, wallet: 'Abstract_Wallet'): _('Therefore, two-factor authentication is disabled.') ]) self.logger.info(msg) - #action = lambda: window.show_message(msg) - #else: - #action = partial(self.settings_dialog, window) - #button = StatusBarButton(read_QIcon("trustedcoin-status.png"), - #_("TrustedCoin"), action) - #window.statusBar().addPermanentWidget(button) self.start_request_thread(wallet) @hook diff --git a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml index 0e963978f..9a846cf03 100644 --- a/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml +++ b/electrum/plugins/trustedcoin/qml/ShowConfirmOTP.qml @@ -12,10 +12,6 @@ WizardComponent { property bool otpVerified: false - function apply() { - wizard_data['trustedcoin_new_otp_secret'] = requestNewSecret.checked - } - ColumnLayout { width: parent.width @@ -40,7 +36,7 @@ WizardComponent { QRImage { Layout.alignment: Qt.AlignHCenter - visible: plugin.remoteKeyState == '' + visible: plugin.remoteKeyState == 'new' || plugin.remoteKeyState == 'reset' qrdata: encodeURI('otpauth://totp/Electrum 2FA ' + wizard_data['wallet_name'] + '?secret=' + plugin.otpSecret + '&digits=6') render: plugin.otpSecret diff --git a/electrum/plugins/trustedcoin/qml/Terms.qml b/electrum/plugins/trustedcoin/qml/Terms.qml index d0e30ffa6..18b453e00 100644 --- a/electrum/plugins/trustedcoin/qml/Terms.qml +++ b/electrum/plugins/trustedcoin/qml/Terms.qml @@ -10,9 +10,10 @@ import "../../../gui/qml/components/controls" WizardComponent { valid: !plugin ? false : email.text.length > 0 // TODO: validate email address - && plugin.termsAndConditions + && tosShown property QtObject plugin + property bool tosShown: false onAccept: { wizard_data['2fa_email'] = email.text @@ -21,7 +22,9 @@ WizardComponent { ColumnLayout { anchors.fill: parent - Label { text: qsTr('Terms and conditions') } + Label { + text: qsTr('Terms and conditions') + } TextHighlightPane { Layout.fillWidth: true @@ -39,7 +42,6 @@ WizardComponent { width: parent.width rightPadding: constants.paddingSmall wrapMode: Text.Wrap - text: plugin ? plugin.termsAndConditions : '' } ScrollIndicator.vertical: ScrollIndicator { } } @@ -51,7 +53,9 @@ WizardComponent { } } - Label { text: qsTr('Email') } + Label { + text: qsTr('Email') + } TextField { id: email @@ -64,4 +68,15 @@ WizardComponent { plugin = AppController.plugin('trustedcoin') plugin.fetchTermsAndConditions() } + + Connections { + target: plugin + function onTermsAndConditionsRetrieved(message) { + termsText.text = message + tosShown = true + } + function onTermsAndConditionsError(message) { + termsText.text = message + } + } } From e94d45edd89ba6cfc2a7aa6beac9479f0c103ab8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 15:09:01 +0000 Subject: [PATCH 1101/1143] swapserver: small clean-up --- electrum/plugins/swapserver/server.py | 9 ++------ electrum/plugins/swapserver/swapserver.py | 25 +++++++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index d5fee453b..0fb1ca724 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -1,19 +1,14 @@ import os import asyncio -import attr -import random from collections import defaultdict -from aiohttp import ClientResponse -from aiohttp import web, client_exceptions -from aiorpcx import timeout_after, TaskTimeout, ignore_after +from aiohttp import web from aiorpcx import NetAddress from electrum.util import log_exceptions, ignore_exceptions from electrum.logging import Logger -from electrum.util import EventListener, event_listener -from electrum.invoices import PR_PAID, PR_EXPIRED +from electrum.util import EventListener class SwapServer(Logger, EventListener): diff --git a/electrum/plugins/swapserver/swapserver.py b/electrum/plugins/swapserver/swapserver.py index d061c8bc1..a4b785347 100644 --- a/electrum/plugins/swapserver/swapserver.py +++ b/electrum/plugins/swapserver/swapserver.py @@ -25,34 +25,37 @@ import asyncio -import os -import random +from typing import TYPE_CHECKING + from electrum.plugin import BasePlugin, hook -from electrum.util import log_exceptions, ignore_exceptions -from electrum import ecc from .server import SwapServer +if TYPE_CHECKING: + from electrum.simple_config import SimpleConfig + from electrum.daemon import Daemon + from electrum.wallet import Abstract_Wallet + class SwapServerPlugin(BasePlugin): - def __init__(self, parent, config, name): + def __init__(self, parent, config: 'SimpleConfig', name): BasePlugin.__init__(self, parent, config, name) self.config = config self.server = None @hook - def daemon_wallet_loaded(self, daemon, wallet): + def daemon_wallet_loaded(self, daemon: 'Daemon', wallet: 'Abstract_Wallet'): # we use the first wallet loaded if self.server is not None: return - if self.config.get('offline'): + if self.config.NETWORK_OFFLINE: return self.server = SwapServer(self.config, wallet) sm = wallet.lnworker.swap_manager - jobs = [ - sm.pay_pending_invoices(), + for coro in [ + sm.pay_pending_invoices(), # FIXME this method can raise, which is not properly handled...? self.server.run(), - ] - asyncio.run_coroutine_threadsafe(daemon._run(jobs=jobs), daemon.asyncio_loop) + ]: + asyncio.run_coroutine_threadsafe(daemon.taskgroup.spawn(coro), daemon.asyncio_loop) From 58a9904a3411a5d49aa98573f80d6db663006982 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 15:19:19 +0000 Subject: [PATCH 1102/1143] daemon: rm "daemon_jobs". maybe makes _run API less error-prone (follow-up prev) --- electrum/daemon.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 086f10773..d2f5a4311 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -405,29 +405,26 @@ def __init__( # wallet_key -> wallet self._wallets = {} # type: Dict[str, Abstract_Wallet] self._wallet_lock = threading.RLock() - daemon_jobs = [] - # Setup commands server - self.commands_server = None - if listen_jsonrpc: - self.commands_server = CommandsServer(self, fd) - daemon_jobs.append(self.commands_server.run()) self._stop_entered = False self._stopping_soon_or_errored = threading.Event() self._stopped_event = threading.Event() + self.taskgroup = OldTaskGroup() - asyncio.run_coroutine_threadsafe(self._run(jobs=daemon_jobs), self.asyncio_loop) + asyncio.run_coroutine_threadsafe(self._run(), self.asyncio_loop) if start_network and self.network: self.start_network() + # Setup commands server + self.commands_server = None + if listen_jsonrpc: + self.commands_server = CommandsServer(self, fd) + asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.commands_server.run()), self.asyncio_loop) @log_exceptions - async def _run(self, jobs: Iterable = None): - if jobs is None: - jobs = [] + async def _run(self): self.logger.info("starting taskgroup.") try: async with self.taskgroup as group: - [await group.spawn(job) for job in jobs] await group.spawn(asyncio.Event().wait) # run forever (until cancel) except Exception as e: self.logger.exception("taskgroup died.") From 8b195ee77a1cd9c85eac1e0a885791f956d819f1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 17:02:40 +0000 Subject: [PATCH 1103/1143] cli: "./run_electrum daemon -d" to block until daemon becomes ready Without this, `$ python3 -m unittest electrum.tests.regtest.TestUnixSockets.test_unixsockets` was failing on my machine but succeeding on CI, due to timing differences. --- electrum/daemon.py | 13 +++++++++++++ run_electrum | 10 ++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index d2f5a4311..ff9a7a2fd 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -151,6 +151,19 @@ async def request_coroutine( time.sleep(1.0) +def wait_until_daemon_becomes_ready(*, config: SimpleConfig, timeout=5) -> bool: + t0 = time.monotonic() + while True: + if time.monotonic() > t0 + timeout: + return False # timeout + try: + request(config, 'ping') + return True # success + except DaemonNotRunning: + time.sleep(0.05) + continue + + def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]: rpc_user = config.RPC_USERNAME or None rpc_password = config.RPC_PASSWORD or None diff --git a/run_electrum b/run_electrum index 413e7b4b8..230665209 100755 --- a/run_electrum +++ b/run_electrum @@ -284,6 +284,7 @@ def sys_exit(i): def main(): + global loop, stop_loop, loop_thread # The hook will only be used in the Qt GUI right now util.setup_thread_excepthook() # on macOS, delete Process Serial Number arg generated for apps launched in Finder @@ -416,7 +417,13 @@ def main(): sys.exit(1) if pid: print_stderr("starting daemon (PID %d)" % pid) - sys.exit(0) + loop, stop_loop, loop_thread = create_and_start_event_loop() + ready = daemon.wait_until_daemon_becomes_ready(config=config, timeout=5) + if ready: + sys_exit(0) + else: + print_stderr("timed out waiting for daemon to get ready") + sys_exit(1) else: # redirect standard file descriptors sys.stdout.flush() @@ -428,7 +435,6 @@ def main(): os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) - global loop, stop_loop, loop_thread loop, stop_loop, loop_thread = create_and_start_event_loop() try: From a674f63ce3c127f3b2dbd8320eac184722d5e637 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 17:06:16 +0000 Subject: [PATCH 1104/1143] qt channels list: add TODO (noticed on regtest) --- electrum/gui/qt/channels_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py index a7fa706c0..ead1260b8 100644 --- a/electrum/gui/qt/channels_list.py +++ b/electrum/gui/qt/channels_list.py @@ -328,6 +328,7 @@ def do_update_rows(self, wallet): self._update_chan_frozen_bg(chan=chan, items=items) self.model().insertRow(0, items) + # FIXME sorting by SHORT_CHANID should treat values as tuple, not as string ( 50x1x1 > 8x1x1 ) self.sortByColumn(self.Columns.SHORT_CHANID, Qt.DescendingOrder) def _update_chan_frozen_bg(self, *, chan: AbstractChannel, items: Sequence[QStandardItem]): From 69336befee959992da811774465f1d49f35f3f42 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 17:17:34 +0000 Subject: [PATCH 1105/1143] follow-up ImportedChannelBackup changes: fix opening wallet w/ old cbs follow-up https://github.com/spesmilo/electrum/pull/8536 ``` 1.52 | E | gui.qt.ElectrumGui | Traceback (most recent call last): File "/home/user/wspace/electrum/electrum/gui/qt/__init__.py", line 342, in start_new_window wallet = self.daemon.load_wallet(path, None) File "/home/user/wspace/electrum/electrum/daemon.py", line 469, in func_wrapper return func(self, *args, **kwargs) File "/home/user/wspace/electrum/electrum/daemon.py", line 479, in load_wallet wallet = self._load_wallet(path, password, manual_upgrades=manual_upgrades, config=self.config) File "/home/user/wspace/electrum/electrum/util.py", line 466, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/daemon.py", line 504, in _load_wallet db = WalletDB(storage.read(), manual_upgrades=manual_upgrades) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 117, in __init__ self._after_upgrade_tasks() File "/home/user/wspace/electrum/electrum/wallet_db.py", line 247, in _after_upgrade_tasks self._load_transactions() File "/home/user/wspace/electrum/electrum/util.py", line 466, in do_profile o = func(*args, **kw_args) File "/home/user/wspace/electrum/electrum/wallet_db.py", line 1536, in _load_transactions self.data = StoredDict(self.data, self, []) File "/home/user/wspace/electrum/electrum/json_db.py", line 117, in __init__ self.__setitem__(k, v) File "/home/user/wspace/electrum/electrum/json_db.py", line 49, in wrapper return func(self, *args, **kwargs) File "/home/user/wspace/electrum/electrum/json_db.py", line 135, in __setitem__ v = self.db._convert_dict(self.path, key, v) File "/home/user/wspace/electrum/electrum/json_db.py", line 247, in _convert_dict v = dict((k, constructor(**x)) for k, x in v.items()) File "/home/user/wspace/electrum/electrum/json_db.py", line 247, in v = dict((k, constructor(**x)) for k, x in v.items()) TypeError: ImportedChannelBackupStorage.__init__() missing 1 required positional argument: 'local_payment_pubkey' ``` --- electrum/lnutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 0447034bf..7d3c37a42 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -279,7 +279,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): remote_delay = attr.ib(type=int, converter=int) remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) - local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes] + local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes, default=None) # type: Optional[bytes] def to_bytes(self) -> bytes: vds = BCDataStream() From 20f4d44f09f75235771cfdc470292701a42a1615 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 3 Aug 2023 22:42:08 +0000 Subject: [PATCH 1106/1143] cli: "daemon -d": init rpc credentials before os.fork() follow-up 8b195ee77a1cd9c85eac1e0a885791f956d819f1 --- run_electrum | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run_electrum b/run_electrum index 230665209..cbd17fd24 100755 --- a/run_electrum +++ b/run_electrum @@ -407,6 +407,9 @@ def main(): print_stderr("Daemon already running (lockfile detected).") print_stderr("Run 'electrum stop' to stop the daemon.") sys.exit(1) + # Initialise rpc credentials to random if not set yet. This would normally be done + # later anyway, but we need to avoid the two sides of the fork setting conflicting random creds. + daemon.get_rpc_credentials(config) # inits creds as side-effect # fork before creating the asyncio event loop try: pid = os.fork() From d51f00e2a358842ace7352201bcac5710cfc8519 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Aug 2023 17:59:47 +0000 Subject: [PATCH 1107/1143] asyncio.wait_for() is too buggy. use util.wait_for2() instead wasted some time because asyncio.wait_for() was suppressing cancellations. [0][1][2] deja vu... [3] Looks like this is finally getting fixed in cpython 3.12 [4] So far away... In attempt to avoid encountering this again, let's try using asyncio.timeout in 3.11, which is how upstream reimplemented wait_for in 3.12 [4], and aiorpcx.timeout_after in 3.8-3.10. [0] https://github.com/python/cpython/issues/86296 [1] https://bugs.python.org/issue42130 [2] https://bugs.python.org/issue45098 [3] https://github.com/kyuupichan/aiorpcX/issues/44 [4] https://github.com/python/cpython/pull/98518 --- electrum/interface.py | 2 +- electrum/lnpeer.py | 11 ++++---- electrum/lnworker.py | 2 +- electrum/network.py | 4 +-- electrum/plugins/payserver/payserver.py | 3 ++- electrum/scripts/ln_features.py | 4 +-- electrum/tests/test_lnpeer.py | 36 ++++++++++++------------- electrum/util.py | 32 +++++++++++++++++++++- 8 files changed, 63 insertions(+), 31 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index f1d6f4d9f..1b4e6e229 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -166,7 +166,7 @@ async def send_request(self, *args, timeout=None, **kwargs): try: # note: RPCSession.send_request raises TaskTimeout in case of a timeout. # TaskTimeout is a subclass of CancelledError, which is *suppressed* in TaskGroups - response = await asyncio.wait_for( + response = await util.wait_for2( super().send_request(*args, **kwargs), timeout) except (TaskTimeout, asyncio.TimeoutError) as e: diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 2ba240e97..1fc8ad0df 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -15,6 +15,7 @@ import aiorpcx from aiorpcx import ignore_after +from async_timeout import timeout from .crypto import sha256, sha256d from . import bitcoin, util @@ -331,7 +332,7 @@ def on_pong(self, payload): async def wait_for_message(self, expected_name: str, channel_id: bytes): q = self.ordered_message_queues[channel_id] - name, payload = await asyncio.wait_for(q.get(), LN_P2P_NETWORK_TIMEOUT) + name, payload = await util.wait_for2(q.get(), LN_P2P_NETWORK_TIMEOUT) # raise exceptions for errors, so that the caller sees them if (err_bytes := payload.get("error")) is not None: err_text = error_text_bytes_to_safe_str(err_bytes) @@ -460,12 +461,12 @@ async def process_gossip(self): async def query_gossip(self): try: - await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT) + await util.wait_for2(self.initialized, LN_P2P_NETWORK_TIMEOUT) except Exception as e: raise GracefulDisconnect(f"Failed to initialize: {e!r}") from e if self.lnworker == self.lnworker.network.lngossip: try: - ids, complete = await asyncio.wait_for(self.get_channel_range(), LN_P2P_NETWORK_TIMEOUT) + ids, complete = await util.wait_for2(self.get_channel_range(), LN_P2P_NETWORK_TIMEOUT) except asyncio.TimeoutError as e: raise GracefulDisconnect("query_channel_range timed out") from e self.logger.info('Received {} channel ids. (complete: {})'.format(len(ids), complete)) @@ -575,7 +576,7 @@ def query_short_channel_ids(self, ids, compressed=True): async def _message_loop(self): try: - await asyncio.wait_for(self.initialize(), LN_P2P_NETWORK_TIMEOUT) + await util.wait_for2(self.initialize(), LN_P2P_NETWORK_TIMEOUT) except (OSError, asyncio.TimeoutError, HandshakeFailed) as e: raise GracefulDisconnect(f'initialize failed: {repr(e)}') from e async for msg in self.transport.read_messages(): @@ -699,7 +700,7 @@ async def channel_establishment_flow( Channel configurations are initialized in this method. """ # will raise if init fails - await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT) + await util.wait_for2(self.initialized, LN_P2P_NETWORK_TIMEOUT) # trampoline is not yet in features if self.lnworker.uses_trampoline() and not self.lnworker.is_trampoline_peer(self.pubkey): raise Exception('Not a trampoline node: ' + str(self.their_features)) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index cafb920d8..d2f907b6e 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1071,7 +1071,7 @@ async def _open_channel_coroutine( funding_sat=funding_sat, push_msat=push_sat * 1000, temp_channel_id=os.urandom(32)) - chan, funding_tx = await asyncio.wait_for(coro, LN_P2P_NETWORK_TIMEOUT) + chan, funding_tx = await util.wait_for2(coro, LN_P2P_NETWORK_TIMEOUT) util.trigger_callback('channels_updated', self.wallet) self.wallet.adb.add_transaction(funding_tx) # save tx as local into the wallet self.wallet.sign_transaction(funding_tx, password) diff --git a/electrum/network.py b/electrum/network.py index 47da82b69..8a6aad319 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -811,7 +811,7 @@ async def _run_new_interface(self, server: ServerAddr): # note: using longer timeouts here as DNS can sometimes be slow! timeout = self.get_network_timeout_seconds(NetworkTimeout.Generic) try: - await asyncio.wait_for(interface.ready, timeout) + await util.wait_for2(interface.ready, timeout) except BaseException as e: self.logger.info(f"couldn't launch iface {server} -- {repr(e)}") await interface.close() @@ -1401,7 +1401,7 @@ async def send_multiple_requests( async def get_response(server: ServerAddr): interface = Interface(network=self, server=server, proxy=self.proxy) try: - await asyncio.wait_for(interface.ready, timeout) + await util.wait_for2(interface.ready, timeout) except BaseException as e: await interface.close() return diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index b220c3cf5..cfdd6ce42 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -31,6 +31,7 @@ from aiohttp import web from aiorpcx import NetAddress +from electrum import util from electrum.util import log_exceptions, ignore_exceptions from electrum.plugin import BasePlugin, hook from electrum.logging import Logger @@ -173,7 +174,7 @@ async def get_status(self, request): return ws while True: try: - await asyncio.wait_for(self.pending[key].wait(), 1) + await util.wait_for2(self.pending[key].wait(), 1) break except asyncio.TimeoutError: # send data on the websocket, to keep it alive diff --git a/electrum/scripts/ln_features.py b/electrum/scripts/ln_features.py index 8b6303fae..7b396534d 100644 --- a/electrum/scripts/ln_features.py +++ b/electrum/scripts/ln_features.py @@ -11,7 +11,7 @@ from electrum.logging import get_logger, configure_logging from electrum.simple_config import SimpleConfig -from electrum import constants +from electrum import constants, util from electrum.daemon import Daemon from electrum.wallet import create_new_wallet from electrum.util import create_and_start_event_loop, log_exceptions, bfh @@ -84,7 +84,7 @@ async def worker(work_queue: asyncio.Queue, results_queue: asyncio.Queue, flag): print(f"worker connecting to {connect_str}") try: peer = await wallet.lnworker.add_peer(connect_str) - res = await asyncio.wait_for(peer.initialized, TIMEOUT) + res = await util.wait_for2(peer.initialized, TIMEOUT) if res: if peer.features & flag == work['features'] & flag: await results_queue.put(True) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index d2eafa6f2..4a6cd785a 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -824,8 +824,8 @@ async def test_payment_race(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def pay(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) # prep _maybe_send_commitment1 = p1.maybe_send_commitment _maybe_send_commitment2 = p2.maybe_send_commitment @@ -1374,8 +1374,8 @@ async def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee w2.enable_htlc_settle = False lnaddr, pay_req = self.prepare_invoice(w2) async def pay(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) # alice sends htlc route, amount_msat = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0:2] p1.pay(route=route, @@ -1401,8 +1401,8 @@ async def test_warning(self): p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def action(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) await p1.send_warning(alice_channel.channel_id, 'be warned!', close_connection=True) gath = asyncio.gather(action(), p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) with self.assertRaises(GracefulDisconnect): @@ -1414,8 +1414,8 @@ async def test_error(self): p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def action(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) await p1.send_error(alice_channel.channel_id, 'some error happened!', force_close_channel=True) assert alice_channel.is_closed() gath.cancel() @@ -1447,8 +1447,8 @@ async def test_close_upfront_shutdown_script(self): async def test(): async def close(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) # bob closes channel with different shutdown script await p1.close_channel(alice_channel.channel_id) gath.cancel() @@ -1477,8 +1477,8 @@ async def main_loop(peer): async def test(): async def close(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) await p1.close_channel(alice_channel.channel_id) gath.cancel() @@ -1538,8 +1538,8 @@ async def test_sending_weird_messages_that_should_be_ignored(self): p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def send_weird_messages(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) # peer1 sends known message with trailing garbage # BOLT-01 says peer2 should ignore trailing garbage raw_msg1 = encode_msg('ping', num_pong_bytes=4, byteslen=4) + bytes(range(55)) @@ -1570,8 +1570,8 @@ async def test_sending_weird_messages__unknown_even_type(self): p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def send_weird_messages(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) # peer1 sends unknown 'even-type' message # BOLT-01 says peer2 should close the connection raw_msg2 = (43334).to_bytes(length=2, byteorder="big") + bytes(range(55)) @@ -1600,8 +1600,8 @@ async def test_sending_weird_messages__known_msg_with_insufficient_length(self): p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) async def send_weird_messages(): - await asyncio.wait_for(p1.initialized, 1) - await asyncio.wait_for(p2.initialized, 1) + await util.wait_for2(p1.initialized, 1) + await util.wait_for2(p2.initialized, 1) # peer1 sends known message with insufficient length for the contents # BOLT-01 says peer2 should fail the connection raw_msg1 = encode_msg('ping', num_pong_bytes=4, byteslen=4)[:-1] diff --git a/electrum/util.py b/electrum/util.py index 53fe4fc7c..933b71698 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -24,7 +24,7 @@ import os, sys, re, json from collections import defaultdict, OrderedDict from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, - Sequence, Dict, Generic, TypeVar, List, Iterable, Set) + Sequence, Dict, Generic, TypeVar, List, Iterable, Set, Awaitable) from datetime import datetime import decimal from decimal import Decimal @@ -1371,6 +1371,36 @@ def _aiorpcx_monkeypatched_unset_task_deadline(task): aiorpcx.curio._unset_task_deadline = _aiorpcx_monkeypatched_unset_task_deadline +async def wait_for2(fut: Awaitable, timeout: Union[int, float, None]): + """Replacement for asyncio.wait_for, + due to bugs: https://bugs.python.org/issue42130 and https://github.com/python/cpython/issues/86296 , + which are only fixed in python 3.12+. + """ + if sys.version_info[:3] >= (3, 12): + return await asyncio.wait_for(fut, timeout) + else: + async with async_timeout(timeout): + return await asyncio.ensure_future(fut, loop=get_running_loop()) + + +if hasattr(asyncio, 'timeout'): # python 3.11+ + async_timeout = asyncio.timeout +else: + class TimeoutAfterAsynciolike(aiorpcx.curio.TimeoutAfter): + async def __aexit__(self, exc_type, exc_value, traceback): + try: + await super().__aexit__(exc_type, exc_value, traceback) + except (aiorpcx.TaskTimeout, aiorpcx.UncaughtTimeoutError): + raise asyncio.TimeoutError from None + except aiorpcx.TimeoutCancellationError: + raise asyncio.CancelledError from None + + def async_timeout(delay: Union[int, float, None]): + if delay is None: + return nullcontext() + return TimeoutAfterAsynciolike(delay) + + class NetworkJobOnDefaultServer(Logger, ABC): """An abstract base class for a job that runs on the main network interface. Every time the main interface changes, the job is From 5a4b98a066b902495449588b5800a0d9bb8b50ab Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Aug 2023 18:17:27 +0000 Subject: [PATCH 1108/1143] CI: don't run unit tests on "python:rc", looks like it's unmaintained The "python:rc" tag on dockerhub has not been updated for 2+ years and is still at 3.10.0rc2. Even 3.11 has been released for many months now. see https://hub.docker.com/_/python/tags --- .cirrus.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 1a4c8af96..c2dd56099 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -18,8 +18,6 @@ task: ELECTRUM_PYTHON_VERSION: 3.10 - env: ELECTRUM_PYTHON_VERSION: 3.11 - - env: - ELECTRUM_PYTHON_VERSION: rc - name: Tox Python 3 debug mode env: ELECTRUM_PYTHON_VERSION: 3 From 8dd5865469aac2b5d4ac546b7088778209bf7947 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Aug 2023 18:21:50 +0000 Subject: [PATCH 1109/1143] rm unused import follow-up d51f00e2a358842ace7352201bcac5710cfc8519 --- electrum/lnpeer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 1fc8ad0df..96855e45d 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -15,7 +15,6 @@ import aiorpcx from aiorpcx import ignore_after -from async_timeout import timeout from .crypto import sha256, sha256d from . import bitcoin, util From cb907c90f9668361298d4ea8b425aa21a845943c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 6 Aug 2023 11:37:26 +0200 Subject: [PATCH 1110/1143] submarine swaps: set prepay_hash for normal swaps. This fixes the group_id of fee prepayment transactions for the server --- electrum/submarine_swaps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index bd4074dcb..5d40a72fb 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -155,7 +155,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash for k, swap in self.swaps.items(): - if swap.is_reverse and swap.prepay_hash is not None: + if swap.prepay_hash is not None: self.prepayments[swap.prepay_hash] = bytes.fromhex(k) # api url self.api_url = wallet.config.get_swapserver_url() @@ -328,6 +328,7 @@ def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoi ) self.wallet.save_invoice(Invoice.from_bech32(invoice)) prepay_invoice = None + prepay_hash = None else: onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) prepay_amount_sat = self.get_claim_fee() * 2 @@ -364,7 +365,7 @@ def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoi locktime = locktime, privkey = privkey, preimage = None, - prepay_hash = None, + prepay_hash = prepay_hash, lockup_address = lockup_address, onchain_amount = onchain_amount_sat, receive_address = receive_address, From c527ef89672f17d97179c8f9733f9c2c5b8b8817 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 7 Aug 2023 18:57:04 +0000 Subject: [PATCH 1111/1143] lnpeer: refuse to forward htlcs that correspond to payreq we created --- electrum/lnpeer.py | 29 +++++++++++++++ electrum/tests/test_lnpeer.py | 68 +++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 96855e45d..62833ac00 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1625,6 +1625,9 @@ def log_fail_reason(reason: str): except Exception: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') if htlc.cltv_expiry - next_cltv_expiry < next_chan.forwarding_cltv_expiry_delta: + log_fail_reason( + f"INCORRECT_CLTV_EXPIRY. " + f"{htlc.cltv_expiry=} - {next_cltv_expiry=} < {next_chan.forwarding_cltv_expiry_delta=}") data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_message raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data) if htlc.cltv_expiry - lnutil.MIN_FINAL_CLTV_EXPIRY_ACCEPTED <= local_height \ @@ -1639,6 +1642,9 @@ def log_fail_reason(reason: str): if htlc.amount_msat - next_amount_msat_htlc < forwarding_fees: data = next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_message raise OnionRoutingFailure(code=OnionFailureCode.FEE_INSUFFICIENT, data=data) + if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(htlc.payment_hash): + log_fail_reason(f"RHASH corresponds to payreq we created") + raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') self.logger.info( f"maybe_forward_htlc. will forward HTLC: inc_chan={incoming_chan.short_channel_id}. inc_htlc={str(htlc)}. " f"next_chan={next_chan.get_id_for_log()}.") @@ -1707,6 +1713,12 @@ async def maybe_forward_trampoline( self.logger.exception('') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') + if self._maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(payment_hash): + self.logger.debug( + f"maybe_forward_trampoline. will FAIL HTLC(s). " + f"RHASH corresponds to payreq we created. {payment_hash.hex()=}") + raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') + # these are the fee/cltv paid by the sender # pay_to_node will raise if they are not sufficient trampoline_cltv_delta = cltv_expiry - cltv_from_onion @@ -1733,6 +1745,23 @@ async def maybe_forward_trampoline( # FIXME: adapt the error code raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') + def _maybe_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self, payment_hash: bytes) -> bool: + """Returns True if the HTLC should be failed. + We must not forward HTLCs with a matching payment_hash to a payment request we created. + Example attack: + - Bob creates payment request with HASH1, for 1 BTC; and gives the payreq to Alice + - Alice sends htlc A->B->C, for 1 sat, with HASH1 + - Bob must not release the preimage of HASH1 + """ + payment_info = self.lnworker.get_payment_info(payment_hash) + is_our_payreq = payment_info and payment_info.direction == RECEIVED + # note: If we don't have the preimage for a payment request, then it must be a hold invoice. + # Hold invoices are created by other parties (e.g. a counterparty initiating a submarine swap), + # and it is the other party choosing the payment_hash. If we failed HTLCs with payment_hashes colliding + # with hold invoices, then a party that can make us save a hold invoice for an arbitrary hash could + # also make us fail arbitrary HTLCs. + return bool(is_our_payreq and self.lnworker.get_preimage(payment_hash)) + def maybe_fulfill_htlc( self, *, chan: Channel, diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 4a6cd785a..ad072fe44 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -538,12 +538,17 @@ def prepare_invoice( *, amount_msat=100_000_000, include_routing_hints=False, + payment_preimage: bytes = None, + payment_hash: bytes = None, ) -> Tuple[LnAddr, str]: amount_btc = amount_msat/Decimal(COIN*1000) - payment_preimage = os.urandom(32) - RHASH = sha256(payment_preimage) - info = PaymentInfo(RHASH, amount_msat, RECEIVED, PR_UNPAID) - w2.save_preimage(RHASH, payment_preimage) + if payment_preimage is None and not payment_hash: + payment_preimage = os.urandom(32) + if payment_hash is None: + payment_hash = sha256(payment_preimage) + info = PaymentInfo(payment_hash, amount_msat, RECEIVED, PR_UNPAID) + if payment_preimage: + w2.save_preimage(payment_hash, payment_preimage) w2.save_payment_info(info) if include_routing_hints: routing_hints, trampoline_hints = w2.calc_routing_hints_for_invoice(amount_msat) @@ -552,11 +557,11 @@ def prepare_invoice( trampoline_hints = [] invoice_features = w2.features.for_invoice() if invoice_features.supports(LnFeatures.PAYMENT_SECRET_OPT): - payment_secret = w2.get_payment_secret(RHASH) + payment_secret = w2.get_payment_secret(payment_hash) else: payment_secret = None lnaddr1 = LnAddr( - paymenthash=RHASH, + paymenthash=payment_hash, amount=amount_btc, tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), ('d', 'coffee'), @@ -1046,6 +1051,57 @@ async def f(): with self.assertRaises(PaymentDone): await f() + async def test_refuse_to_forward_htlc_that_corresponds_to_payreq_we_created(self): + # This test checks that the following attack does not work: + # - Bob creates payment request with HASH1, for 1 BTC; and gives the payreq to Alice + # - Alice sends htlc A->B->D, for 100k sat, with HASH1 + # - Bob must not release the preimage of HASH1 + graph_def = self.GRAPH_DEFINITIONS['square_graph'] + graph_def.pop('carol') + graph_def['alice']['channels'].pop('carol') + # now graph is linear: A <-> B <-> D + graph = self.prepare_chans_and_peers_in_graph(graph_def) + peers = graph.peers.values() + async def pay(): + lnaddr1, pay_req1 = self.prepare_invoice( + graph.workers['bob'], + amount_msat=100_000_000_000, + ) + lnaddr2, pay_req2 = self.prepare_invoice( + graph.workers['dave'], + amount_msat=100_000_000, + payment_hash=lnaddr1.paymenthash, # Dave is cooperating with Alice, and he reuses Bob's hash + include_routing_hints=True, + ) + with self.subTest(msg="try to make Bob forward in legacy (non-trampoline) mode"): + result, log = await graph.workers['alice'].pay_invoice(pay_req2, attempts=1) + self.assertFalse(result) + self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code) + self.assertEqual(None, graph.workers['alice'].get_preimage(lnaddr1.paymenthash)) + with self.subTest(msg="try to make Bob forward in trampoline mode"): + # declare Bob as trampoline forwarding node + electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = { + graph.workers['bob'].name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.workers['bob'].node_keypair.pubkey), + } + await self._activate_trampoline(graph.workers['alice']) + result, log = await graph.workers['alice'].pay_invoice(pay_req2, attempts=5) + self.assertFalse(result) + self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code) + self.assertEqual(None, graph.workers['alice'].get_preimage(lnaddr1.paymenthash)) + raise SuccessfulTest() + + async def f(): + async with OldTaskGroup() as group: + for peer in peers: + await group.spawn(peer._message_loop()) + await group.spawn(peer.htlc_switch()) + for peer in peers: + await peer.initialized + await group.spawn(pay()) + + with self.assertRaises(SuccessfulTest): + await f() + @needs_test_with_all_chacha20_implementations async def test_payment_with_temp_channel_failure_and_liquidity_hints(self): # prepare channels such that a temporary channel failure happens at c->d From 44bdd20ccc40bb307cc3510d8741af7058e2c6e8 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 4 Aug 2023 13:27:05 +0000 Subject: [PATCH 1112/1143] lnworker: add RecvMPPResolution with "FAILED" state - add RecvMPPResolution enum for possible states of a pending incoming MPP, and use it in check_mpp_status - new state: "FAILED", to allow nicely failing back the whole MPP set - key more things with payment_hash+payment_secret, for consistency (just payment_hash is insufficient for trampoline forwarding) --- electrum/lnpeer.py | 19 ++++- electrum/lnworker.py | 139 +++++++++++++++++++--------------- electrum/tests/test_lnpeer.py | 4 +- 3 files changed, 94 insertions(+), 68 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 62833ac00..11edfe81a 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1826,14 +1826,25 @@ def log_fail_reason(reason: str): log_fail_reason(f"'payment_secret' missing from onion") raise exc_incorrect_or_unknown_pd - payment_status = self.lnworker.check_mpp_status(payment_secret_from_onion, chan.short_channel_id, htlc, total_msat) - if payment_status is None: + from .lnworker import RecvMPPResolution + mpp_resolution = self.lnworker.check_mpp_status( + payment_secret=payment_secret_from_onion, + short_channel_id=chan.short_channel_id, + htlc=htlc, + expected_msat=total_msat, + ) + if mpp_resolution == RecvMPPResolution.WAITING: return None, None - elif payment_status is False: + elif mpp_resolution == RecvMPPResolution.EXPIRED: log_fail_reason(f"MPP_TIMEOUT") raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') + elif mpp_resolution == RecvMPPResolution.FAILED: + log_fail_reason(f"mpp_resolution is FAILED") + raise exc_incorrect_or_unknown_pd + elif mpp_resolution == RecvMPPResolution.ACCEPTED: + pass # continue else: - assert payment_status is True + raise Exception(f"unexpected {mpp_resolution=}") payment_hash = htlc.payment_hash diff --git a/electrum/lnworker.py b/electrum/lnworker.py index d2f907b6e..53ec4e3e3 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -8,7 +8,8 @@ import random import time import operator -from enum import IntEnum +import enum +from enum import IntEnum, Enum from typing import (Optional, Sequence, Tuple, List, Set, Dict, TYPE_CHECKING, NamedTuple, Union, Mapping, Any, Iterable, AsyncGenerator, DefaultDict, Callable) import threading @@ -167,9 +168,15 @@ class PaymentInfo(NamedTuple): status: int +class RecvMPPResolution(Enum): + WAITING = enum.auto() + EXPIRED = enum.auto() + ACCEPTED = enum.auto() + FAILED = enum.auto() + + class ReceivedMPPStatus(NamedTuple): - is_expired: bool - is_accepted: bool + resolution: RecvMPPResolution expected_msat: int htlc_set: Set[Tuple[ShortChannelID, UpdateAddHtlc]] @@ -673,8 +680,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.sent_htlcs = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]] self.sent_htlcs_info = dict() # (RHASH, scid, htlc_id) -> route, payment_secret, amount_msat, bucket_msat, trampoline_fee_level - self.sent_buckets = dict() # payment_secret -> (amount_sent, amount_failed) - self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_secret -> ReceivedMPPStatus + self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) + self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_key -> ReceivedMPPStatus self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self) # detect inflight payments @@ -1418,13 +1425,14 @@ async def pay_to_route( key = (payment_hash, short_channel_id, htlc.htlc_id) self.sent_htlcs_info[key] = route, payment_secret, amount_msat, total_msat, amount_receiver_msat, trampoline_fee_level, trampoline_route + payment_key = payment_hash + payment_secret # if we sent MPP to a trampoline, add item to sent_buckets if self.uses_trampoline() and amount_msat != total_msat: - if payment_secret not in self.sent_buckets: - self.sent_buckets[payment_secret] = (0, 0) - amount_sent, amount_failed = self.sent_buckets[payment_secret] + if payment_key not in self.sent_buckets: + self.sent_buckets[payment_key] = (0, 0) + amount_sent, amount_failed = self.sent_buckets[payment_key] amount_sent += amount_receiver_msat - self.sent_buckets[payment_secret] = amount_sent, amount_failed + self.sent_buckets[payment_key] = amount_sent, amount_failed if self.network.path_finder: # add inflight htlcs to liquidity hints self.network.path_finder.update_inflight_htlcs(route, add_htlcs=True) @@ -1867,6 +1875,14 @@ def get_bolt11_invoice( def get_payment_secret(self, payment_hash): return sha256(sha256(self.payment_secret_key) + payment_hash) + def _get_payment_key(self, payment_hash: bytes) -> bytes: + """Return payment bucket key. + We bucket htlcs based on payment_hash+payment_secret. payment_secret is included + as it changes over a trampoline path (in the outer onion), and these paths can overlap. + """ + payment_secret = self.get_payment_secret(payment_hash) + return payment_hash + payment_secret + def create_payment_info(self, *, amount_msat: Optional[int], write_to_disk=True) -> bytes: payment_preimage = os.urandom(32) payment_hash = sha256(payment_preimage) @@ -1923,103 +1939,101 @@ def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> self.wallet.save_db() def check_mpp_status( - self, payment_secret: bytes, + self, *, + payment_secret: bytes, short_channel_id: ShortChannelID, htlc: UpdateAddHtlc, expected_msat: int, - ) -> Optional[bool]: - """ return MPP status: True (accepted), False (expired) or None (waiting) - """ + ) -> RecvMPPResolution: payment_hash = htlc.payment_hash - self.update_mpp_with_received_htlc(payment_secret, short_channel_id, htlc, expected_msat) - is_expired, is_accepted = self.get_mpp_status(payment_secret) - if not is_accepted and not is_expired: + payment_key = payment_hash + payment_secret + self.update_mpp_with_received_htlc( + payment_key=payment_key, scid=short_channel_id, htlc=htlc, expected_msat=expected_msat) + mpp_resolution = self.received_mpp_htlcs[payment_key].resolution + if mpp_resolution == RecvMPPResolution.WAITING: bundle = self.get_payment_bundle(payment_hash) if bundle: - payment_secrets = [self.get_payment_secret(h) for h in bundle] - if payment_secret not in payment_secrets: + payment_keys = [self._get_payment_key(h) for h in bundle] + if payment_key not in payment_keys: # outer trampoline onion secret differs from inner onion # the latter, not the former, might be part of a bundle - payment_secrets = [payment_secret] + payment_keys = [payment_key] else: - payment_secrets = [payment_secret] - first_timestamp = min([self.get_first_timestamp_of_mpp(x) for x in payment_secrets]) + payment_keys = [payment_key] + first_timestamp = min([self.get_first_timestamp_of_mpp(pkey) for pkey in payment_keys]) if self.get_payment_status(payment_hash) == PR_PAID: - is_accepted = True + mpp_resolution = RecvMPPResolution.ACCEPTED elif self.stopping_soon: - is_expired = True # try to time out pending HTLCs before shutting down - elif all([self.is_mpp_amount_reached(x) for x in payment_secrets]): - is_accepted = True + # try to time out pending HTLCs before shutting down + mpp_resolution = RecvMPPResolution.EXPIRED + elif all([self.is_mpp_amount_reached(pkey) for pkey in payment_keys]): + mpp_resolution = RecvMPPResolution.ACCEPTED elif time.time() - first_timestamp > self.MPP_EXPIRY: - is_expired = True + mpp_resolution = RecvMPPResolution.EXPIRED - if is_accepted or is_expired: - for x in payment_secrets: - if x in self.received_mpp_htlcs: - self.set_mpp_status(x, is_expired, is_accepted) + if mpp_resolution != RecvMPPResolution.WAITING: + for pkey in payment_keys: + if pkey in self.received_mpp_htlcs: + self.set_mpp_resolution(payment_key=pkey, resolution=mpp_resolution) - self.maybe_cleanup_mpp_status(payment_secret, short_channel_id, htlc) - return True if is_accepted else (False if is_expired else None) + self.maybe_cleanup_mpp_status(payment_key, short_channel_id, htlc) + return mpp_resolution def update_mpp_with_received_htlc( self, - payment_secret: bytes, - short_channel_id: ShortChannelID, + *, + payment_key: bytes, + scid: ShortChannelID, htlc: UpdateAddHtlc, expected_msat: int, ): # add new htlc to set - mpp_status = self.received_mpp_htlcs.get(payment_secret) + mpp_status = self.received_mpp_htlcs.get(payment_key) if mpp_status is None: mpp_status = ReceivedMPPStatus( - is_expired=False, - is_accepted=False, + resolution=RecvMPPResolution.WAITING, expected_msat=expected_msat, htlc_set=set(), ) - assert expected_msat == mpp_status.expected_msat - key = (short_channel_id, htlc) + if expected_msat != mpp_status.expected_msat: + self.logger.info( + f"marking received mpp as failed. inconsistent total_msats in bucket. {payment_key.hex()=}") + mpp_status = mpp_status._replace(resolution=RecvMPPResolution.FAILED) + key = (scid, htlc) if key not in mpp_status.htlc_set: mpp_status.htlc_set.add(key) # side-effecting htlc_set - self.received_mpp_htlcs[payment_secret] = mpp_status - - def get_mpp_status(self, payment_secret: bytes) -> Tuple[bool, bool]: - mpp_status = self.received_mpp_htlcs[payment_secret] - return mpp_status.is_expired, mpp_status.is_accepted + self.received_mpp_htlcs[payment_key] = mpp_status - def set_mpp_status(self, payment_secret: bytes, is_expired: bool, is_accepted: bool): - mpp_status = self.received_mpp_htlcs[payment_secret] - self.received_mpp_htlcs[payment_secret] = mpp_status._replace( - is_expired=is_expired, - is_accepted=is_accepted, - ) + def set_mpp_resolution(self, *, payment_key: bytes, resolution: RecvMPPResolution): + mpp_status = self.received_mpp_htlcs[payment_key] + self.received_mpp_htlcs[payment_key] = mpp_status._replace(resolution=resolution) - def is_mpp_amount_reached(self, payment_secret: bytes) -> bool: - mpp_status = self.received_mpp_htlcs.get(payment_secret) + def is_mpp_amount_reached(self, payment_key: bytes) -> bool: + mpp_status = self.received_mpp_htlcs.get(payment_key) if not mpp_status: return False total = sum([_htlc.amount_msat for scid, _htlc in mpp_status.htlc_set]) return total >= mpp_status.expected_msat - def get_first_timestamp_of_mpp(self, payment_secret: bytes) -> int: - mpp_status = self.received_mpp_htlcs.get(payment_secret) + def get_first_timestamp_of_mpp(self, payment_key: bytes) -> int: + mpp_status = self.received_mpp_htlcs.get(payment_key) if not mpp_status: return int(time.time()) return min([_htlc.timestamp for scid, _htlc in mpp_status.htlc_set]) def maybe_cleanup_mpp_status( self, - payment_secret: bytes, + payment_key: bytes, short_channel_id: ShortChannelID, htlc: UpdateAddHtlc, ) -> None: - mpp_status = self.received_mpp_htlcs[payment_secret] - if not mpp_status.is_accepted and not mpp_status.is_expired: + mpp_status = self.received_mpp_htlcs[payment_key] + if mpp_status.resolution == RecvMPPResolution.WAITING: return key = (short_channel_id, htlc) mpp_status.htlc_set.remove(key) # side-effecting htlc_set - if not mpp_status.htlc_set and payment_secret in self.received_mpp_htlcs: - self.received_mpp_htlcs.pop(payment_secret) + if not mpp_status.htlc_set and payment_key in self.received_mpp_htlcs: + self.received_mpp_htlcs.pop(payment_key) def get_payment_status(self, payment_hash: bytes) -> int: info = self.get_payment_info(payment_hash) @@ -2126,10 +2140,11 @@ def htlc_failed( self.logger.info(f"htlc_failed {failure_message}") # check sent_buckets if we use trampoline - if self.uses_trampoline() and payment_secret in self.sent_buckets: - amount_sent, amount_failed = self.sent_buckets[payment_secret] + payment_key = payment_hash + payment_secret + if self.uses_trampoline() and payment_key in self.sent_buckets: + amount_sent, amount_failed = self.sent_buckets[payment_key] amount_failed += amount_receiver_msat - self.sent_buckets[payment_secret] = amount_sent, amount_failed + self.sent_buckets[payment_key] = amount_sent, amount_failed if amount_sent != amount_failed: self.logger.info('bucket still active...') return diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index ad072fe44..8c7ecbeee 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -283,13 +283,13 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln add_payment_info_for_hold_invoice = LNWallet.add_payment_info_for_hold_invoice update_mpp_with_received_htlc = LNWallet.update_mpp_with_received_htlc - get_mpp_status = LNWallet.get_mpp_status - set_mpp_status = LNWallet.set_mpp_status + set_mpp_resolution = LNWallet.set_mpp_resolution is_mpp_amount_reached = LNWallet.is_mpp_amount_reached get_first_timestamp_of_mpp = LNWallet.get_first_timestamp_of_mpp maybe_cleanup_mpp_status = LNWallet.maybe_cleanup_mpp_status bundle_payments = LNWallet.bundle_payments get_payment_bundle = LNWallet.get_payment_bundle + _get_payment_key = LNWallet._get_payment_key class MockTransport: From afac158c8004125e0b063d8bec03467ca5489a99 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 8 Aug 2023 16:28:20 +0000 Subject: [PATCH 1113/1143] lnworker: clean-up sent_htlcs_q and sent_htlcs_info - introduce SentHtlcInfo named tuple - some previously unnamed tuples are now much shorter: create_routes_for_payment no longer returns an 8-tuple! - sent_htlcs_q (renamed from sent_htlcs), is now keyed on payment_hash+payment_secret (needed for proper trampoline forwarding) --- electrum/lnpeer.py | 2 + electrum/lnworker.py | 137 +++++++++++++++++++++------------- electrum/tests/test_lnpeer.py | 85 +++++++++++---------- 3 files changed, 132 insertions(+), 92 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 11edfe81a..b5f7843ff 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1742,6 +1742,8 @@ async def maybe_forward_trampoline( except OnionRoutingFailure as e: raise except PaymentFailure as e: + self.logger.debug( + f"maybe_forward_trampoline. PaymentFailure for {payment_hash.hex()=}, {payment_secret.hex()=}: {e!r}") # FIXME: adapt the error code raise OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 53ec4e3e3..bfc8cb5b0 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -69,7 +69,7 @@ NoPathFound, InvalidGossipMsg) from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput -from .lnonion import OnionFailureCode, OnionRoutingFailure +from .lnonion import OnionFailureCode, OnionRoutingFailure, OnionPacket from .lnmsg import decode_msg from .i18n import _ from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use, @@ -181,6 +181,20 @@ class ReceivedMPPStatus(NamedTuple): htlc_set: Set[Tuple[ShortChannelID, UpdateAddHtlc]] +SentHtlcKey = Tuple[bytes, ShortChannelID, int] # RHASH, scid, htlc_id + + +class SentHtlcInfo(NamedTuple): + route: LNPaymentRoute + payment_secret_orig: bytes + payment_secret_bucket: bytes + amount_msat: int + bucket_msat: int + amount_receiver_msat: int + trampoline_fee_level: Optional[int] + trampoline_route: Optional[LNPaymentRoute] + + class ErrorAddingPeer(Exception): pass @@ -678,8 +692,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): for channel_id, storage in channel_backups.items(): self._channel_backups[bfh(channel_id)] = ChannelBackup(storage, lnworker=self) - self.sent_htlcs = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]] - self.sent_htlcs_info = dict() # (RHASH, scid, htlc_id) -> route, payment_secret, amount_msat, bucket_msat, trampoline_fee_level + self.sent_htlcs_q = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]] + self.sent_htlcs_info = dict() # type: Dict[SentHtlcKey, SentHtlcInfo] self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_key -> ReceivedMPPStatus @@ -1268,7 +1282,8 @@ async def pay_to_node( if fwd_trampoline_cltv_delta < 576: raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'') - self.logs[payment_hash.hex()] = log = [] + payment_key = payment_hash + payment_secret + self.logs[payment_hash.hex()] = log = [] # TODO incl payment_secret in key (re trampoline forwarding) # when encountering trampoline forwarding difficulties in the legacy case, we # sometimes need to fall back to a single trampoline forwarder, at the expense @@ -1300,28 +1315,24 @@ async def pay_to_node( channels=channels, ) # 2. send htlcs - async for route, amount_msat, total_msat, amount_receiver_msat, cltv_delta, bucket_payment_secret, trampoline_onion, trampoline_route in routes: - amount_inflight += amount_receiver_msat + async for sent_htlc_info, cltv_delta, trampoline_onion in routes: + amount_inflight += sent_htlc_info.amount_receiver_msat if amount_inflight > amount_to_pay: # safety belts raise Exception(f"amount_inflight={amount_inflight} > amount_to_pay={amount_to_pay}") + sent_htlc_info = sent_htlc_info._replace(trampoline_fee_level=self.trampoline_fee_level) await self.pay_to_route( - route=route, - amount_msat=amount_msat, - total_msat=total_msat, - amount_receiver_msat=amount_receiver_msat, + sent_htlc_info=sent_htlc_info, payment_hash=payment_hash, - payment_secret=bucket_payment_secret, min_cltv_expiry=cltv_delta, trampoline_onion=trampoline_onion, - trampoline_fee_level=self.trampoline_fee_level, - trampoline_route=trampoline_route) + ) # invoice_status is triggered in self.set_invoice_status when it actally changes. # It is also triggered here to update progress for a lightning payment in the GUI # (e.g. attempt counter) util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) # 3. await a queue self.logger.info(f"amount inflight {amount_inflight}") - htlc_log = await self.sent_htlcs[payment_hash].get() + htlc_log = await self.sent_htlcs_q[payment_key].get() amount_inflight -= htlc_log.amount_msat if amount_inflight < 0: raise Exception(f"amount_inflight={amount_inflight} < 0") @@ -1394,48 +1405,44 @@ def maybe_raise_trampoline_fee(htlc_log): async def pay_to_route( self, *, - route: LNPaymentRoute, - amount_msat: int, - total_msat: int, - amount_receiver_msat:int, + sent_htlc_info: SentHtlcInfo, payment_hash: bytes, - payment_secret: bytes, min_cltv_expiry: int, trampoline_onion: bytes = None, - trampoline_fee_level: int, - trampoline_route: Optional[List]) -> None: - - # send a single htlc - short_channel_id = route[0].short_channel_id + ) -> None: + """Sends a single HTLC.""" + shi = sent_htlc_info + del sent_htlc_info # just renamed + short_channel_id = shi.route[0].short_channel_id chan = self.get_channel_by_short_id(short_channel_id) assert chan, ShortChannelID(short_channel_id) - peer = self._peers.get(route[0].node_id) + peer = self._peers.get(shi.route[0].node_id) if not peer: raise PaymentFailure('Dropped peer') await peer.initialized htlc = peer.pay( - route=route, + route=shi.route, chan=chan, - amount_msat=amount_msat, - total_msat=total_msat, + amount_msat=shi.amount_msat, + total_msat=shi.bucket_msat, payment_hash=payment_hash, min_final_cltv_expiry=min_cltv_expiry, - payment_secret=payment_secret, + payment_secret=shi.payment_secret_bucket, trampoline_onion=trampoline_onion) key = (payment_hash, short_channel_id, htlc.htlc_id) - self.sent_htlcs_info[key] = route, payment_secret, amount_msat, total_msat, amount_receiver_msat, trampoline_fee_level, trampoline_route - payment_key = payment_hash + payment_secret + self.sent_htlcs_info[key] = shi + payment_key = payment_hash + shi.payment_secret_bucket # if we sent MPP to a trampoline, add item to sent_buckets - if self.uses_trampoline() and amount_msat != total_msat: + if self.uses_trampoline() and shi.amount_msat != shi.bucket_msat: if payment_key not in self.sent_buckets: self.sent_buckets[payment_key] = (0, 0) amount_sent, amount_failed = self.sent_buckets[payment_key] - amount_sent += amount_receiver_msat + amount_sent += shi.amount_receiver_msat self.sent_buckets[payment_key] = amount_sent, amount_failed if self.network.path_finder: # add inflight htlcs to liquidity hints - self.network.path_finder.update_inflight_htlcs(route, add_htlcs=True) + self.network.path_finder.update_inflight_htlcs(shi.route, add_htlcs=True) util.trigger_callback('htlc_added', chan, htlc, SENT) def handle_error_code_from_failed_htlc( @@ -1633,7 +1640,7 @@ async def create_routes_for_payment( fwd_trampoline_onion=None, full_path: LNPaymentPath = None, channels: Optional[Sequence[Channel]] = None, - ) -> AsyncGenerator[Tuple[LNPaymentRoute, int], None]: + ) -> AsyncGenerator[Tuple[SentHtlcInfo, int, Optional[OnionPacket]], None]: """Creates multiple routes for splitting a payment over the available private channels. @@ -1719,7 +1726,17 @@ async def create_routes_for_payment( node_features=trampoline_features) ] self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}') - routes.append((route, part_amount_msat_with_fees, per_trampoline_amount_with_fees, part_amount_msat, per_trampoline_cltv_delta, per_trampoline_secret, trampoline_onion, trampoline_route)) + shi = SentHtlcInfo( + route=route, + payment_secret_orig=payment_secret, + payment_secret_bucket=per_trampoline_secret, + amount_msat=part_amount_msat_with_fees, + bucket_msat=per_trampoline_amount_with_fees, + amount_receiver_msat=part_amount_msat, + trampoline_fee_level=None, + trampoline_route=trampoline_route, + ) + routes.append((shi, per_trampoline_cltv_delta, trampoline_onion)) if per_trampoline_fees != 0: self.logger.info('not enough margin to pay trampoline fee') raise NoPathFound() @@ -1741,7 +1758,17 @@ async def create_routes_for_payment( full_path=full_path, ) ) - routes.append((route, part_amount_msat, final_total_msat, part_amount_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion, None)) + shi = SentHtlcInfo( + route=route, + payment_secret_orig=payment_secret, + payment_secret_bucket=payment_secret, + amount_msat=part_amount_msat, + bucket_msat=final_total_msat, + amount_receiver_msat=part_amount_msat, + trampoline_fee_level=None, + trampoline_route=None, + ) + routes.append((shi, min_cltv_expiry, fwd_trampoline_onion)) except NoPathFound: continue for route in routes: @@ -2096,14 +2123,16 @@ def _on_maybe_forwarded_htlc_resolved(self, chan: Channel, htlc_id: int) -> None def htlc_fulfilled(self, chan: Channel, payment_hash: bytes, htlc_id: int): util.trigger_callback('htlc_fulfilled', payment_hash, chan, htlc_id) self._on_maybe_forwarded_htlc_resolved(chan=chan, htlc_id=htlc_id) - q = self.sent_htlcs.get(payment_hash) + q = None + if shi := self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id)): + payment_key = payment_hash + shi.payment_secret_orig + q = self.sent_htlcs_q.get(payment_key) if q: - route, payment_secret, amount_msat, bucket_msat, amount_receiver_msat, trampoline_fee_level, trampoline_route = self.sent_htlcs_info[(payment_hash, chan.short_channel_id, htlc_id)] htlc_log = HtlcLog( success=True, - route=route, - amount_msat=amount_receiver_msat, - trampoline_fee_level=trampoline_fee_level) + route=shi.route, + amount_msat=shi.amount_receiver_msat, + trampoline_fee_level=shi.trampoline_fee_level) q.put_nowait(htlc_log) else: key = payment_hash.hex() @@ -2120,12 +2149,16 @@ def htlc_failed( util.trigger_callback('htlc_failed', payment_hash, chan, htlc_id) self._on_maybe_forwarded_htlc_resolved(chan=chan, htlc_id=htlc_id) - q = self.sent_htlcs.get(payment_hash) + q = None + if shi := self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id)): + payment_okey = payment_hash + shi.payment_secret_orig + q = self.sent_htlcs_q.get(payment_okey) if q: # detect if it is part of a bucket # if yes, wait until the bucket completely failed - key = (payment_hash, chan.short_channel_id, htlc_id) - route, payment_secret, amount_msat, bucket_msat, amount_receiver_msat, trampoline_fee_level, trampoline_route = self.sent_htlcs_info[key] + shi = self.sent_htlcs_info[(payment_hash, chan.short_channel_id, htlc_id)] + amount_receiver_msat = shi.amount_receiver_msat + route = shi.route if error_bytes: # TODO "decode_onion_error" might raise, catch and maybe blacklist/penalise someone? try: @@ -2140,19 +2173,19 @@ def htlc_failed( self.logger.info(f"htlc_failed {failure_message}") # check sent_buckets if we use trampoline - payment_key = payment_hash + payment_secret - if self.uses_trampoline() and payment_key in self.sent_buckets: - amount_sent, amount_failed = self.sent_buckets[payment_key] + payment_bkey = payment_hash + shi.payment_secret_bucket + if self.uses_trampoline() and payment_bkey in self.sent_buckets: + amount_sent, amount_failed = self.sent_buckets[payment_bkey] amount_failed += amount_receiver_msat - self.sent_buckets[payment_key] = amount_sent, amount_failed + self.sent_buckets[payment_bkey] = amount_sent, amount_failed if amount_sent != amount_failed: self.logger.info('bucket still active...') return self.logger.info('bucket failed') amount_receiver_msat = amount_sent - if trampoline_route: - route = trampoline_route + if shi.trampoline_route: + route = shi.trampoline_route htlc_log = HtlcLog( success=False, route=route, @@ -2160,7 +2193,7 @@ def htlc_failed( error_bytes=error_bytes, failure_msg=failure_message, sender_idx=sender_idx, - trampoline_fee_level=trampoline_fee_level) + trampoline_fee_level=shi.trampoline_fee_level) q.put_nowait(htlc_log) else: self.logger.info(f"received unknown htlc_failed, probably from previous session") diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 8c7ecbeee..0bd463fcc 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -31,7 +31,7 @@ from electrum.lnchannel import ChannelState, PeerState, Channel from electrum.lnrouter import LNPathFinder, PathEdge, LNPathInconsistent from electrum.channel_db import ChannelDB -from electrum.lnworker import LNWallet, NoPathFound +from electrum.lnworker import LNWallet, NoPathFound, SentHtlcInfo from electrum.lnmsg import encode_msg, decode_msg from electrum import lnmsg from electrum.logging import console_stderr_handler, Logger @@ -166,7 +166,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.enable_htlc_settle = True self.enable_htlc_forwarding = True self.received_mpp_htlcs = dict() - self.sent_htlcs = defaultdict(asyncio.Queue) + self.sent_htlcs_q = defaultdict(asyncio.Queue) self.sent_htlcs_info = dict() self.sent_buckets = defaultdict(set) self.trampoline_forwardings = set() @@ -740,7 +740,7 @@ async def f(): with self.assertRaises(SuccessfulTest): await f() - async def _activate_trampoline(self, w): + async def _activate_trampoline(self, w: MockLNWallet): if w.network.channel_db: w.network.channel_db.stop() await w.network.channel_db.stopped_event.wait() @@ -837,38 +837,44 @@ async def pay(): lnaddr2, pay_req2 = self.prepare_invoice(w2) lnaddr1, pay_req1 = self.prepare_invoice(w1) # create the htlc queues now (side-effecting defaultdict) - q1 = w1.sent_htlcs[lnaddr2.paymenthash] - q2 = w2.sent_htlcs[lnaddr1.paymenthash] + q1 = w1.sent_htlcs_q[lnaddr2.paymenthash + lnaddr2.payment_secret] + q2 = w2.sent_htlcs_q[lnaddr1.paymenthash + lnaddr1.payment_secret] # alice sends htlc BUT NOT COMMITMENT_SIGNED p1.maybe_send_commitment = lambda x: None - route1 = (await w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2))[0][0] - amount_msat = lnaddr2.get_amount_msat() - await w1.pay_to_route( + route1 = (await w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2))[0][0].route + shi1 = SentHtlcInfo( route=route1, - amount_msat=amount_msat, - total_msat=amount_msat, - amount_receiver_msat=amount_msat, + payment_secret_orig=lnaddr2.payment_secret, + payment_secret_bucket=lnaddr2.payment_secret, + amount_msat=lnaddr2.get_amount_msat(), + bucket_msat=lnaddr2.get_amount_msat(), + amount_receiver_msat=lnaddr2.get_amount_msat(), + trampoline_fee_level=None, + trampoline_route=None, + ) + await w1.pay_to_route( + sent_htlc_info=shi1, payment_hash=lnaddr2.paymenthash, min_cltv_expiry=lnaddr2.get_min_final_cltv_expiry(), - payment_secret=lnaddr2.payment_secret, - trampoline_fee_level=0, - trampoline_route=None, ) p1.maybe_send_commitment = _maybe_send_commitment1 # bob sends htlc BUT NOT COMMITMENT_SIGNED p2.maybe_send_commitment = lambda x: None - route2 = (await w2.create_routes_from_invoice(lnaddr1.get_amount_msat(), decoded_invoice=lnaddr1))[0][0] - amount_msat = lnaddr1.get_amount_msat() - await w2.pay_to_route( + route2 = (await w2.create_routes_from_invoice(lnaddr1.get_amount_msat(), decoded_invoice=lnaddr1))[0][0].route + shi2 = SentHtlcInfo( route=route2, - amount_msat=amount_msat, - total_msat=amount_msat, - amount_receiver_msat=amount_msat, + payment_secret_orig=lnaddr1.payment_secret, + payment_secret_bucket=lnaddr1.payment_secret, + amount_msat=lnaddr1.get_amount_msat(), + bucket_msat=lnaddr1.get_amount_msat(), + amount_receiver_msat=lnaddr1.get_amount_msat(), + trampoline_fee_level=None, + trampoline_route=None, + ) + await w2.pay_to_route( + sent_htlc_info=shi2, payment_hash=lnaddr1.paymenthash, min_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), - payment_secret=lnaddr1.payment_secret, - trampoline_fee_level=0, - trampoline_route=None, ) p2.maybe_send_commitment = _maybe_send_commitment2 # sleep a bit so that they both receive msgs sent so far @@ -878,9 +884,9 @@ async def pay(): p2.maybe_send_commitment(bob_channel) htlc_log1 = await q1.get() - assert htlc_log1.success + self.assertTrue(htlc_log1.success) htlc_log2 = await q2.get() - assert htlc_log2.success + self.assertTrue(htlc_log2.success) raise PaymentDone() async def f(): @@ -1184,10 +1190,7 @@ async def pay( if not bob_forwarding: graph.workers['bob'].enable_htlc_forwarding = False if alice_uses_trampoline: - if graph.workers['alice'].network.channel_db: - graph.workers['alice'].network.channel_db.stop() - await graph.workers['alice'].network.channel_db.stopped_event.wait() - graph.workers['alice'].network.channel_db = None + await self._activate_trampoline(graph.workers['alice']) else: assert graph.workers['alice'].network.channel_db is not None lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True, amount_msat=amount_to_pay) @@ -1433,7 +1436,7 @@ async def pay(): await util.wait_for2(p1.initialized, 1) await util.wait_for2(p2.initialized, 1) # alice sends htlc - route, amount_msat = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0:2] + route = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0].route p1.pay(route=route, chan=alice_channel, amount_msat=lnaddr.get_amount_msat(), @@ -1556,7 +1559,8 @@ async def test_channel_usage_after_closing(self): lnaddr, pay_req = self.prepare_invoice(w2) lnaddr = w1._check_invoice(pay_req) - route, amount_msat = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0:2] + shi = (await w1.create_routes_from_invoice(lnaddr.get_amount_msat(), decoded_invoice=lnaddr))[0][0] + route, amount_msat = shi.route, shi.amount_msat assert amount_msat == lnaddr.get_amount_msat() await w1.force_close_channel(alice_channel.channel_id) @@ -1570,20 +1574,21 @@ async def test_channel_usage_after_closing(self): # AssertionError is ok since we shouldn't use old routes, and the # route finding should fail when channel is closed async def f(): - min_cltv_expiry = lnaddr.get_min_final_cltv_expiry() - payment_hash = lnaddr.paymenthash - payment_secret = lnaddr.payment_secret - pay = w1.pay_to_route( + shi = SentHtlcInfo( route=route, + payment_secret_orig=lnaddr.payment_secret, + payment_secret_bucket=lnaddr.payment_secret, amount_msat=amount_msat, - total_msat=amount_msat, + bucket_msat=amount_msat, amount_receiver_msat=amount_msat, - payment_hash=payment_hash, - payment_secret=payment_secret, - min_cltv_expiry=min_cltv_expiry, - trampoline_fee_level=0, + trampoline_fee_level=None, trampoline_route=None, ) + pay = w1.pay_to_route( + sent_htlc_info=shi, + payment_hash=lnaddr.paymenthash, + min_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), + ) await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) with self.assertRaises(PaymentFailure): await f() From 47a591b87fdb6a48008dcd487fc943915c8d4c68 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 8 Aug 2023 17:13:10 +0000 Subject: [PATCH 1114/1143] lnworker.pay_invoice: log more related https://github.com/spesmilo/electrum/issues/7987#issuecomment-1670002482 --- electrum/lnworker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index bfc8cb5b0..eb33114c3 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1225,7 +1225,10 @@ async def pay_invoice( self.save_payment_info(info) self.wallet.set_label(key, lnaddr.get_description()) - self.logger.info(f"pay_invoice starting session for RHASH={payment_hash.hex()}") + self.logger.info( + f"pay_invoice starting session for RHASH={payment_hash.hex()}. " + f"using_trampoline={self.uses_trampoline()}. " + f"invoice_features={invoice_features.get_names()}") self.set_invoice_status(key, PR_INFLIGHT) success = False try: From bf86cd67616ac78573e8d4af2adc657a38a7f2d4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 9 Aug 2023 11:44:41 +0200 Subject: [PATCH 1115/1143] lnpeer and lnworker cleanup: - rename trampoline_forwardings -> final_onion_forwardings, because this dict is used for both trampoline and hold invoices - remove timeout from hold_invoice_callbacks (redundant with invoice) - add test_failure boolean parameter to TestPeer._test_simple_payment, in order to test correct propagation of OnionRoutingFailures. - maybe_fulfill_htlc: raise an OnionRoutingFailure if we do not have the preimage for a payment that does not have a hold invoice callback. Without this, the above unit tests stall when we use test_failure=True --- electrum/lnpeer.py | 23 +++++++++-------------- electrum/lnworker.py | 15 ++++++--------- electrum/submarine_swaps.py | 4 ++-- electrum/tests/test_lnpeer.py | 35 +++++++++++++++++++++++++---------- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index b5f7843ff..b3f058cc5 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1887,12 +1887,7 @@ def log_fail_reason(reason: str): if preimage: return preimage, None else: - # for hold invoices, trigger callback - cb, timeout = hold_invoice_callback - if int(time.time()) < timeout: - return None, lambda: cb(payment_hash) - else: - raise exc_incorrect_or_unknown_pd + return None, lambda: hold_invoice_callback(payment_hash) # TODO don't accept payments twice for same invoice # TODO check invoice expiry @@ -1903,8 +1898,8 @@ def log_fail_reason(reason: str): preimage = self.lnworker.get_preimage(payment_hash) if not preimage: - self.logger.info(f"missing callback {payment_hash.hex()}") - return None, None + self.logger.info(f"missing preimage and no hold invoice callback {payment_hash.hex()}") + raise exc_incorrect_or_unknown_pd expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)] expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) # legacy secret for old invoices @@ -2424,23 +2419,23 @@ def process_unfulfilled_htlc( # trampoline- HTLC we are supposed to forward, but haven't forwarded yet if not self.lnworker.enable_htlc_forwarding: pass - elif payment_key in self.lnworker.trampoline_forwardings: + elif payment_key in self.lnworker.final_onion_forwardings: # we are already forwarding this payment self.logger.info(f"we are already forwarding this.") else: # add to list of ongoing payments - self.lnworker.trampoline_forwardings.add(payment_key) + self.lnworker.final_onion_forwardings.add(payment_key) # clear previous failures - self.lnworker.trampoline_forwarding_failures.pop(payment_key, None) + self.lnworker.final_onion_forwarding_failures.pop(payment_key, None) async def wrapped_callback(): forwarding_coro = forwarding_callback() try: await forwarding_coro except OnionRoutingFailure as e: - self.lnworker.trampoline_forwarding_failures[payment_key] = e + self.lnworker.final_onion_forwarding_failures[payment_key] = e finally: # remove from list of payments, so that another attempt can be initiated - self.lnworker.trampoline_forwardings.remove(payment_key) + self.lnworker.final_onion_forwardings.remove(payment_key) asyncio.ensure_future(wrapped_callback()) fw_info = payment_key.hex() return None, fw_info, None @@ -2449,7 +2444,7 @@ async def wrapped_callback(): payment_key = bytes.fromhex(forwarding_info) preimage = self.lnworker.get_preimage(payment_hash) # get (and not pop) failure because the incoming payment might be multi-part - error_reason = self.lnworker.trampoline_forwarding_failures.get(payment_key) + error_reason = self.lnworker.final_onion_forwarding_failures.get(payment_key) if error_reason: self.logger.info(f'trampoline forwarding failure: {error_reason.code_name()}') raise error_reason diff --git a/electrum/lnworker.py b/electrum/lnworker.py index eb33114c3..44170d4d4 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -703,8 +703,8 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): for payment_hash in self.get_payments(status='inflight').keys(): self.set_invoice_status(payment_hash.hex(), PR_INFLIGHT) - self.trampoline_forwardings = set() - self.trampoline_forwarding_failures = {} # todo: should be persisted + self.final_onion_forwardings = set() + self.final_onion_forwarding_failures = {} # todo: should be persisted # map forwarded htlcs (fw_info=(scid_hex, htlc_id)) to originating peer pubkeys self.downstream_htlc_to_upstream_peer_map = {} # type: Dict[Tuple[str, int], bytes] # payment_hash -> callback, timeout: @@ -1954,11 +1954,8 @@ def add_payment_info_for_hold_invoice(self, payment_hash: bytes, lightning_amoun info = PaymentInfo(payment_hash, lightning_amount_sat * 1000, RECEIVED, PR_UNPAID) self.save_payment_info(info, write_to_disk=False) - def register_callback_for_hold_invoice( - self, payment_hash: bytes, cb: Callable[[bytes], None], timeout: int, - ): - expiry = int(time.time()) + timeout - self.hold_invoice_callbacks[payment_hash] = cb, expiry + def register_callback_for_hold_invoice(self, payment_hash: bytes, cb: Callable[[bytes], None]): + self.hold_invoice_callbacks[payment_hash] = cb def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> None: key = info.payment_hash.hex() @@ -2758,7 +2755,7 @@ def maybe_add_backup_from_tx(self, tx): util.trigger_callback('channels_updated', self.wallet) self.lnwatcher.add_channel(cb.funding_outpoint.to_str(), cb.get_funding_address()) - def fail_trampoline_forwarding(self, payment_key): + def fail_final_onion_forwarding(self, payment_key): """ use this to fail htlcs received for hold invoices""" e = OnionRoutingFailure(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') - self.trampoline_forwarding_failures[payment_key] = e + self.final_onion_forwarding_failures[payment_key] = e diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 5d40a72fb..17ea412e2 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -247,7 +247,7 @@ async def _claim_swap(self, swap: SwapData) -> None: self.logger.info(f'found confirmed refund') payment_secret = self.lnworker.get_payment_secret(swap.payment_hash) payment_key = swap.payment_hash + payment_secret - self.lnworker.fail_trampoline_forwarding(payment_key) + self.lnworker.fail_final_onion_forwarding(payment_key) if delta < 0: # too early for refund @@ -343,7 +343,7 @@ def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoi ) # add payment info to lnworker self.lnworker.add_payment_info_for_hold_invoice(payment_hash, main_amount_sat) - self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback, 60*60*24) + self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) _, prepay_invoice = self.lnworker.get_bolt11_invoice( payment_hash=prepay_hash, diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 0bd463fcc..544e0d9ed 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -36,7 +36,7 @@ from electrum import lnmsg from electrum.logging import console_stderr_handler, Logger from electrum.lnworker import PaymentInfo, RECEIVED -from electrum.lnonion import OnionFailureCode +from electrum.lnonion import OnionFailureCode, OnionRoutingFailure from electrum.lnutil import UpdateAddHtlc from electrum.lnutil import LOCAL, REMOTE from electrum.invoices import PR_PAID, PR_UNPAID @@ -169,8 +169,8 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.sent_htlcs_q = defaultdict(asyncio.Queue) self.sent_htlcs_info = dict() self.sent_buckets = defaultdict(set) - self.trampoline_forwardings = set() - self.trampoline_forwarding_failures = {} + self.final_onion_forwardings = set() + self.final_onion_forwarding_failures = {} self.inflight_payments = set() self.preimages = {} self.stopping_soon = False @@ -749,6 +749,7 @@ async def _activate_trampoline(self, w: MockLNWallet): async def _test_simple_payment( self, test_trampoline: bool, + test_failure:bool=False, test_hold_invoice=False, test_bundle=False, test_bundle_timeout=False @@ -765,12 +766,16 @@ async def pay(lnaddr, pay_req): else: raise PaymentFailure() lnaddr, pay_req = self.prepare_invoice(w2) - if test_hold_invoice: - payment_hash = lnaddr.paymenthash + payment_hash = lnaddr.paymenthash + if test_failure or test_hold_invoice: preimage = bytes.fromhex(w2.preimages.pop(payment_hash.hex())) - async def cb(payment_hash): - w2.save_preimage(payment_hash, preimage) - w2.register_callback_for_hold_invoice(payment_hash, cb, 60) + if test_hold_invoice: + async def cb(payment_hash): + if not test_failure: + w2.save_preimage(payment_hash, preimage) + else: + raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') + w2.register_callback_for_hold_invoice(payment_hash, cb) if test_bundle: lnaddr2, pay_req2 = self.prepare_invoice(w2) @@ -799,11 +804,16 @@ async def f(): await f() @needs_test_with_all_chacha20_implementations - async def test_simple_payment(self): + async def test_simple_payment_success(self): for test_trampoline in [False, True]: with self.assertRaises(PaymentDone): await self._test_simple_payment(test_trampoline=test_trampoline) + async def test_simple_payment_failure(self): + for test_trampoline in [False, True]: + with self.assertRaises(PaymentFailure): + await self._test_simple_payment(test_trampoline=test_trampoline, test_failure=True) + async def test_payment_bundle(self): for test_trampoline in [False, True]: with self.assertRaises(PaymentDone): @@ -814,11 +824,16 @@ async def test_payment_bundle_timeout(self): with self.assertRaises(PaymentFailure): await self._test_simple_payment(test_trampoline=test_trampoline, test_bundle=True, test_bundle_timeout=True) - async def test_simple_payment_with_hold_invoice(self): + async def test_simple_payment_success_with_hold_invoice(self): for test_trampoline in [False, True]: with self.assertRaises(PaymentDone): await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True) + async def test_simple_payment_failure_with_hold_invoice(self): + for test_trampoline in [False, True]: + with self.assertRaises(PaymentFailure): + await self._test_simple_payment(test_trampoline=test_trampoline, test_hold_invoice=True, test_failure=True) + @needs_test_with_all_chacha20_implementations async def test_payment_race(self): """Alice and Bob pay each other simultaneously. From 1ce50b9deed4886d67d5e34188827ea4549bb92c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 9 Aug 2023 14:15:49 +0200 Subject: [PATCH 1116/1143] submarine swaps: register callbacks on startup --- electrum/lnworker.py | 2 +- electrum/submarine_swaps.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 44170d4d4..652f01270 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -697,7 +697,6 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_key -> ReceivedMPPStatus - self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self) # detect inflight payments self.inflight_payments = set() # (not persisted) keys of invoices that are in PR_INFLIGHT state for payment_hash in self.get_payments(status='inflight').keys(): @@ -710,6 +709,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): # payment_hash -> callback, timeout: self.hold_invoice_callbacks = {} # type: Dict[bytes, Tuple[Callable[[bytes], None], int]] self.payment_bundles = [] # lists of hashes. todo:persist + self.swap_manager = SwapManager(wallet=self.wallet, lnworker=self) def has_deterministic_node_id(self) -> bool: diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 17ea412e2..58119e887 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -152,6 +152,8 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): for payment_hash, swap in self.swaps.items(): swap._payment_hash = bytes.fromhex(payment_hash) self._add_or_reindex_swap(swap) + if not swap.is_reverse and not swap.is_redeemed: + self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash for k, swap in self.swaps.items(): From 40f2087ac3cde164eb3e69d69c3775d9d970792f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Wed, 25 Jan 2023 17:52:38 +0100 Subject: [PATCH 1117/1143] Add option for support_large_channels. max_funding_sats is a config variable and defaults to the old value. --- electrum/gui/qml/qechannelopener.py | 2 +- electrum/gui/qt/new_channel_dialog.py | 6 +++--- electrum/gui/qt/utxo_list.py | 4 ++-- electrum/lnpeer.py | 3 +++ electrum/lnutil.py | 4 ---- electrum/lnworker.py | 10 +++++----- electrum/simple_config.py | 1 + 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index d05e7d94f..862dba431 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -141,7 +141,7 @@ def validateConnectString(self, connect_str): return False return True - # FIXME "max" button in amount_dialog should enforce LN_MAX_FUNDING_SAT + # FIXME "max" button in amount_dialog should enforce LIGHTNING_MAX_FUNDING_SAT @pyqtSlot() @pyqtSlot(bool) def openChannel(self, confirm_backup_conflict=False): diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py index e71f21fa9..349b17f96 100644 --- a/electrum/gui/qt/new_channel_dialog.py +++ b/electrum/gui/qt/new_channel_dialog.py @@ -4,7 +4,7 @@ from electrum.i18n import _ from electrum.transaction import PartialTxOutput, PartialTransaction -from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT +from electrum.lnutil import MIN_FUNDING_SAT from electrum.lnworker import hardcoded_trampoline_nodes from electrum import ecc from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates @@ -131,13 +131,13 @@ def spend_max(self): self.window.show_error(str(e)) return amount = tx.output_value() - amount = min(amount, LN_MAX_FUNDING_SAT) + amount = min(amount, self.config.LIGHTNING_MAX_FUNDING_SAT) self.amount_e.setAmount(amount) def run(self): if not self.exec_(): return - if self.max_button.isChecked() and self.amount_e.get_amount() < LN_MAX_FUNDING_SAT: + if self.max_button.isChecked() and self.amount_e.get_amount() < self.config.LIGHTNING_MAX_FUNDING_SAT: # if 'max' enabled and amount is strictly less than max allowed, # that means we have fewer coins than max allowed, and hence we can # spend all coins diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py index 7f8bdc3d3..3ec85a1c3 100644 --- a/electrum/gui/qt/utxo_list.py +++ b/electrum/gui/qt/utxo_list.py @@ -34,7 +34,7 @@ from electrum.i18n import _ from electrum.bitcoin import is_address from electrum.transaction import PartialTxInput, PartialTxOutput -from electrum.lnutil import LN_MAX_FUNDING_SAT, MIN_FUNDING_SAT +from electrum.lnutil import MIN_FUNDING_SAT from electrum.util import profiler from .util import ColorScheme, MONOSPACE_FONT, EnterButton @@ -243,7 +243,7 @@ def can_open_channel(self, coins): if self.wallet.lnworker is None: return False value = sum(x.value_sats() for x in coins) - return value >= MIN_FUNDING_SAT and value <= LN_MAX_FUNDING_SAT + return value >= MIN_FUNDING_SAT and value <= self.config.LIGHTNING_MAX_FUNDING_SAT def open_channel_with_coins(self, coins): # todo : use a single dialog in new flow diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index b3f058cc5..a4e2b5324 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -603,6 +603,9 @@ def close_and_cleanup(self): def is_shutdown_anysegwit(self): return self.features.supports(LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT) + def supports_large_channels(self): + return self.features.supports(LnFeatures.OPTION_SUPPORT_LARGE_CHANNELS) + def is_channel_type(self): return self.features.supports(LnFeatures.OPTION_CHANNEL_TYPE_OPT) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 7d3c37a42..ba900a106 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -49,7 +49,6 @@ COMMITMENT_TX_WEIGHT = 724 HTLC_OUTPUT_WEIGHT = 172 -LN_MAX_FUNDING_SAT = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 # dummy address for fee estimation of funding tx @@ -106,9 +105,6 @@ def validate_params(self, *, funding_sat: int) -> None: raise Exception(f"{conf_name}. invalid pubkey in channel config") if funding_sat < MIN_FUNDING_SAT: raise Exception(f"funding_sat too low: {funding_sat} sat < {MIN_FUNDING_SAT}") - # MUST set funding_satoshis to less than 2^24 satoshi - if funding_sat > LN_MAX_FUNDING_SAT: - raise Exception(f"funding_sat too high: {funding_sat} sat > {LN_MAX_FUNDING_SAT}") # MUST set push_msat to equal or less than 1000 * funding_satoshis if not (0 <= self.initial_msat <= 1000 * funding_sat): raise Exception(f"{conf_name}. insane initial_msat={self.initial_msat}. (funding_sat={funding_sat})") diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 652f01270..0d57eb27b 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -34,7 +34,6 @@ from .invoices import BaseInvoice from .util import NetworkRetryManager, JsonRPCClient, NotEnoughFunds from .util import EventListener, event_listener -from .lnutil import LN_MAX_FUNDING_SAT from .keystore import BIP32_KeyStore from .bitcoin import COIN from .bitcoin import opcodes, make_op_return, address_to_scripthash @@ -222,6 +221,7 @@ class ErrorAddingPeer(Exception): pass | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_SCID_ALIAS_OPT + | LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT ) LNGOSSIP_FEATURES = ( @@ -1156,7 +1156,7 @@ def suggest_funding_amount(self, amount_to_pay, coins): assert lightning_needed > 0 min_funding_sat = lightning_needed + (lightning_needed // 20) + 1000 # safety margin min_funding_sat = max(min_funding_sat, 100_000) # at least 1mBTC - if min_funding_sat > LN_MAX_FUNDING_SAT: + if min_funding_sat > self.config.LIGHTNING_MAX_FUNDING_SAT: return fee_est = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True) # to avoid NoDynamicFeeEstimates try: @@ -1165,7 +1165,7 @@ def suggest_funding_amount(self, amount_to_pay, coins): except NotEnoughFunds: return # if available, suggest twice that amount: - if 2 * min_funding_sat <= LN_MAX_FUNDING_SAT: + if 2 * min_funding_sat <= self.config.LIGHTNING_MAX_FUNDING_SAT: try: self.mktx_for_open_channel(coins=coins, funding_sat=2*min_funding_sat, node_id=bytes(32), fee_est=fee_est) funding_sat = 2 * min_funding_sat @@ -1175,8 +1175,8 @@ def suggest_funding_amount(self, amount_to_pay, coins): def open_channel(self, *, connect_str: str, funding_tx: PartialTransaction, funding_sat: int, push_amt_sat: int, password: str = None) -> Tuple[Channel, PartialTransaction]: - if funding_sat > LN_MAX_FUNDING_SAT: - raise Exception(_("Requested channel capacity is over protocol allowed maximum.")) + if funding_sat > self.config.LIGHTNING_MAX_FUNDING_SAT: + raise Exception(_("Requested channel capacity is over maximum.")) coro = self._open_channel_coroutine( connect_str=connect_str, funding_tx=funding_tx, funding_sat=funding_sat, push_sat=push_amt_sat, password=password) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 0ccfafb14..b4a183bfd 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -888,6 +888,7 @@ def get_swapserver_url(self): LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) + LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=pow(2, 24) - 1, type_=int) EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool) EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool) From e38605c10a316d225e5140cf2c9aa8c16e8ac94f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 9 Aug 2023 14:36:36 +0000 Subject: [PATCH 1118/1143] CLI: fix regression re handling "unknown command", re payment_identifiers ``` $ ./run_electrum sadasdasddsa Traceback (most recent call last): File "/home/user/wspace/electrum/./run_electrum", line 532, in main() File "/home/user/wspace/electrum/./run_electrum", line 398, in main if uri and not PaymentIdentifier(None, uri).is_valid(): File "/home/user/wspace/electrum/electrum/payment_identifier.py", line 136, in __init__ self.parse(text) File "/home/user/wspace/electrum/electrum/payment_identifier.py", line 265, in parse elif contact := self.contacts.by_name(text): AttributeError: 'NoneType' object has no attribute 'by_name' ``` --- electrum/payment_identifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py index 05aa3c3b3..556f2da69 100644 --- a/electrum/payment_identifier.py +++ b/electrum/payment_identifier.py @@ -104,7 +104,7 @@ class PaymentIdentifier(Logger): * lightning address """ - def __init__(self, wallet: 'Abstract_Wallet', text: str): + def __init__(self, wallet: Optional['Abstract_Wallet'], text: str): Logger.__init__(self) self._state = PaymentIdentifierState.EMPTY self.wallet = wallet @@ -262,7 +262,7 @@ def parse(self, text: str): self._type = PaymentIdentifierType.SPK self.spk = scriptpubkey self.set_state(PaymentIdentifierState.AVAILABLE) - elif contact := self.contacts.by_name(text): + elif self.contacts and (contact := self.contacts.by_name(text)): if contact['type'] == 'address': self._type = PaymentIdentifierType.BIP21 self.bip21 = { From 44ef5a35b7e24ad093ff3f70fd95f4a8b90fcff3 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 9 Aug 2023 14:43:49 +0000 Subject: [PATCH 1119/1143] CLI: fix regression re handling "unknown command", re locale if qt is not installed, e.g. on a server, was getting: ``` $ ./run_electrum sadasdasddsa Traceback (most recent call last): File "/home/user/wspace/electrum/./run_electrum", line 532, in main() File "/home/user/wspace/electrum/./run_electrum", line 383, in main lang = get_default_language(gui_name=gui_name) File "/home/user/wspace/electrum/electrum/gui/default_lang.py", line 23, in get_default_language from PyQt5.QtCore import QLocale ModuleNotFoundError: No module named 'PyQt5.QtCore' ``` --- run_electrum | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/run_electrum b/run_electrum index cbd17fd24..a458dde7f 100755 --- a/run_electrum +++ b/run_electrum @@ -376,12 +376,15 @@ def main(): # Note: it is ok to call set_language() again later, but note that any call only applies # to not-yet-evaluated strings. if cmdname == 'gui': - from electrum.gui.default_lang import get_default_language gui_name = config.GUI_NAME lang = config.LOCALIZATION_LANGUAGE if not lang: - lang = get_default_language(gui_name=gui_name) - _logger.info(f"get_default_language: detected default as {lang=!r}") + try: + from electrum.gui.default_lang import get_default_language + lang = get_default_language(gui_name=gui_name) + _logger.info(f"get_default_language: detected default as {lang=!r}") + except ImportError as e: + _logger.info(f"get_default_language: failed. got exc={e!r}") set_language(lang) if config.get('testnet'): From 2f2be1a6068888f51bc1c266c8dc2448b19125ed Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 9 Aug 2023 15:38:59 +0000 Subject: [PATCH 1120/1143] lnpeer: follow-up OPTION_SUPPORT_LARGE_CHANNEL follow-up 40f2087ac3cde164eb3e69d69c3775d9d970792f --- electrum/lnpeer.py | 9 +++++---- electrum/lnutil.py | 20 +++++++++++++++----- electrum/simple_config.py | 3 ++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index a4e2b5324..c8dbd8bfd 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -603,9 +603,6 @@ def close_and_cleanup(self): def is_shutdown_anysegwit(self): return self.features.supports(LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT) - def supports_large_channels(self): - return self.features.supports(LnFeatures.OPTION_SUPPORT_LARGE_CHANNELS) - def is_channel_type(self): return self.features.supports(LnFeatures.OPTION_CHANNEL_TYPE_OPT) @@ -662,7 +659,7 @@ def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwn current_htlc_signatures=b'', htlc_minimum_msat=1, ) - local_config.validate_params(funding_sat=funding_sat) + local_config.validate_params(funding_sat=funding_sat, config=self.network.config, peer_features=self.features) return local_config def temporarily_reserve_funding_tx_change_address(func): @@ -807,6 +804,8 @@ async def channel_establishment_flow( funding_sat=funding_sat, is_local_initiator=True, initial_feerate_per_kw=feerate, + config=self.network.config, + peer_features=self.features, ) # -> funding created @@ -967,6 +966,8 @@ async def on_open_channel(self, payload): funding_sat=funding_sat, is_local_initiator=False, initial_feerate_per_kw=feerate, + config=self.network.config, + peer_features=self.features, ) # note: we ignore payload['channel_flags'], which e.g. contains 'announce_channel'. diff --git a/electrum/lnutil.py b/electrum/lnutil.py index ba900a106..821b94537 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -38,6 +38,7 @@ from .lnchannel import Channel, AbstractChannel from .lnrouter import LNPaymentRoute from .lnonion import OnionRoutingFailure + from .simple_config import SimpleConfig _logger = get_logger(__name__) @@ -49,6 +50,7 @@ COMMITMENT_TX_WEIGHT = 724 HTLC_OUTPUT_WEIGHT = 172 +LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 # dummy address for fee estimation of funding tx @@ -92,7 +94,7 @@ class ChannelConfig(StoredObject): htlc_minimum_msat = attr.ib(type=int) # smallest value for INCOMING htlc upfront_shutdown_script = attr.ib(type=bytes, converter=hex_to_bytes) - def validate_params(self, *, funding_sat: int) -> None: + def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_features: 'LnFeatures') -> None: conf_name = type(self).__name__ for key in ( self.payment_basepoint, @@ -105,6 +107,12 @@ def validate_params(self, *, funding_sat: int) -> None: raise Exception(f"{conf_name}. invalid pubkey in channel config") if funding_sat < MIN_FUNDING_SAT: raise Exception(f"funding_sat too low: {funding_sat} sat < {MIN_FUNDING_SAT}") + if not peer_features.supports(LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT): + # MUST set funding_satoshis to less than 2^24 satoshi + if funding_sat > LN_MAX_FUNDING_SAT_LEGACY: + raise Exception(f"funding_sat too high: {funding_sat} sat > {LN_MAX_FUNDING_SAT_LEGACY} (legacy limit)") + if funding_sat > config.LIGHTNING_MAX_FUNDING_SAT: + raise Exception(f"funding_sat too high: {funding_sat} sat > {config.LIGHTNING_MAX_FUNDING_SAT} (config setting)") # MUST set push_msat to equal or less than 1000 * funding_satoshis if not (0 <= self.initial_msat <= 1000 * funding_sat): raise Exception(f"{conf_name}. insane initial_msat={self.initial_msat}. (funding_sat={funding_sat})") @@ -139,10 +147,12 @@ def cross_validate_params( funding_sat: int, is_local_initiator: bool, # whether we are the funder initial_feerate_per_kw: int, + config: 'SimpleConfig', + peer_features: 'LnFeatures', ) -> None: # first we validate the configs separately - local_config.validate_params(funding_sat=funding_sat) - remote_config.validate_params(funding_sat=funding_sat) + local_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features) + remote_config.validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features) # now do tests that need access to both configs if is_local_initiator: funder, fundee = LOCAL, REMOTE @@ -209,10 +219,10 @@ def from_seed(cls, **kwargs): kwargs['payment_basepoint'] = keypair_generator(LnKeyFamily.PAYMENT_BASE) return LocalConfig(**kwargs) - def validate_params(self, *, funding_sat: int) -> None: + def validate_params(self, *, funding_sat: int, config: 'SimpleConfig', peer_features: 'LnFeatures') -> None: conf_name = type(self).__name__ # run base checks regardless whether LOCAL/REMOTE config - super().validate_params(funding_sat=funding_sat) + super().validate_params(funding_sat=funding_sat, config=config, peer_features=peer_features) # run some stricter checks on LOCAL config (make sure we ourselves do the sane thing, # even if we are lenient with REMOTE for compatibility reasons) HTLC_MINIMUM_MSAT_MIN = 1 diff --git a/electrum/simple_config.py b/electrum/simple_config.py index b4a183bfd..4c3df5f1a 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -18,6 +18,7 @@ from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT from .util import format_satoshis, format_fee_satoshis, os_chmod from .util import user_dir, make_dir, NoDynamicFeeEstimates, quantize_feerate +from .lnutil import LN_MAX_FUNDING_SAT_LEGACY from .i18n import _ from .logging import get_logger, Logger @@ -888,7 +889,7 @@ def get_swapserver_url(self): LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) - LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=pow(2, 24) - 1, type_=int) + LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int) EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool) EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool) From c5493a354dec523b1d06437b1272b07b722df1bd Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 9 Aug 2023 16:00:09 +0000 Subject: [PATCH 1121/1143] qt PayToEdit: always add context menu items fixes regression from https://github.com/spesmilo/electrum/pull/8462 --- electrum/gui/qt/paytoedit.py | 1 + electrum/gui/qt/util.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 0ffe02314..371d7fc17 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -167,6 +167,7 @@ def line_edit_changed(): ) self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self) + self.line_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.line_edit, self) self.edit_timer = QTimer(self) self.edit_timer.setSingleShot(True) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 50bdacaa0..ac4920e48 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -15,7 +15,8 @@ from PyQt5 import QtWidgets, QtCore from PyQt5.QtGui import (QFont, QColor, QCursor, QPixmap, QStandardItem, QImage, - QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent, QMouseEvent) + QPalette, QIcon, QFontMetrics, QShowEvent, QPainter, QHelpEvent, QMouseEvent, + QContextMenuEvent) from PyQt5.QtCore import (Qt, QPersistentModelIndex, QModelIndex, pyqtSignal, QCoreApplication, QItemSelectionModel, QThread, QSortFilterProxyModel, QSize, QLocale, QAbstractItemModel, @@ -38,6 +39,8 @@ if TYPE_CHECKING: from .main_window import ElectrumWindow from .installwizard import InstallWizard + from .paytoedit import PayToEdit + from electrum.simple_config import SimpleConfig @@ -529,7 +532,7 @@ def get_iconname_camera() -> str: return "camera_white.png" if ColorScheme.dark_scheme else "camera_dark.png" -def editor_contextMenuEvent(self, p, e): +def editor_contextMenuEvent(self, p: 'PayToEdit', e: 'QContextMenuEvent') -> None: m = self.createStandardContextMenu() m.addSeparator() m.addAction(read_QIcon(get_iconname_camera()), _("Read QR code with camera"), p.on_qr_from_camera_input_btn) From ff547e3dcf9b698d1342eb3649534a54e7b75b34 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 08:11:37 +0200 Subject: [PATCH 1122/1143] swapserver: use a single config variable for swapserver_url; testnet and regtest have their own config files --- electrum/simple_config.py | 21 ++++++++++----------- electrum/submarine_swaps.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4c3df5f1a..448fef906 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -47,6 +47,15 @@ FEE_RATIO_HIGH_WARNING = 0.05 # warn user if fee/amount for on-chain tx is higher than this +def default_swapserver_url(): + if constants.net == constants.BitcoinMainnet: + return 'https://swaps.electrum.org/api' + elif constants.net == constants.BitcoinTestnet: + return 'https://swaps.electrum.org/testnet' + else: + return 'http://localhost:5455/api' + + _logger = get_logger(__name__) @@ -845,14 +854,6 @@ def __setattr__(self, name, value): f"Either use config.cv.{name}.set() or assign to config.{name} instead.") return CVLookupHelper() - def get_swapserver_url(self): - if constants.net == constants.BitcoinMainnet: - return self.SWAPSERVER_URL_MAINNET - elif constants.net == constants.BitcoinTestnet: - return self.SWAPSERVER_URL_TESTNET - else: - return self.SWAPSERVER_URL_REGTEST - # config variables -----> NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool) @@ -966,9 +967,7 @@ def get_swapserver_url(self): SSL_CERTFILE_PATH = ConfigVar('ssl_certfile', default='', type_=str) SSL_KEYFILE_PATH = ConfigVar('ssl_keyfile', default='', type_=str) # submarine swap server - SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str) - SWAPSERVER_URL_TESTNET = ConfigVar('swapserver_url_testnet', default='https://swaps.electrum.org/testnet', type_=str) - SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='http://localhost:5455/api', type_=str) + SWAPSERVER_URL = ConfigVar('swapserver_url', default=default_swapserver_url(), type_=str) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 58119e887..071a8491b 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -160,7 +160,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): if swap.prepay_hash is not None: self.prepayments[swap.prepay_hash] = bytes.fromhex(k) # api url - self.api_url = wallet.config.get_swapserver_url() + self.api_url = wallet.config.SWAPSERVER_URL # init default min & max self.init_min_max_values() From 11af4e47a8c4d2bec1c0672c28b555f449e2665a Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 09:02:42 +0200 Subject: [PATCH 1123/1143] follow-up ff547e3dcf9b698d1342eb3649534a54e7b75b34 --- electrum/simple_config.py | 19 ++++++++++--------- electrum/submarine_swaps.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 448fef906..06285f5a8 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -47,14 +47,6 @@ FEE_RATIO_HIGH_WARNING = 0.05 # warn user if fee/amount for on-chain tx is higher than this -def default_swapserver_url(): - if constants.net == constants.BitcoinMainnet: - return 'https://swaps.electrum.org/api' - elif constants.net == constants.BitcoinTestnet: - return 'https://swaps.electrum.org/testnet' - else: - return 'http://localhost:5455/api' - _logger = get_logger(__name__) @@ -854,6 +846,15 @@ def __setattr__(self, name, value): f"Either use config.cv.{name}.set() or assign to config.{name} instead.") return CVLookupHelper() + def get_swapserver_url(self): + if constants.net == constants.BitcoinMainnet: + default = 'https://swaps.electrum.org/api' + elif constants.net == constants.BitcoinTestnet: + default = 'https://swaps.electrum.org/testnet' + else: + default = 'http://localhost:5455/api' + return self.SWAPSERVER_URL or default + # config variables -----> NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool) @@ -967,7 +968,7 @@ def __setattr__(self, name, value): SSL_CERTFILE_PATH = ConfigVar('ssl_certfile', default='', type_=str) SSL_KEYFILE_PATH = ConfigVar('ssl_keyfile', default='', type_=str) # submarine swap server - SWAPSERVER_URL = ConfigVar('swapserver_url', default=default_swapserver_url(), type_=str) + SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 071a8491b..58119e887 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -160,7 +160,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): if swap.prepay_hash is not None: self.prepayments[swap.prepay_hash] = bytes.fromhex(k) # api url - self.api_url = wallet.config.SWAPSERVER_URL + self.api_url = wallet.config.get_swapserver_url() # init default min & max self.init_min_max_values() From fd10ae3a3b0c52571755d251b3e087c7aa950050 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 28 Jul 2023 09:28:45 +0200 Subject: [PATCH 1124/1143] New flow for submarine swaps: - client requests payment_hash from the server - client sends an invoice with that hash - client waits to receive HTLCs, then broadcasts funding tx This means that we now use same script for normal and reverse swaps. The new flow is enabled by setting option LIGHTNING_SWAP_HTLC_FIRST in the client. The old protocol is still supported server-side. --- electrum/plugins/swapserver/server.py | 51 ++- electrum/plugins/swapserver/swapserver.py | 1 - electrum/simple_config.py | 1 + electrum/submarine_swaps.py | 396 ++++++++++++++-------- 4 files changed, 305 insertions(+), 144 deletions(-) diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 0fb1ca724..305f26c78 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -5,11 +5,10 @@ from aiohttp import web from aiorpcx import NetAddress - from electrum.util import log_exceptions, ignore_exceptions from electrum.logging import Logger from electrum.util import EventListener - +from electrum.lnaddr import lndecode class SwapServer(Logger, EventListener): """ @@ -24,6 +23,7 @@ def __init__(self, config, wallet): Logger.__init__(self) self.config = config self.wallet = wallet + self.sm = self.wallet.lnworker.swap_manager self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS) self.register_callbacks() # eventlistener @@ -36,6 +36,8 @@ async def run(self): app = web.Application() app.add_routes([web.get('/api/getpairs', self.get_pairs)]) app.add_routes([web.post('/api/createswap', self.create_swap)]) + app.add_routes([web.post('/api/createnormalswap', self.create_normal_swap)]) + app.add_routes([web.post('/api/addswapinvoice', self.add_swap_invoice)]) runner = web.AppRunner(app) await runner.setup() @@ -44,7 +46,7 @@ async def run(self): self.logger.info(f"now running and listening. addr={self.addr}") async def get_pairs(self, r): - sm = self.wallet.lnworker.swap_manager + sm = self.sm sm.init_pairs() pairs = { "info": [], @@ -84,9 +86,36 @@ async def get_pairs(self, r): } return web.json_response(pairs) + async def add_swap_invoice(self, r): + request = await r.json() + invoice = request['invoice'] + self.sm.add_invoice(invoice, pay_now=True) + return web.json_response({}) + + async def create_normal_swap(self, r): + # normal for client, reverse for server + request = await r.json() + lightning_amount_sat = request['invoiceAmount'] + their_pubkey = bytes.fromhex(request['refundPublicKey']) + assert len(their_pubkey) == 33 + swap = self.sm.create_reverse_swap( + payment_hash=None, + lightning_amount_sat=lightning_amount_sat, + their_pubkey=their_pubkey + ) + response = { + "id": swap.payment_hash.hex(), + 'preimageHash': swap.payment_hash.hex(), + "acceptZeroConf": False, + "expectedAmount": swap.onchain_amount, + "timeoutBlockHeight": swap.locktime, + "address": swap.lockup_address, + "redeemScript": swap.redeem_script.hex(), + } + return web.json_response(response) + async def create_swap(self, r): - sm = self.wallet.lnworker.swap_manager - sm.init_pairs() + self.sm.init_pairs() request = await r.json() req_type = request['type'] assert request['pairId'] == 'BTC/BTC' @@ -96,7 +125,7 @@ async def create_swap(self, r): their_pubkey=bytes.fromhex(request['claimPublicKey']) assert len(payment_hash) == 32 assert len(their_pubkey) == 33 - swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap( + swap, invoice, prepay_invoice = self.sm.create_normal_swap( lightning_amount_sat=lightning_amount_sat, payment_hash=payment_hash, their_pubkey=their_pubkey @@ -111,13 +140,19 @@ async def create_swap(self, r): "onchainAmount": swap.onchain_amount, } elif req_type == 'submarine': + # old protocol their_invoice=request['invoice'] their_pubkey=bytes.fromhex(request['refundPublicKey']) assert len(their_pubkey) == 33 - swap, payment_hash, invoice, prepay_invoice = sm.add_server_swap( - invoice=their_invoice, + lnaddr = lndecode(their_invoice) + payment_hash = lnaddr.paymenthash + lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int + swap = self.sm.create_reverse_swap( + lightning_amount_sat=lightning_amount_sat, + payment_hash=payment_hash, their_pubkey=their_pubkey ) + self.sm.add_invoice(their_invoice, pay_now=False) response = { "id": payment_hash.hex(), "acceptZeroConf": False, diff --git a/electrum/plugins/swapserver/swapserver.py b/electrum/plugins/swapserver/swapserver.py index a4b785347..9a35e9714 100644 --- a/electrum/plugins/swapserver/swapserver.py +++ b/electrum/plugins/swapserver/swapserver.py @@ -55,7 +55,6 @@ def daemon_wallet_loaded(self, daemon: 'Daemon', wallet: 'Abstract_Wallet'): self.server = SwapServer(self.config, wallet) sm = wallet.lnworker.swap_manager for coro in [ - sm.pay_pending_invoices(), # FIXME this method can raise, which is not properly handled...? self.server.run(), ]: asyncio.run_coroutine_threadsafe(daemon.taskgroup.spawn(coro), daemon.asyncio_loop) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 06285f5a8..6cbf67810 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -890,6 +890,7 @@ def get_swapserver_url(self): LIGHTNING_USE_GOSSIP = ConfigVar('use_gossip', default=False, type_=bool) LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) + LIGHTNING_SWAP_HTLC_FIRST = ConfigVar('swap_htlc_first', default=False, type_=bool) LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 58119e887..c23e017a4 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -24,6 +24,10 @@ from .address_synchronizer import TX_HEIGHT_LOCAL from .i18n import _ +from .bitcoin import construct_script +from .crypto import ripemd +from .invoices import Invoice + if TYPE_CHECKING: from .network import Network from .wallet import Abstract_Wallet @@ -81,6 +85,40 @@ opcodes.OP_CHECKSIG ] +def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, *, refund_pubkey=None, claim_pubkey=None): + redeem_script = bytes.fromhex(redeem_script) + parsed_script = [x for x in script_GetOp(redeem_script)] + if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP): + raise Exception("rswap check failed: scriptcode does not match template") + if script_to_p2wsh(redeem_script.hex()) != lockup_address: + raise Exception("rswap check failed: inconsistent scriptcode and address") + if ripemd(payment_hash) != parsed_script[5][1]: + raise Exception("rswap check failed: our preimage not in script") + if claim_pubkey and claim_pubkey != parsed_script[7][1]: + raise Exception("rswap check failed: our pubkey not in script") + if refund_pubkey and refund_pubkey != parsed_script[13][1]: + raise Exception("rswap check failed: our pubkey not in script") + if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'): + raise Exception("rswap check failed: inconsistent locktime and script") + return parsed_script[7][1], parsed_script[13][1] + +def check_normal_redeem_script(redeem_script, lockup_address, payment_hash, locktime, *, refund_pubkey=None, claim_pubkey=None): + redeem_script = bytes.fromhex(redeem_script) + parsed_script = [x for x in script_GetOp(redeem_script)] + if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP): + raise Exception("fswap check failed: scriptcode does not match template") + if script_to_p2wsh(redeem_script.hex()) != lockup_address: + raise Exception("fswap check failed: inconsistent scriptcode and address") + if ripemd(payment_hash) != parsed_script[1][1]: + raise Exception("fswap check failed: our preimage not in script") + if claim_pubkey and claim_pubkey != parsed_script[4][1]: + raise Exception("fswap check failed: our pubkey not in script") + if refund_pubkey and refund_pubkey != parsed_script[9][1]: + raise Exception("fswap check failed: our pubkey not in script") + if locktime != int.from_bytes(parsed_script[6][1], byteorder='little'): + raise Exception("fswap check failed: inconsistent locktime and script") + return parsed_script[4][1], parsed_script[9][1] + class SwapServerError(Exception): def __str__(self): @@ -174,9 +212,11 @@ def start_network(self, *, network: 'Network', lnwatcher: 'LNWalletWatcher'): if swap.is_redeemed: continue self.add_lnwatcher_callback(swap) + coro = self.pay_pending_invoices() + asyncio.run_coroutine_threadsafe(network.taskgroup.spawn(coro), network.asyncio_loop) async def pay_pending_invoices(self): - # for server + # FIXME this method can raise, which is not properly handled...? self.invoices_to_pay = set() while True: await asyncio.sleep(1) @@ -302,85 +342,164 @@ def add_lnwatcher_callback(self, swap: SwapData) -> None: self.lnwatcher.add_callback(swap.lockup_address, callback) async def hold_invoice_callback(self, payment_hash): + # note: this assumes the keystore is not encrypted key = payment_hash.hex() if key in self.swaps: swap = self.swaps[key] if swap.funding_txid is None: - await self.start_normal_swap(swap, None, None) - - def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoice=None, their_pubkey=None): - from .bitcoin import construct_script - from .crypto import ripemd - from .lnaddr import lndecode - from .invoices import Invoice + await self.broadcast_funding_tx(swap, None, None) + def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash=None, their_pubkey=None): + """ server method """ locktime = self.network.get_local_height() + 140 - privkey = os.urandom(32) - our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) - is_reverse_for_server = (invoice is not None) - if is_reverse_for_server: - # client is doing a normal swap - lnaddr = lndecode(invoice) - payment_hash = lnaddr.paymenthash - lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int - onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) - redeem_script = construct_script( - WITNESS_TEMPLATE_SWAP, - {1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey} - ) - self.wallet.save_invoice(Invoice.from_bech32(invoice)) - prepay_invoice = None - prepay_hash = None - else: - onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) + our_privkey = os.urandom(32) + our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True) + onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) # what the client is going to receive + redeem_script = construct_script( + WITNESS_TEMPLATE_REVERSE_SWAP, + {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} + ) + return self.add_normal_swap( + redeem_script=redeem_script, + locktime=locktime, + onchain_amount_sat=onchain_amount_sat, + lightning_amount_sat=lightning_amount_sat, + payment_hash=payment_hash, + our_privkey=our_privkey, + their_pubkey=their_pubkey, + invoice=None, + prepay=True, + ) + + def add_normal_swap(self, *, redeem_script=None, locktime=None, onchain_amount_sat=None, lightning_amount_sat=None, payment_hash=None, our_privkey=None, their_pubkey=None, invoice=None, prepay=None): + """ if invoice is None, create a hold invoice """ + if prepay: prepay_amount_sat = self.get_claim_fee() * 2 - main_amount_sat = lightning_amount_sat - prepay_amount_sat - lnaddr, invoice = self.lnworker.get_bolt11_invoice( + invoice_amount_sat = lightning_amount_sat - prepay_amount_sat + else: + invoice_amount_sat = lightning_amount_sat + + if not invoice: + _, invoice = self.lnworker.get_bolt11_invoice( payment_hash=payment_hash, - amount_msat=main_amount_sat * 1000, + amount_msat=invoice_amount_sat * 1000, message='Submarine swap', expiry=3600 * 24, fallback_address=None, channels=None, ) # add payment info to lnworker - self.lnworker.add_payment_info_for_hold_invoice(payment_hash, main_amount_sat) + self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) + + if prepay: prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) _, prepay_invoice = self.lnworker.get_bolt11_invoice( payment_hash=prepay_hash, amount_msat=prepay_amount_sat * 1000, - message='prepay', + message='Submarine swap mining fees', expiry=3600 * 24, fallback_address=None, channels=None, ) self.lnworker.bundle_payments([payment_hash, prepay_hash]) + self.prepayments[prepay_hash] = payment_hash + else: + prepay_invoice = None + prepay_hash = None + + lockup_address = script_to_p2wsh(redeem_script) + receive_address = self.wallet.get_receiving_address() + swap = SwapData( + redeem_script = bytes.fromhex(redeem_script), + locktime = locktime, + privkey = our_privkey, + preimage = None, + prepay_hash = prepay_hash, + lockup_address = lockup_address, + onchain_amount = onchain_amount_sat, + receive_address = receive_address, + lightning_amount = lightning_amount_sat, + is_reverse = False, + is_redeemed = False, + funding_txid = None, + spending_txid = None, + ) + swap._payment_hash = payment_hash + self._add_or_reindex_swap(swap) + self.add_lnwatcher_callback(swap) + return swap, invoice, prepay_invoice + + def create_reverse_swap(self, *, lightning_amount_sat=None, payment_hash=None, their_pubkey=None): + """ server method. payment_hash is not None for old clients """ + locktime = self.network.get_local_height() + 140 + privkey = os.urandom(32) + our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) + onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) + # + if payment_hash is None: + preimage = os.urandom(32) + assert lightning_amount_sat is not None + payment_hash = sha256(preimage) redeem_script = construct_script( WITNESS_TEMPLATE_REVERSE_SWAP, - {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} + {1:32, 5:ripemd(payment_hash), 7:our_pubkey, 10:locktime, 13:their_pubkey} + ) + else: + # old client + preimage = None + redeem_script = construct_script( + WITNESS_TEMPLATE_SWAP, + {1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey} ) + swap = self.add_reverse_swap( + redeem_script=redeem_script, + locktime=locktime, + privkey=privkey, + preimage=preimage, + payment_hash=payment_hash, + prepay_hash=None, + onchain_amount_sat=onchain_amount_sat, + lightning_amount_sat=lightning_amount_sat) + return swap + + def add_reverse_swap(self, *, redeem_script=None, locktime=None, privkey=None, lightning_amount_sat=None, onchain_amount_sat=None, preimage=None, payment_hash=None, prepay_hash=None): lockup_address = script_to_p2wsh(redeem_script) receive_address = self.wallet.get_receiving_address() swap = SwapData( redeem_script = bytes.fromhex(redeem_script), locktime = locktime, privkey = privkey, - preimage = None, + preimage = preimage, prepay_hash = prepay_hash, lockup_address = lockup_address, onchain_amount = onchain_amount_sat, receive_address = receive_address, lightning_amount = lightning_amount_sat, - is_reverse = is_reverse_for_server, + is_reverse = True, is_redeemed = False, funding_txid = None, spending_txid = None, ) + if prepay_hash: + self.prepayments[prepay_hash] = payment_hash swap._payment_hash = payment_hash self._add_or_reindex_swap(swap) self.add_lnwatcher_callback(swap) - return swap, payment_hash, invoice, prepay_invoice + return swap + + def add_invoice(self, invoice, pay_now=False): + invoice = Invoice.from_bech32(invoice) + key = invoice.rhash + payment_hash = bytes.fromhex(key) + assert key in self.swaps + self.wallet.save_invoice(invoice) + if pay_now: + # check that we have the preimage + swap = self.get_swap(payment_hash) + assert sha256(swap.preimage) == payment_hash + assert swap.spending_txid is None + self.invoices_to_pay.add(key) async def normal_swap( self, @@ -397,54 +516,73 @@ async def normal_swap( - User creates on-chain output locked to RHASH. - Server pays LN invoice. User reveals preimage. - Server spends the on-chain output using preimage. + + New flow: + - user requests swap + - server creates preimage, sends RHASH to user + - user creates hold invoice, sends it to server + """ assert self.network assert self.lnwatcher - privkey = os.urandom(32) - pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) amount_msat = lightning_amount_sat * 1000 - payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat) - lnaddr, invoice = self.lnworker.get_bolt11_invoice( - payment_hash=payment_hash, - amount_msat=amount_msat, - message='swap', - expiry=3600 * 24, - fallback_address=None, - channels=channels, - ) - preimage = self.lnworker.get_preimage(payment_hash) - request_data = { - "type": "submarine", - "pairId": "BTC/BTC", - "orderSide": "sell", - "invoice": invoice, - "refundPublicKey": pubkey.hex() - } - response = await self.network.async_send_http_on_proxy( - 'post', - self.api_url + '/createswap', - json=request_data, - timeout=30) - data = json.loads(response) - response_id = data["id"] + refund_privkey = os.urandom(32) + refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) + + if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: + self.logger.info('requesting preimage hash for swap') + request_data = { + "invoiceAmount": lightning_amount_sat, + "refundPublicKey": refund_pubkey.hex() + } + response = await self.network.async_send_http_on_proxy( + 'post', + self.api_url + '/createnormalswap', + json=request_data, + timeout=30) + data = json.loads(response) + payment_hash = bytes.fromhex(data["preimageHash"]) + preimage = None + invoice = None + else: + # create invoice, send it to server + payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat) + preimage = self.lnworker.get_preimage(payment_hash) + _, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=payment_hash, + amount_msat=amount_msat, + message='swap', + expiry=3600 * 24, + fallback_address=None, + channels=channels, + ) + request_data = { + "type": "submarine", + "pairId": "BTC/BTC", + "orderSide": "sell", + "invoice": invoice, + "refundPublicKey": refund_pubkey.hex() + } + response = await self.network.async_send_http_on_proxy( + 'post', + self.api_url + '/createswap', + json=request_data, + timeout=30) + + data = json.loads(response) + response_id = data["id"] + zeroconf = data["acceptZeroConf"] onchain_amount = data["expectedAmount"] locktime = data["timeoutBlockHeight"] lockup_address = data["address"] redeem_script = data["redeemScript"] # verify redeem_script is built with our pubkey and preimage - redeem_script = bytes.fromhex(redeem_script) - parsed_script = [x for x in script_GetOp(redeem_script)] - if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP): - raise Exception("fswap check failed: scriptcode does not match template") - if script_to_p2wsh(redeem_script.hex()) != lockup_address: - raise Exception("fswap check failed: inconsistent scriptcode and address") - if hash_160(preimage) != parsed_script[1][1]: - raise Exception("fswap check failed: our preimage not in script") - if pubkey != parsed_script[9][1]: - raise Exception("fswap check failed: our pubkey not in script") - if locktime != int.from_bytes(parsed_script[6][1], byteorder='little'): - raise Exception("fswap check failed: inconsistent locktime and script") + if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: + claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey) + else: + claim_pubkey, _ = check_normal_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey) + # check that onchain_amount is not more than what we estimated if onchain_amount > expected_onchain_amount_sat: raise Exception(f"fswap check failed: onchain_amount is more than what we estimated: " @@ -452,30 +590,41 @@ async def normal_swap( # verify that they are not locking up funds for more than a day if locktime - self.network.get_local_height() >= 144: raise Exception("fswap check failed: locktime too far in future") - # save swap data in wallet in case we need a refund - receive_address = self.wallet.get_receiving_address() - swap = SwapData( - redeem_script = redeem_script, - locktime = locktime, - privkey = privkey, - preimage = preimage, - prepay_hash = None, - lockup_address = lockup_address, - onchain_amount = onchain_amount, - receive_address = receive_address, - lightning_amount = lightning_amount_sat, - is_reverse = False, - is_redeemed = False, - funding_txid = None, - spending_txid = None, - ) - swap._payment_hash = payment_hash - self._add_or_reindex_swap(swap) - self.add_lnwatcher_callback(swap) - return await self.start_normal_swap(swap, tx, password) + + swap, invoice, _ = self.add_normal_swap( + redeem_script=redeem_script, + locktime=locktime, + lightning_amount_sat=lightning_amount_sat, + onchain_amount_sat=onchain_amount, + payment_hash=payment_hash, + our_privkey=refund_privkey, + their_pubkey=claim_pubkey, + invoice=invoice, + prepay=False) + + if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: + # send invoice to server and wait for htlcs + request_data = { + "preimageHash": payment_hash.hex(), + "invoice": invoice, + "refundPublicKey": refund_pubkey.hex(), + } + response = await self.network.async_send_http_on_proxy( + 'post', + self.api_url + '/addswapinvoice', + json=request_data, + timeout=30) + data = json.loads(response) + # wait for funding tx + while swap.funding_txid is None: + await asyncio.sleep(0.1) + else: + # broadcast funding tx right away + await self.broadcast_funding_tx(swap, tx, password) + return swap.funding_txid @log_exceptions - async def start_normal_swap(self, swap, tx, password): + async def broadcast_funding_tx(self, swap, tx, password): # create funding tx # note: rbf must not decrease payment # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output @@ -488,9 +637,9 @@ async def start_normal_swap(self, swap, tx, password): tx.add_outputs([funding_output]) tx.set_rbf(True) self.wallet.sign_transaction(tx, password) + await self.network.broadcast_transaction(tx) swap.funding_txid = tx.txid() - return swap.funding_txid async def reverse_swap( self, @@ -513,16 +662,16 @@ async def reverse_swap( assert self.network assert self.lnwatcher privkey = os.urandom(32) - pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) + our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) preimage = os.urandom(32) - preimage_hash = sha256(preimage) + payment_hash = sha256(preimage) request_data = { "type": "reversesubmarine", "pairId": "BTC/BTC", "orderSide": "buy", "invoiceAmount": lightning_amount_sat, - "preimageHash": preimage_hash.hex(), - "claimPublicKey": pubkey.hex() + "preimageHash": payment_hash.hex(), + "claimPublicKey": our_pubkey.hex() } response = await self.network.async_send_http_on_proxy( 'post', @@ -538,18 +687,7 @@ async def reverse_swap( onchain_amount = data["onchainAmount"] response_id = data['id'] # verify redeem_script is built with our pubkey and preimage - redeem_script = bytes.fromhex(redeem_script) - parsed_script = [x for x in script_GetOp(redeem_script)] - if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP): - raise Exception("rswap check failed: scriptcode does not match template") - if script_to_p2wsh(redeem_script.hex()) != lockup_address: - raise Exception("rswap check failed: inconsistent scriptcode and address") - if hash_160(preimage) != parsed_script[5][1]: - raise Exception("rswap check failed: our preimage not in script") - if pubkey != parsed_script[7][1]: - raise Exception("rswap check failed: our pubkey not in script") - if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'): - raise Exception("rswap check failed: inconsistent locktime and script") + check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=None, claim_pubkey=our_pubkey) # check that the onchain amount is what we expected if onchain_amount < expected_onchain_amount_sat: raise Exception(f"rswap check failed: onchain_amount is less than what we expected: " @@ -557,10 +695,10 @@ async def reverse_swap( # verify that we will have enough time to get our tx confirmed if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA: raise Exception("rswap check failed: locktime too close") - # verify invoice preimage_hash + # verify invoice payment_hash lnaddr = self.lnworker._check_invoice(invoice) invoice_amount = int(lnaddr.get_amount_sat()) - if lnaddr.paymenthash != preimage_hash: + if lnaddr.paymenthash != payment_hash: raise Exception("rswap check failed: inconsistent RHASH and invoice") # check that the lightning amount is what we requested if fee_invoice: @@ -573,29 +711,17 @@ async def reverse_swap( raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) " f"not what we requested ({lightning_amount_sat})") # save swap data to wallet file - receive_address = self.wallet.get_receiving_address() - swap = SwapData( - redeem_script = redeem_script, - locktime = locktime, - privkey = privkey, - preimage = preimage, - prepay_hash = prepay_hash, - lockup_address = lockup_address, - onchain_amount = onchain_amount, - receive_address = receive_address, - lightning_amount = lightning_amount_sat, - is_reverse = True, - is_redeemed = False, - funding_txid = None, - spending_txid = None, - ) - swap._payment_hash = preimage_hash - self._add_or_reindex_swap(swap) - # add callback to lnwatcher - self.add_lnwatcher_callback(swap) + swap = self.add_reverse_swap( + redeem_script=redeem_script, + locktime=locktime, + privkey=privkey, + preimage=preimage, + payment_hash=payment_hash, + prepay_hash=prepay_hash, + onchain_amount_sat=onchain_amount, + lightning_amount_sat=lightning_amount_sat) # initiate fee payment. if fee_invoice: - self.prepayments[prepay_hash] = preimage_hash asyncio.ensure_future(self.lnworker.pay_invoice(fee_invoice, attempts=10)) # we return if we detect funding async def wait_for_funding(swap): From 88883d762c0f6f91428611084323332f5546fa0c Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 10:29:32 +0200 Subject: [PATCH 1125/1143] swapserver: remove /api from url --- electrum/plugins/swapserver/server.py | 8 ++++---- electrum/simple_config.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 305f26c78..ba15b4cb6 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -34,10 +34,10 @@ def __init__(self, config, wallet): @log_exceptions async def run(self): app = web.Application() - app.add_routes([web.get('/api/getpairs', self.get_pairs)]) - app.add_routes([web.post('/api/createswap', self.create_swap)]) - app.add_routes([web.post('/api/createnormalswap', self.create_normal_swap)]) - app.add_routes([web.post('/api/addswapinvoice', self.add_swap_invoice)]) + app.add_routes([web.get('/getpairs', self.get_pairs)]) + app.add_routes([web.post('/createswap', self.create_swap)]) + app.add_routes([web.post('/createnormalswap', self.create_normal_swap)]) + app.add_routes([web.post('/addswapinvoice', self.add_swap_invoice)]) runner = web.AppRunner(app) await runner.setup() diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 6cbf67810..e0596d854 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -852,7 +852,7 @@ def get_swapserver_url(self): elif constants.net == constants.BitcoinTestnet: default = 'https://swaps.electrum.org/testnet' else: - default = 'http://localhost:5455/api' + default = 'http://localhost:5455' return self.SWAPSERVER_URL or default # config variables -----> From e0c1fbfe777813e066041bde148fbca84fccccb4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 13:44:02 +0200 Subject: [PATCH 1126/1143] normal swaps: use different callbacks for server and client - client creates tx immediately - server defers tx creation, and does not use encrypted storage --- electrum/submarine_swaps.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index c23e017a4..2e6f8c014 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -347,7 +347,8 @@ async def hold_invoice_callback(self, payment_hash): if key in self.swaps: swap = self.swaps[key] if swap.funding_txid is None: - await self.broadcast_funding_tx(swap, None, None) + tx = self.create_funding_tx(swap, None, None) + await self.broadcast_funding_tx(swap, tx) def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash=None, their_pubkey=None): """ server method """ @@ -359,7 +360,7 @@ def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash=None, th WITNESS_TEMPLATE_REVERSE_SWAP, {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} ) - return self.add_normal_swap( + swap, invoice, prepay_invoice = self.add_normal_swap( redeem_script=redeem_script, locktime=locktime, onchain_amount_sat=onchain_amount_sat, @@ -370,6 +371,8 @@ def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash=None, th invoice=None, prepay=True, ) + self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) + return swap, invoice, prepay_invoice def add_normal_swap(self, *, redeem_script=None, locktime=None, onchain_amount_sat=None, lightning_amount_sat=None, payment_hash=None, our_privkey=None, their_pubkey=None, invoice=None, prepay=None): """ if invoice is None, create a hold invoice """ @@ -390,7 +393,6 @@ def add_normal_swap(self, *, redeem_script=None, locktime=None, onchain_amount_s ) # add payment info to lnworker self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) - self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) if prepay: prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) @@ -602,6 +604,7 @@ async def normal_swap( invoice=invoice, prepay=False) + tx = self.create_funding_tx(swap, tx, password) if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: # send invoice to server and wait for htlcs request_data = { @@ -615,16 +618,18 @@ async def normal_swap( json=request_data, timeout=30) data = json.loads(response) + async def callback(payment_hash): + await self.broadcast_funding_tx(swap, tx) + self.lnworker.register_callback_for_hold_invoice(payment_hash, callback) # wait for funding tx while swap.funding_txid is None: await asyncio.sleep(0.1) else: # broadcast funding tx right away - await self.broadcast_funding_tx(swap, tx, password) + await self.broadcast_funding_tx(swap, tx) return swap.funding_txid - @log_exceptions - async def broadcast_funding_tx(self, swap, tx, password): + def create_funding_tx(self, swap, tx, password): # create funding tx # note: rbf must not decrease payment # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output @@ -637,7 +642,10 @@ async def broadcast_funding_tx(self, swap, tx, password): tx.add_outputs([funding_output]) tx.set_rbf(True) self.wallet.sign_transaction(tx, password) + return tx + @log_exceptions + async def broadcast_funding_tx(self, swap, tx): await self.network.broadcast_transaction(tx) swap.funding_txid = tx.txid() From 8931420938c9fa64e5f7775c69612b7a818001ca Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 14:22:43 +0000 Subject: [PATCH 1127/1143] interface: log: silence some tracebacks ``` 191.73 | D | i/interface.[btc.electroncash.dk:60002] | (disconnect) trace for RPCError(-32603, 'internal error: bitcoind request timed out') Traceback (most recent call last): File "...\electrum\electrum\interface.py", line 514, in wrapper_func return await func(self, *args, **kwargs) File "...\electrum\electrum\interface.py", line 537, in run await self.open_session(ssl_context) File "...\electrum\electrum\interface.py", line 687, in open_session async with self.taskgroup as group: File "...\aiorpcX\aiorpcx\curio.py", line 304, in __aexit__ await self.join() File "...\electrum\electrum\util.py", line 1309, in join task.result() File "...\electrum\electrum\interface.py", line 724, in request_fee_estimates async with OldTaskGroup() as group: File "...\aiorpcX\aiorpcx\curio.py", line 304, in __aexit__ await self.join() File "...\electrum\electrum\util.py", line 1309, in join task.result() File "...\electrum\electrum\interface.py", line 1128, in get_estimatefee res = await self.session.send_request('blockchain.estimatefee', [num_blocks]) File "...\electrum\electrum\interface.py", line 169, in send_request response = await util.wait_for2( File "...\electrum\electrum\util.py", line 1383, in wait_for2 return await asyncio.ensure_future(fut, loop=get_running_loop()) File "...\aiorpcX\aiorpcx\session.py", line 540, in send_request return await self._send_concurrent(message, future, 1) File "...\aiorpcX\aiorpcx\session.py", line 512, in _send_concurrent return await future aiorpcx.jsonrpc.RPCError: (-32603, 'internal error: bitcoind request timed out') ``` ``` 93.60 | E | asyncio | Task exception was never retrieved future: exception=RPCError(-32603, 'internal error: bitcoind request timed out')> Traceback (most recent call last): File "...\electrum\electrum\interface.py", line 1132, in get_estimatefee res = await self.session.send_request('blockchain.estimatefee', [num_blocks]) File "...\electrum\electrum\interface.py", line 169, in send_request response = await util.wait_for2( File "...\electrum\electrum\util.py", line 1383, in wait_for2 return await asyncio.ensure_future(fut, loop=get_running_loop()) File "...\aiorpcX\aiorpcx\session.py", line 540, in send_request return await self._send_concurrent(message, future, 1) File "...\aiorpcX\aiorpcx\session.py", line 512, in _send_concurrent return await future aiorpcx.jsonrpc.RPCError: (-32603, 'internal error: bitcoind request timed out') ``` --- electrum/interface.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/electrum/interface.py b/electrum/interface.py index 1b4e6e229..352aedb6b 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -690,10 +690,14 @@ async def open_session(self, sslc, exit_early=False): await group.spawn(self.run_fetch_blocks) await group.spawn(self.monitor_connection) except aiorpcx.jsonrpc.RPCError as e: - if e.code in (JSONRPC.EXCESSIVE_RESOURCE_USAGE, - JSONRPC.SERVER_BUSY, - JSONRPC.METHOD_NOT_FOUND): - raise GracefulDisconnect(e, log_level=logging.WARNING) from e + if e.code in ( + JSONRPC.EXCESSIVE_RESOURCE_USAGE, + JSONRPC.SERVER_BUSY, + JSONRPC.METHOD_NOT_FOUND, + JSONRPC.INTERNAL_ERROR, + ): + log_level = logging.WARNING if self.is_main_server() else logging.INFO + raise GracefulDisconnect(e, log_level=log_level) from e raise finally: self.got_disconnected.set() # set this ASAP, ideally before any awaits @@ -1128,12 +1132,20 @@ async def get_estimatefee(self, num_blocks: int) -> int: res = await self.session.send_request('blockchain.estimatefee', [num_blocks]) except aiorpcx.jsonrpc.ProtocolError as e: # The protocol spec says the server itself should already have returned -1 - # if it cannot provide an estimate, however apparently electrs does not conform + # if it cannot provide an estimate, however apparently "electrs" does not conform # and sends an error instead. Convert it here: if "cannot estimate fee" in e.message: res = -1 else: raise + except aiorpcx.jsonrpc.RPCError as e: + # The protocol spec says the server itself should already have returned -1 + # if it cannot provide an estimate. "Fulcrum" often sends: + # aiorpcx.jsonrpc.RPCError: (-32603, 'internal error: bitcoind request timed out') + if e.code == JSONRPC.INTERNAL_ERROR: + res = -1 + else: + raise # check response if res != -1: assert_non_negative_int_or_float(res) From a187210f9027d4e21de4e4f863aa3c558268a408 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 14:31:15 +0000 Subject: [PATCH 1128/1143] labels plugin: don't log received data this log line is 120 KB for one of my wallets (not even cherry-picking a large one) --- electrum/plugins/labels/labels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/plugins/labels/labels.py b/electrum/plugins/labels/labels.py index 9e392f6f9..a6dd40f2b 100644 --- a/electrum/plugins/labels/labels.py +++ b/electrum/plugins/labels/labels.py @@ -137,7 +137,7 @@ async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool): if response["labels"] is None or len(response["labels"]) == 0: self.logger.info('no new labels') return - self.logger.debug(f"labels received {response!r}") + #self.logger.debug(f"labels received {response!r}") self.logger.info(f'received {len(response["labels"])} labels') result = {} for label in response["labels"]: From d663d92424107f7f0ce9a4bcd98f33760efff2bc Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 14:46:00 +0000 Subject: [PATCH 1129/1143] qml: handle importChannelBackupFailed in WalletMainView error was not being shown when scanning/pasting channel backup from Send screen --- electrum/gui/qml/components/WalletMainView.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 8adeb2d89..b0437cec1 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -312,6 +312,10 @@ Item { }) dialog.open() } + function onImportChannelBackupFailed(message) { + var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), text: message }) + dialog.open() + } } Component { From dfa0dd47b728d22a3b8143af4bdd1819a0997abd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 17:06:31 +0200 Subject: [PATCH 1130/1143] swapserver: remove config option LIGHTNING_SWAP_HTLC_FIRST; read it from get_pairs instead. --- electrum/plugins/swapserver/server.py | 1 + electrum/simple_config.py | 1 - electrum/submarine_swaps.py | 8 +++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index ba15b4cb6..0e22c2588 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -51,6 +51,7 @@ async def get_pairs(self, r): pairs = { "info": [], "warnings": [], + "htlcFirst": True, "pairs": { "BTC/BTC": { "rate": 1, diff --git a/electrum/simple_config.py b/electrum/simple_config.py index e0596d854..753bf388b 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -890,7 +890,6 @@ def get_swapserver_url(self): LIGHTNING_USE_GOSSIP = ConfigVar('use_gossip', default=False, type_=bool) LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) - LIGHTNING_SWAP_HTLC_FIRST = ConfigVar('swap_htlc_first', default=False, type_=bool) LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 2e6f8c014..4a8e60676 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -181,6 +181,7 @@ def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): self.percentage = 0 self._min_amount = None self._max_amount = None + self.server_supports_htlc_first = False self.wallet = wallet self.lnworker = lnworker @@ -531,7 +532,7 @@ async def normal_swap( refund_privkey = os.urandom(32) refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) - if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: + if self.server_supports_htlc_first: self.logger.info('requesting preimage hash for swap') request_data = { "invoiceAmount": lightning_amount_sat, @@ -580,7 +581,7 @@ async def normal_swap( lockup_address = data["address"] redeem_script = data["redeemScript"] # verify redeem_script is built with our pubkey and preimage - if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: + if self.server_supports_htlc_first: claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey) else: claim_pubkey, _ = check_normal_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey) @@ -605,7 +606,7 @@ async def normal_swap( prepay=False) tx = self.create_funding_tx(swap, tx, password) - if self.wallet.config.LIGHTNING_SWAP_HTLC_FIRST: + if self.server_supports_htlc_first: # send invoice to server and wait for htlcs request_data = { "preimageHash": payment_hash.hex(), @@ -780,6 +781,7 @@ async def get_pairs(self) -> None: limits = pairs['pairs']['BTC/BTC']['limits'] self._min_amount = limits['minimal'] self._max_amount = limits['maximal'] + self.server_supports_htlc_first = pairs.get('htlcFirst', False) def pairs_filename(self): return os.path.join(self.wallet.config.path, 'swap_pairs') From 5a3abdde85c86f82dd86ae28fe0ee232e310349f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 15:22:36 +0000 Subject: [PATCH 1131/1143] qml: don't apply ElListView deadzones on Linux desktop otherwise I can't click on list items if the window is moved too far to the right :O follow-up 583afefe33d7999fd3dfbcc5821be840e8dc1b4b Note: on modern Android, apps don't always run full-screen. You can pop them out into small windows and move them. Haven't tested how the deadzones work then though. --- electrum/gui/qml/components/controls/ElListView.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qml/components/controls/ElListView.qml b/electrum/gui/qml/components/controls/ElListView.qml index 9ae1c121e..60111b50e 100644 --- a/electrum/gui/qml/components/controls/ElListView.qml +++ b/electrum/gui/qml/components/controls/ElListView.qml @@ -26,13 +26,15 @@ ListView { // android back gesture is used function layoutExclusionZones() { var reserve = constants.fingerWidth / 2 - var p = root.mapToGlobal(0, 0) + var p = root.mapToGlobal(0, 0) // note: coords on whole *screen*, not just window width_left_exclusion_zone = Math.max(0, reserve - p.x) p = root.mapToGlobal(width, 0) width_right_exclusion_zone = Math.max(0, reserve - (app.width - p.x)) } Component.onCompleted: { - layoutExclusionZones() + if (AppController.isAndroid()) { + layoutExclusionZones() + } } } From 012ce1c1bb6fad43e55ab18e92f85ec7514e96e6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 17:24:23 +0200 Subject: [PATCH 1132/1143] Remove SSL options from config. This is out of scope for Electrum; HTTP services that require SSL should be exposed to the world through a reverse proxy. --- electrum/daemon.py | 2 +- electrum/plugins/payserver/payserver.py | 6 ++---- electrum/plugins/swapserver/server.py | 2 +- electrum/simple_config.py | 19 ------------------- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index ff9a7a2fd..becf9d441 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -373,7 +373,7 @@ def __init__(self, network: 'Network', netaddress): async def run(self): self.runner = web.AppRunner(self.app) await self.runner.setup() - site = web.TCPSite(self.runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context()) + site = web.TCPSite(self.runner, host=str(self.addr.host), port=self.addr.port) await site.start() self.logger.info(f"now running and listening. addr={self.addr}") diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index cfdd6ce42..b2d4c5e00 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -94,9 +94,7 @@ def has_www_dir(cls) -> bool: def base_url(self): payserver = self.config.PAYSERVER_ADDRESS payserver = NetAddress.from_string(payserver) - use_ssl = bool(self.config.SSL_KEYFILE_PATH) - protocol = 'https' if use_ssl else 'http' - return '%s://%s:%d'%(protocol, payserver.host, payserver.port) + return 'http://%s:%d'%(payserver.host, payserver.port) @property def root(self): @@ -123,7 +121,7 @@ async def run(self): app.add_routes([web.post('/api/create_invoice', self.create_request)]) runner = web.AppRunner(app) await runner.setup() - site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context()) + site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port) await site.start() self.logger.info(f"now running and listening. addr={self.addr}") diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 0e22c2588..36654453f 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -41,7 +41,7 @@ async def run(self): runner = web.AppRunner(app) await runner.setup() - site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context()) + site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port) await site.start() self.logger.info(f"now running and listening. addr={self.addr}") diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 753bf388b..21e0e3710 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -3,7 +3,6 @@ import time import os import stat -import ssl from decimal import Decimal from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set from numbers import Real @@ -757,22 +756,6 @@ def get_video_device(self): device = '' return device - def get_ssl_context(self): - ssl_keyfile = self.SSL_KEYFILE_PATH - ssl_certfile = self.SSL_CERTFILE_PATH - if ssl_keyfile and ssl_certfile: - ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile) - return ssl_context - - def get_ssl_domain(self): - from .paymentrequest import check_ssl_config - if self.SSL_KEYFILE_PATH and self.SSL_CERTFILE_PATH: - SSL_identity = check_ssl_config(self) - else: - SSL_identity = None - return SSL_identity - def get_netaddress(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> Optional[NetAddress]: if isinstance(key, (ConfigVar, ConfigVarWithConfig)): key = key.key() @@ -965,8 +948,6 @@ def get_swapserver_url(self): WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool) CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool) - SSL_CERTFILE_PATH = ConfigVar('ssl_certfile', default='', type_=str) - SSL_KEYFILE_PATH = ConfigVar('ssl_keyfile', default='', type_=str) # submarine swap server SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) From 9ea5193329e55b862f5123de46569025687333f6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 15:40:46 +0000 Subject: [PATCH 1133/1143] requirements-hw: pin ledger-bitcoin to "<0.2.2" ledger-bitcoin==0.2.2 added new deps we don't want to bundle. otherwise it should be ok to use. see https://github.com/LedgerHQ/app-bitcoin-new/issues/192 --- contrib/requirements/requirements-hw.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index e0c0efbf2..5c1b6ca64 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -11,8 +11,10 @@ keepkey>=6.3.1 # device plugin: ledger # note: btchip-python only needed for "legacy" protocol and HW.1 support +# note: ledger-bitcoin==0.2.2 added new deps we don't want to bundle. otherwise it should be ok to use. + see https://github.com/LedgerHQ/app-bitcoin-new/issues/192 btchip-python>=0.1.32 -ledger-bitcoin>=0.2.0,<0.3.0 +ledger-bitcoin>=0.2.0,<0.2.2 hidapi # device plugin: coldcard From f85354903d331ce128da4940cbfedfb3d91da3c5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Aug 2023 13:44:02 +0200 Subject: [PATCH 1134/1143] swapserver: try many times, to increase trampoline fee --- electrum/submarine_swaps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 4a8e60676..7d832a568 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -234,7 +234,7 @@ async def pay_pending_invoices(self): # fixme: should consider cltv of ln payment self.logger.info(f'locktime too close {key}') continue - success, log = await self.lnworker.pay_invoice(invoice.lightning_invoice, attempts=1) + success, log = await self.lnworker.pay_invoice(invoice.lightning_invoice, attempts=10) if not success: self.logger.info(f'failed to pay invoice {key}') continue From cc030c60e9b634cc918595b76324d4e0088086a7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 18:05:02 +0000 Subject: [PATCH 1135/1143] lnutil: make LnFeatures.supports() faster LnFeatures.supports() is became part of the hot path of LNPathFinder.find_path_for_payment() in 6b43eac6fdba2d42a36291185c3c0337e98e0b43, which made find_path_for_payment considerably slower than before. It used to take around 0.8 sec originally, then after linked commit went to ~9 sec, and now this takes it down to ~1.1 sec (on my laptop). --- electrum/lnrouter.py | 2 +- electrum/lnutil.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index c89ba7f33..9fd6d329b 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -493,7 +493,7 @@ def _edge_cost( # it's ok if we are missing the node_announcement (node_info) for this node, # but if we have it, we enforce that they support var_onion_optin node_features = LnFeatures(node_info.features) - if not node_features.supports(LnFeatures.VAR_ONION_OPT): + if not node_features.supports(LnFeatures.VAR_ONION_OPT): # note: this is kind of slow. could be cached. return float('inf'), 0 route_edge = RouteEdge.from_channel_policy( channel_policy=channel_policy, diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 821b94537..e5fe62009 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1250,13 +1250,13 @@ def supports(self, feature: 'LnFeatures') -> bool: you can do: myfeatures.supports(LnFeatures.VAR_ONION_OPT) """ - enabled_bits = list_enabled_bits(feature) - if len(enabled_bits) != 1: + if (1 << (feature.bit_length() - 1)) != feature: raise ValueError(f"'feature' cannot be a combination of features: {feature}") - flag = enabled_bits[0] - our_flags = set(list_enabled_bits(self)) - return (flag in our_flags - or get_ln_flag_pair_of_bit(flag) in our_flags) + if feature.bit_length() % 2 == 0: # feature is OPT + feature_other = feature >> 1 + else: # feature is REQ + feature_other = feature << 1 + return (self & feature != 0) or (self & feature_other != 0) def get_names(self) -> Sequence[str]: r = [] From 25574744492b40d807e1911ef208dffc89aef326 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 10 Aug 2023 18:06:59 +0000 Subject: [PATCH 1136/1143] (trivial) fix typo in requirements-hw.txt follow-up 9ea5193329e55b862f5123de46569025687333f6 --- contrib/requirements/requirements-hw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 5c1b6ca64..a77e273fe 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -12,7 +12,7 @@ keepkey>=6.3.1 # device plugin: ledger # note: btchip-python only needed for "legacy" protocol and HW.1 support # note: ledger-bitcoin==0.2.2 added new deps we don't want to bundle. otherwise it should be ok to use. - see https://github.com/LedgerHQ/app-bitcoin-new/issues/192 +# see https://github.com/LedgerHQ/app-bitcoin-new/issues/192 btchip-python>=0.1.32 ledger-bitcoin>=0.2.0,<0.2.2 hidapi From 9f5f802cd14159fe96e469215a3ef3f883f4ab97 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Aug 2023 08:12:54 +0200 Subject: [PATCH 1137/1143] config: save ports instead of net addresses (follow-up 012ce1c1bb6fad43e55ab18e92f85ec7514e96e6) --- electrum/daemon.py | 12 ++++++------ electrum/plugins/payserver/payserver.py | 12 ++++-------- electrum/plugins/swapserver/server.py | 7 +++---- electrum/simple_config.py | 20 ++++---------------- electrum/tests/regtest/regtest.sh | 2 +- 5 files changed, 18 insertions(+), 35 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index becf9d441..2ebe408ce 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -357,8 +357,8 @@ async def run_cmdline(self, config_options): class WatchTowerServer(AuthenticatedServer): - def __init__(self, network: 'Network', netaddress): - self.addr = netaddress + def __init__(self, network: 'Network', port:int): + self.port = port self.config = network.config self.network = network watchtower_user = self.config.WATCHTOWER_SERVER_USER or "" @@ -373,9 +373,9 @@ def __init__(self, network: 'Network', netaddress): async def run(self): self.runner = web.AppRunner(self.app) await self.runner.setup() - site = web.TCPSite(self.runner, host=str(self.addr.host), port=self.addr.port) + site = web.TCPSite(self.runner, host='localhost', port=self.port) await site.start() - self.logger.info(f"now running and listening. addr={self.addr}") + self.logger.info(f"running and listening on port {self.port}") async def get_ctn(self, *args): return await self.lnwatcher.get_ctn(*args) @@ -453,8 +453,8 @@ def start_network(self): assert not self.config.NETWORK_OFFLINE assert self.network # server-side watchtower - if watchtower_address := self.config.get_netaddress(self.config.cv.WATCHTOWER_SERVER_ADDRESS): - self.watchtower = WatchTowerServer(self.network, watchtower_address) + if watchtower_port := self.config.WATCHTOWER_SERVER_PORT: + self.watchtower = WatchTowerServer(self.network, watchtower_port) asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.watchtower.run), self.asyncio_loop) self.network.start(jobs=[self.fx.run]) diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index b2d4c5e00..64b2f8d69 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Optional from aiohttp import web -from aiorpcx import NetAddress from electrum import util from electrum.util import log_exceptions, ignore_exceptions @@ -80,8 +79,7 @@ def __init__(self, config: 'SimpleConfig', wallet: 'Abstract_Wallet'): assert self.has_www_dir(), self.WWW_DIR self.config = config self.wallet = wallet - url = self.config.PAYSERVER_ADDRESS - self.addr = NetAddress.from_string(url) + self.port = self.config.PAYSERVER_PORT self.pending = defaultdict(asyncio.Event) self.register_callbacks() @@ -92,9 +90,7 @@ def has_www_dir(cls) -> bool: @property def base_url(self): - payserver = self.config.PAYSERVER_ADDRESS - payserver = NetAddress.from_string(payserver) - return 'http://%s:%d'%(payserver.host, payserver.port) + return 'http://localhost:%d'%self.port @property def root(self): @@ -121,9 +117,9 @@ async def run(self): app.add_routes([web.post('/api/create_invoice', self.create_request)]) runner = web.AppRunner(app) await runner.setup() - site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port) + site = web.TCPSite(runner, host='localhost', port=self.port) await site.start() - self.logger.info(f"now running and listening. addr={self.addr}") + self.logger.info(f"running and listening on port {self.port}") async def create_request(self, request): params = await request.post() diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 36654453f..e8852b8a4 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -3,7 +3,6 @@ from collections import defaultdict from aiohttp import web -from aiorpcx import NetAddress from electrum.util import log_exceptions, ignore_exceptions from electrum.logging import Logger @@ -24,7 +23,7 @@ def __init__(self, config, wallet): self.config = config self.wallet = wallet self.sm = self.wallet.lnworker.swap_manager - self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS) + self.port = self.config.SWAPSERVER_PORT self.register_callbacks() # eventlistener self.pending = defaultdict(asyncio.Event) @@ -41,9 +40,9 @@ async def run(self): runner = web.AppRunner(app) await runner.setup() - site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port) + site = web.TCPSite(runner, host='localhost', port=self.port) await site.start() - self.logger.info(f"now running and listening. addr={self.addr}") + self.logger.info(f"running and listening on port {self.port}") async def get_pairs(self, r): sm = self.sm diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 21e0e3710..a840baa6d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -9,7 +9,6 @@ from functools import cached_property from copy import deepcopy -from aiorpcx import NetAddress from . import util from . import constants @@ -756,17 +755,6 @@ def get_video_device(self): device = '' return device - def get_netaddress(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> Optional[NetAddress]: - if isinstance(key, (ConfigVar, ConfigVarWithConfig)): - key = key.key() - assert isinstance(key, str), key - text = self.get(key) - if text: - try: - return NetAddress.from_string(text) - except Exception: - pass - def format_amount( self, amount_sat, @@ -950,23 +938,23 @@ def get_swapserver_url(self): # submarine swap server SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str) + SWAPSERVER_PORT = ConfigVar('swapserver_port', default=5455, type_=int) TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool) + # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) # run WT locally WATCHTOWER_SERVER_ENABLED = ConfigVar('run_watchtower', default=False, type_=bool) - WATCHTOWER_SERVER_ADDRESS = ConfigVar('watchtower_address', default=None, type_=str) + WATCHTOWER_SERVER_PORT = ConfigVar('watchtower_port', default=None, type_=int) WATCHTOWER_SERVER_USER = ConfigVar('watchtower_user', default=None, type_=str) WATCHTOWER_SERVER_PASSWORD = ConfigVar('watchtower_password', default=None, type_=str) - PAYSERVER_ADDRESS = ConfigVar('payserver_address', default='localhost:8080', type_=str) + PAYSERVER_PORT = ConfigVar('payserver_port', default=8080, type_=int) PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str) PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool) - SWAPSERVER_ADDRESS = ConfigVar('swapserver_address', default='localhost:5455', type_=str) - PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 836380daa..e32141945 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -413,7 +413,7 @@ if [[ $1 == "configure_test_watchtower" ]]; then $carol setconfig -o run_watchtower true $carol setconfig -o watchtower_user wtuser $carol setconfig -o watchtower_password wtpassword - $carol setconfig -o watchtower_address 127.0.0.1:12345 + $carol setconfig -o watchtower_port 12345 $bob setconfig -o watchtower_url http://wtuser:wtpassword@127.0.0.1:12345 fi From 8f768d1da5b9d17ef08bbaced5dbf167d95447b6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 11 Aug 2023 13:53:57 +0000 Subject: [PATCH 1138/1143] lnworker.pay_to_node: log num htlcs in-flight --- electrum/lnworker.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 0d57eb27b..5e44a44b2 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1296,6 +1296,7 @@ async def pay_to_node( self.failed_trampoline_routes = [] start_time = time.time() amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees) + nhtlcs_inflight = 0 while True: amount_to_send = amount_to_pay - amount_inflight if amount_to_send > 0: @@ -1319,6 +1320,7 @@ async def pay_to_node( ) # 2. send htlcs async for sent_htlc_info, cltv_delta, trampoline_onion in routes: + nhtlcs_inflight += 1 amount_inflight += sent_htlc_info.amount_receiver_msat if amount_inflight > amount_to_pay: # safety belts raise Exception(f"amount_inflight={amount_inflight} > amount_to_pay={amount_to_pay}") @@ -1329,16 +1331,17 @@ async def pay_to_node( min_cltv_expiry=cltv_delta, trampoline_onion=trampoline_onion, ) - # invoice_status is triggered in self.set_invoice_status when it actally changes. + # invoice_status is triggered in self.set_invoice_status when it actually changes. # It is also triggered here to update progress for a lightning payment in the GUI # (e.g. attempt counter) util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) # 3. await a queue - self.logger.info(f"amount inflight {amount_inflight}") - htlc_log = await self.sent_htlcs_q[payment_key].get() + self.logger.info(f"(paysession for RHASH {payment_hash.hex()}) {amount_inflight=}. {nhtlcs_inflight=}") + htlc_log = await self.sent_htlcs_q[payment_key].get() # TODO maybe wait a bit, more failures might come amount_inflight -= htlc_log.amount_msat - if amount_inflight < 0: - raise Exception(f"amount_inflight={amount_inflight} < 0") + nhtlcs_inflight -= 1 + if amount_inflight < 0 or nhtlcs_inflight < 0: + raise Exception(f"{amount_inflight=}, {nhtlcs_inflight=}. both should be >= 0 !") log.append(htlc_log) if htlc_log.success: if self.network.path_finder: From 35c9ac8f3173deaac14230ae1991a19d0d10987d Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 11 Aug 2023 15:08:18 +0000 Subject: [PATCH 1139/1143] lnworker: MPP send: more aggressively split large htlcs related: https://github.com/spesmilo/electrum/issues/7987#issuecomment-1670002482 --- electrum/lnrouter.py | 3 +++ electrum/lnworker.py | 32 +++++++++++++++++++++++++++---- electrum/simple_config.py | 1 + electrum/tests/regtest/regtest.sh | 1 + electrum/tests/test_lnpeer.py | 2 ++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py index 9fd6d329b..167134b99 100644 --- a/electrum/lnrouter.py +++ b/electrum/lnrouter.py @@ -153,6 +153,9 @@ def is_route_sane_to_use(route: LNPaymentRoute, invoice_amount_msat: int, min_fi # TODO revise ad-hoc heuristics if cltv > NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: return False + # FIXME in case of MPP, the fee checks are done independently for each part, + # which is ok for the proportional checks but not for the absolute ones. + # This is not that big of a deal though as we don't split into *too many* parts. if not is_fee_sane(total_fee, payment_amount_msat=invoice_amount_msat): return False return True diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 5e44a44b2..8777d5626 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -83,7 +83,7 @@ from .channel_db import get_mychannel_info, get_mychannel_policy from .submarine_swaps import SwapManager from .channel_db import ChannelInfo, Policy -from .mpp_split import suggest_splits +from .mpp_split import suggest_splits, SplitConfigRating from .trampoline import create_trampoline_route_and_onion, TRAMPOLINE_FEES, is_legacy_relay if TYPE_CHECKING: @@ -661,6 +661,8 @@ class LNWallet(LNWorker): MPP_EXPIRY = 120 TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 3 # seconds PAYMENT_TIMEOUT = 120 + MPP_SPLIT_PART_FRACTION = 0.2 + MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000 def __init__(self, wallet: 'Abstract_Wallet', xprv): self.wallet = wallet @@ -1604,12 +1606,21 @@ def suggest_peer(self) -> Optional[bytes]: else: return random.choice(list(hardcoded_trampoline_nodes().values())).pubkey - def suggest_splits(self, amount_msat: int, my_active_channels, invoice_features, r_tags): + def suggest_splits( + self, + *, + amount_msat: int, + final_total_msat: int, + my_active_channels: Sequence[Channel], + invoice_features: LnFeatures, + r_tags, + ) -> List['SplitConfigRating']: channels_with_funds = { (chan.channel_id, chan.node_id): int(chan.available_to_spend(HTLCOwner.LOCAL)) for chan in my_active_channels } self.logger.info(f"channels_with_funds: {channels_with_funds}") + exclude_single_part_payments = False if self.uses_trampoline(): # in the case of a legacy payment, we don't allow splitting via different # trampoline nodes, because of https://github.com/ACINQ/eclair/issues/2127 @@ -1621,10 +1632,15 @@ def suggest_splits(self, amount_msat: int, my_active_channels, invoice_features, else: exclude_multinode_payments = False exclude_single_channel_splits = False + if invoice_features.supports(LnFeatures.BASIC_MPP_OPT) and not self.config.TEST_FORCE_DISABLE_MPP: + # if amt is still large compared to total_msat, split it: + if (amount_msat / final_total_msat > self.MPP_SPLIT_PART_FRACTION + and amount_msat > self.MPP_SPLIT_PART_MINAMT_MSAT): + exclude_single_part_payments = True split_configurations = suggest_splits( amount_msat, channels_with_funds, - exclude_single_part_payments=False, + exclude_single_part_payments=exclude_single_part_payments, exclude_multinode_payments=exclude_multinode_payments, exclude_single_channel_splits=exclude_single_channel_splits ) @@ -1664,7 +1680,13 @@ async def create_routes_for_payment( chan.is_active() and not chan.is_frozen_for_sending()] # try random order random.shuffle(my_active_channels) - split_configurations = self.suggest_splits(amount_msat, my_active_channels, invoice_features, r_tags) + split_configurations = self.suggest_splits( + amount_msat=amount_msat, + final_total_msat=final_total_msat, + my_active_channels=my_active_channels, + invoice_features=invoice_features, + r_tags=r_tags, + ) for sc in split_configurations: is_multichan_mpp = len(sc.config.items()) > 1 is_mpp = sum(len(x) for x in list(sc.config.values())) > 1 @@ -1672,6 +1694,8 @@ async def create_routes_for_payment( continue if not is_mpp and self.config.TEST_FORCE_MPP: continue + if is_mpp and self.config.TEST_FORCE_DISABLE_MPP: + continue self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}") routes = [] try: diff --git a/electrum/simple_config.py b/electrum/simple_config.py index a840baa6d..971324f3c 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -869,6 +869,7 @@ def get_swapserver_url(self): TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool) TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool) TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool) + TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool) TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int) TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None) TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index e32141945..079d835df 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -93,6 +93,7 @@ if [[ $1 == "init" ]]; then $agent setconfig --offline use_gossip True $agent setconfig --offline server 127.0.0.1:51001:t $agent setconfig --offline lightning_to_self_delay 144 + $agent setconfig --offline test_force_disable_mpp True # alice is funded, bob is listening if [[ $2 == "bob" ]]; then $bob setconfig --offline lightning_listen localhost:9735 diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 544e0d9ed..87cabc95d 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -133,6 +133,8 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): PAYMENT_TIMEOUT = 120 TIMEOUT_SHUTDOWN_FAIL_PENDING_HTLCS = 0 INITIAL_TRAMPOLINE_FEE_LEVEL = 0 + MPP_SPLIT_PART_FRACTION = 1 # this disables the forced splitting + MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000 def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name): self.name = name From 98bea49a3cebbafb158858d7a7430b443357ae5f Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 11 Aug 2023 15:48:48 +0000 Subject: [PATCH 1140/1143] lnworker.pay_to_node: make trampoline fee_level and failed_routes local multiple instances of pay_to_node might run concurrently, esp for trampoline forwarding --- electrum/lnworker.py | 32 +++++++++++++++++++------------- electrum/tests/test_lnpeer.py | 1 + electrum/trampoline.py | 22 +++++++++++----------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 8777d5626..c2d8cc970 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1294,8 +1294,8 @@ async def pay_to_node( # sometimes need to fall back to a single trampoline forwarder, at the expense # of privacy use_two_trampolines = True - self.trampoline_fee_level = self.INITIAL_TRAMPOLINE_FEE_LEVEL - self.failed_trampoline_routes = [] + trampoline_fee_level = self.INITIAL_TRAMPOLINE_FEE_LEVEL + failed_trampoline_routes = [] start_time = time.time() amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees) nhtlcs_inflight = 0 @@ -1315,7 +1315,8 @@ async def pay_to_node( full_path=full_path, payment_hash=payment_hash, payment_secret=payment_secret, - trampoline_fee_level=self.trampoline_fee_level, + trampoline_fee_level=trampoline_fee_level, + failed_trampoline_routes=failed_trampoline_routes, use_two_trampolines=use_two_trampolines, fwd_trampoline_onion=fwd_trampoline_onion, channels=channels, @@ -1326,7 +1327,7 @@ async def pay_to_node( amount_inflight += sent_htlc_info.amount_receiver_msat if amount_inflight > amount_to_pay: # safety belts raise Exception(f"amount_inflight={amount_inflight} > amount_to_pay={amount_to_pay}") - sent_htlc_info = sent_htlc_info._replace(trampoline_fee_level=self.trampoline_fee_level) + sent_htlc_info = sent_htlc_info._replace(trampoline_fee_level=trampoline_fee_level) await self.pay_to_route( sent_htlc_info=sent_htlc_info, payment_hash=payment_hash, @@ -1338,7 +1339,7 @@ async def pay_to_node( # (e.g. attempt counter) util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) # 3. await a queue - self.logger.info(f"(paysession for RHASH {payment_hash.hex()}) {amount_inflight=}. {nhtlcs_inflight=}") + self.logger.info(f"paysession for RHASH {payment_hash.hex()} waiting... {amount_inflight=}. {nhtlcs_inflight=}") htlc_log = await self.sent_htlcs_q[payment_key].get() # TODO maybe wait a bit, more failures might come amount_inflight -= htlc_log.amount_msat nhtlcs_inflight -= 1 @@ -1373,12 +1374,14 @@ async def pay_to_node( # trampoline if self.uses_trampoline(): def maybe_raise_trampoline_fee(htlc_log): - if htlc_log.trampoline_fee_level == self.trampoline_fee_level: - self.trampoline_fee_level += 1 - self.failed_trampoline_routes = [] - self.logger.info(f'raising trampoline fee level {self.trampoline_fee_level}') + nonlocal trampoline_fee_level + nonlocal failed_trampoline_routes + if htlc_log.trampoline_fee_level == trampoline_fee_level: + trampoline_fee_level += 1 + failed_trampoline_routes = [] + self.logger.info(f'raising trampoline fee level {trampoline_fee_level}') else: - self.logger.info(f'NOT raising trampoline fee level, already at {self.trampoline_fee_level}') + self.logger.info(f'NOT raising trampoline fee level, already at {trampoline_fee_level}') # FIXME The trampoline nodes in the path are chosen randomly. # Some of the errors might depend on how we have chosen them. # Having more attempts is currently useful in part because of the randomness, @@ -1402,8 +1405,10 @@ def maybe_raise_trampoline_fee(htlc_log): trampoline_route = htlc_log.route r = [hop.end_node.hex() for hop in trampoline_route] self.logger.info(f'failed trampoline route: {r}') - assert r not in self.failed_trampoline_routes - self.failed_trampoline_routes.append(r) + if r not in failed_trampoline_routes: + failed_trampoline_routes.append(r) + else: + pass # maybe the route was reused between different MPP parts continue else: raise PaymentFailure(failure_msg.code_name()) @@ -1658,6 +1663,7 @@ async def create_routes_for_payment( payment_hash: bytes, payment_secret: bytes, trampoline_fee_level: int, + failed_trampoline_routes: Iterable[Sequence[str]], use_two_trampolines: bool, fwd_trampoline_onion=None, full_path: LNPaymentPath = None, @@ -1731,7 +1737,7 @@ async def create_routes_for_payment( local_height=local_height, trampoline_fee_level=trampoline_fee_level, use_two_trampolines=use_two_trampolines, - failed_routes=self.failed_trampoline_routes) + failed_routes=failed_trampoline_routes) # node_features is only used to determine is_tlv per_trampoline_secret = os.urandom(32) per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 87cabc95d..f1f69d583 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -240,6 +240,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln r_tags=decoded_invoice.get_routing_info('r'), invoice_features=decoded_invoice.get_features(), trampoline_fee_level=0, + failed_trampoline_routes=[], use_two_trampolines=False, payment_hash=decoded_invoice.paymenthash, payment_secret=decoded_invoice.payment_secret, diff --git a/electrum/trampoline.py b/electrum/trampoline.py index c3c5dd6f7..9de96d25d 100644 --- a/electrum/trampoline.py +++ b/electrum/trampoline.py @@ -2,7 +2,7 @@ import bitstring import random -from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List +from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence from .lnutil import LnFeatures from .lnonion import calc_hops_data_for_payment, new_onion_packet @@ -141,7 +141,7 @@ def trampoline_policy( raise NoPathFound() -def extend_trampoline_route( +def _extend_trampoline_route( route: List, start_node: bytes, end_node: bytes, @@ -161,7 +161,7 @@ def extend_trampoline_route( node_features=trampoline_features)) -def choose_second_trampoline(my_trampoline, trampolines, failed_routes): +def _choose_second_trampoline(my_trampoline, trampolines, failed_routes: Iterable[Sequence[str]]): if my_trampoline in trampolines: trampolines.remove(my_trampoline) for r in failed_routes: @@ -184,7 +184,7 @@ def create_trampoline_route( r_tags, trampoline_fee_level: int, use_two_trampolines: bool, - failed_routes: list, + failed_routes: Iterable[Sequence[str]], ) -> LNPaymentRoute: # we decide whether to convert to a legacy payment is_legacy, invoice_trampolines = is_legacy_relay(invoice_features, r_tags) @@ -194,14 +194,14 @@ def create_trampoline_route( second_trampoline = None # our first trampoline hop is decided by the channel we use - extend_trampoline_route(route, my_pubkey, my_trampoline, trampoline_fee_level) + _extend_trampoline_route(route, my_pubkey, my_trampoline, trampoline_fee_level) if is_legacy: # we add another different trampoline hop for privacy if use_two_trampolines: trampolines = trampolines_by_id() - second_trampoline = choose_second_trampoline(my_trampoline, list(trampolines.keys()), failed_routes) - extend_trampoline_route(route, my_trampoline, second_trampoline, trampoline_fee_level) + second_trampoline = _choose_second_trampoline(my_trampoline, list(trampolines.keys()), failed_routes) + _extend_trampoline_route(route, my_trampoline, second_trampoline, trampoline_fee_level) # the last trampoline onion must contain routing hints for the last trampoline # node to find the recipient invoice_routing_info = encode_routing_info(r_tags) @@ -219,11 +219,11 @@ def create_trampoline_route( else: add_trampoline = True if add_trampoline: - second_trampoline = choose_second_trampoline(my_trampoline, invoice_trampolines, failed_routes) - extend_trampoline_route(route, my_trampoline, second_trampoline, trampoline_fee_level) + second_trampoline = _choose_second_trampoline(my_trampoline, invoice_trampolines, failed_routes) + _extend_trampoline_route(route, my_trampoline, second_trampoline, trampoline_fee_level) # final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob) - extend_trampoline_route(route, route[-1].end_node, invoice_pubkey, trampoline_fee_level, pay_fees=False) + _extend_trampoline_route(route, route[-1].end_node, invoice_pubkey, trampoline_fee_level, pay_fees=False) # check that we can pay amount and fees for edge in route[::-1]: amount_msat += edge.fee_for_edge(amount_msat) @@ -294,7 +294,7 @@ def create_trampoline_route_and_onion( local_height: int, trampoline_fee_level: int, use_two_trampolines: bool, - failed_routes: list): + failed_routes: Iterable[Sequence[str]]): # create route for the trampoline_onion trampoline_route = create_trampoline_route( amount_msat=amount_msat, From 00e88c4e50c71386664fffeb069bd1e86a8ead9a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 11 Aug 2023 18:36:39 +0000 Subject: [PATCH 1141/1143] lnworker: introduce PaySession cls, refactor pay_to_node --- electrum/lnworker.py | 274 ++++++++++++++++++++-------------- electrum/tests/test_lnpeer.py | 35 ++--- 2 files changed, 176 insertions(+), 133 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index c2d8cc970..9f3eae07a 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -17,7 +17,7 @@ import aiohttp import json from datetime import datetime, timezone -from functools import partial +from functools import partial, cached_property from collections import defaultdict import concurrent from concurrent import futures @@ -655,6 +655,105 @@ def process_node_anns(): self.logger.debug(f'process_gossip: {len(categorized_chan_upds.good)}/{len(chan_upds)}') +class PaySession(Logger): + def __init__( + self, + *, + payment_hash: bytes, + payment_secret: bytes, + initial_trampoline_fee_level: int, + invoice_features: int, + r_tags, + min_cltv_expiry: int, + amount_to_pay: int, # total payment amount final receiver will get + invoice_pubkey: bytes, + ): + assert payment_hash + assert payment_secret + self.payment_hash = payment_hash + self.payment_secret = payment_secret + self.payment_key = payment_hash + payment_secret + Logger.__init__(self) + + self.invoice_features = LnFeatures(invoice_features) + self.r_tags = r_tags + self.min_cltv_expiry = min_cltv_expiry + self.amount_to_pay = amount_to_pay + self.invoice_pubkey = invoice_pubkey + + self.sent_htlcs_q = asyncio.Queue() # type: asyncio.Queue[HtlcLog] + self.start_time = time.time() + + self.trampoline_fee_level = initial_trampoline_fee_level + self.failed_trampoline_routes = [] + self.use_two_trampolines = True + + self._amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees) + self._nhtlcs_inflight = 0 + + def diagnostic_name(self): + pkey = sha256(self.payment_key) + return f"{self.payment_hash[:4].hex()}-{pkey[:2].hex()}" + + def maybe_raise_trampoline_fee(self, htlc_log: HtlcLog): + if htlc_log.trampoline_fee_level == self.trampoline_fee_level: + self.trampoline_fee_level += 1 + self.failed_trampoline_routes = [] + self.logger.info(f'raising trampoline fee level {self.trampoline_fee_level}') + else: + self.logger.info(f'NOT raising trampoline fee level, already at {self.trampoline_fee_level}') + + def handle_failed_trampoline_htlc(self, *, htlc_log: HtlcLog, failure_msg: OnionRoutingFailure): + # FIXME The trampoline nodes in the path are chosen randomly. + # Some of the errors might depend on how we have chosen them. + # Having more attempts is currently useful in part because of the randomness, + # instead we should give feedback to create_routes_for_payment. + # Sometimes the trampoline node fails to send a payment and returns + # TEMPORARY_CHANNEL_FAILURE, while it succeeds with a higher trampoline fee. + if failure_msg.code in ( + OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, + OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, + OnionFailureCode.TEMPORARY_CHANNEL_FAILURE): + # TODO: parse the node policy here (not returned by eclair yet) + # TODO: erring node is always the first trampoline even if second + # trampoline demands more fees, we can't influence this + self.maybe_raise_trampoline_fee(htlc_log) + elif self.use_two_trampolines: + self.use_two_trampolines = False + elif failure_msg.code in ( + OnionFailureCode.UNKNOWN_NEXT_PEER, + OnionFailureCode.TEMPORARY_NODE_FAILURE): + trampoline_route = htlc_log.route + r = [hop.end_node.hex() for hop in trampoline_route] + self.logger.info(f'failed trampoline route: {r}') + if r not in self.failed_trampoline_routes: + self.failed_trampoline_routes.append(r) + else: + pass # maybe the route was reused between different MPP parts + else: + raise PaymentFailure(failure_msg.code_name()) + + async def wait_for_one_htlc_to_resolve(self) -> HtlcLog: + self.logger.info(f"waiting... amount_inflight={self._amount_inflight}. nhtlcs_inflight={self._nhtlcs_inflight}") + htlc_log = await self.sent_htlcs_q.get() + self._amount_inflight -= htlc_log.amount_msat + self._nhtlcs_inflight -= 1 + if self._amount_inflight < 0 or self._nhtlcs_inflight < 0: + raise Exception(f"amount_inflight={self._amount_inflight}, nhtlcs_inflight={self._nhtlcs_inflight}. both should be >= 0 !") + return htlc_log + + def add_new_htlc(self, sent_htlc_info: SentHtlcInfo) -> SentHtlcInfo: + self._nhtlcs_inflight += 1 + self._amount_inflight += sent_htlc_info.amount_receiver_msat + if self._amount_inflight > self.amount_to_pay: # safety belts + raise Exception(f"amount_inflight={self._amount_inflight} > amount_to_pay={self.amount_to_pay}") + sent_htlc_info = sent_htlc_info._replace(trampoline_fee_level=self.trampoline_fee_level) + return sent_htlc_info + + def get_outstanding_amount_to_send(self) -> int: + return self.amount_to_pay - self._amount_inflight + + class LNWallet(LNWorker): lnwatcher: Optional['LNWalletWatcher'] @@ -694,9 +793,9 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): for channel_id, storage in channel_backups.items(): self._channel_backups[bfh(channel_id)] = ChannelBackup(storage, lnworker=self) - self.sent_htlcs_q = defaultdict(asyncio.Queue) # type: Dict[bytes, asyncio.Queue[HtlcLog]] + self._paysessions = dict() # type: Dict[bytes, PaySession] self.sent_htlcs_info = dict() # type: Dict[SentHtlcKey, SentHtlcInfo] - self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) + self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) # TODO move into PaySession self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_key -> ReceivedMPPStatus # detect inflight payments @@ -1274,9 +1373,9 @@ async def pay_to_node( invoice_features: int, attempts: int = None, full_path: LNPaymentPath = None, - fwd_trampoline_onion=None, - fwd_trampoline_fee=None, - fwd_trampoline_cltv_delta=None, + fwd_trampoline_onion: OnionPacket = None, + fwd_trampoline_fee: int = None, + fwd_trampoline_cltv_delta: int = None, channels: Optional[Sequence[Channel]] = None, ) -> None: @@ -1288,46 +1387,37 @@ async def pay_to_node( raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'') payment_key = payment_hash + payment_secret + #assert payment_key not in self._paysessions # FIXME + self._paysessions[payment_key] = paysession = PaySession( + payment_hash=payment_hash, + payment_secret=payment_secret, + initial_trampoline_fee_level=self.INITIAL_TRAMPOLINE_FEE_LEVEL, + invoice_features=invoice_features, + r_tags=r_tags, + min_cltv_expiry=min_cltv_expiry, + amount_to_pay=amount_to_pay, + invoice_pubkey=node_pubkey, + ) self.logs[payment_hash.hex()] = log = [] # TODO incl payment_secret in key (re trampoline forwarding) # when encountering trampoline forwarding difficulties in the legacy case, we # sometimes need to fall back to a single trampoline forwarder, at the expense # of privacy - use_two_trampolines = True - trampoline_fee_level = self.INITIAL_TRAMPOLINE_FEE_LEVEL - failed_trampoline_routes = [] - start_time = time.time() - amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees) - nhtlcs_inflight = 0 while True: - amount_to_send = amount_to_pay - amount_inflight - if amount_to_send > 0: + if (amount_to_send := paysession.get_outstanding_amount_to_send()) > 0: # 1. create a set of routes for remaining amount. # note: path-finding runs in a separate thread so that we don't block the asyncio loop # graph updates might occur during the computation routes = self.create_routes_for_payment( + paysession=paysession, amount_msat=amount_to_send, - final_total_msat=amount_to_pay, - invoice_pubkey=node_pubkey, - min_cltv_expiry=min_cltv_expiry, - r_tags=r_tags, - invoice_features=invoice_features, full_path=full_path, - payment_hash=payment_hash, - payment_secret=payment_secret, - trampoline_fee_level=trampoline_fee_level, - failed_trampoline_routes=failed_trampoline_routes, - use_two_trampolines=use_two_trampolines, fwd_trampoline_onion=fwd_trampoline_onion, channels=channels, ) # 2. send htlcs async for sent_htlc_info, cltv_delta, trampoline_onion in routes: - nhtlcs_inflight += 1 - amount_inflight += sent_htlc_info.amount_receiver_msat - if amount_inflight > amount_to_pay: # safety belts - raise Exception(f"amount_inflight={amount_inflight} > amount_to_pay={amount_to_pay}") - sent_htlc_info = sent_htlc_info._replace(trampoline_fee_level=trampoline_fee_level) + sent_htlc_info = paysession.add_new_htlc(sent_htlc_info) await self.pay_to_route( sent_htlc_info=sent_htlc_info, payment_hash=payment_hash, @@ -1339,12 +1429,7 @@ async def pay_to_node( # (e.g. attempt counter) util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) # 3. await a queue - self.logger.info(f"paysession for RHASH {payment_hash.hex()} waiting... {amount_inflight=}. {nhtlcs_inflight=}") - htlc_log = await self.sent_htlcs_q[payment_key].get() # TODO maybe wait a bit, more failures might come - amount_inflight -= htlc_log.amount_msat - nhtlcs_inflight -= 1 - if amount_inflight < 0 or nhtlcs_inflight < 0: - raise Exception(f"{amount_inflight=}, {nhtlcs_inflight=}. both should be >= 0 !") + htlc_log = await paysession.wait_for_one_htlc_to_resolve() # TODO maybe wait a bit, more failures might come log.append(htlc_log) if htlc_log.success: if self.network.path_finder: @@ -1357,7 +1442,7 @@ async def pay_to_node( self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False) return # htlc failed - if (attempts is not None and len(log) >= attempts) or (attempts is None and time.time() - start_time > self.PAYMENT_TIMEOUT): + if (attempts is not None and len(log) >= attempts) or (attempts is None and time.time() - paysession.start_time > self.PAYMENT_TIMEOUT): raise PaymentFailure('Giving up after %d attempts'%len(log)) # if we get a tmp channel failure, it might work to split the amount and try more routes # if we get a channel update, we might retry the same route and amount @@ -1373,45 +1458,8 @@ async def pay_to_node( raise PaymentFailure(failure_msg.code_name()) # trampoline if self.uses_trampoline(): - def maybe_raise_trampoline_fee(htlc_log): - nonlocal trampoline_fee_level - nonlocal failed_trampoline_routes - if htlc_log.trampoline_fee_level == trampoline_fee_level: - trampoline_fee_level += 1 - failed_trampoline_routes = [] - self.logger.info(f'raising trampoline fee level {trampoline_fee_level}') - else: - self.logger.info(f'NOT raising trampoline fee level, already at {trampoline_fee_level}') - # FIXME The trampoline nodes in the path are chosen randomly. - # Some of the errors might depend on how we have chosen them. - # Having more attempts is currently useful in part because of the randomness, - # instead we should give feedback to create_routes_for_payment. - # Sometimes the trampoline node fails to send a payment and returns - # TEMPORARY_CHANNEL_FAILURE, while it succeeds with a higher trampoline fee. - if code in ( - OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, - OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, - OnionFailureCode.TEMPORARY_CHANNEL_FAILURE): - # TODO: parse the node policy here (not returned by eclair yet) - # TODO: erring node is always the first trampoline even if second - # trampoline demands more fees, we can't influence this - maybe_raise_trampoline_fee(htlc_log) - continue - elif use_two_trampolines: - use_two_trampolines = False - elif code in ( - OnionFailureCode.UNKNOWN_NEXT_PEER, - OnionFailureCode.TEMPORARY_NODE_FAILURE): - trampoline_route = htlc_log.route - r = [hop.end_node.hex() for hop in trampoline_route] - self.logger.info(f'failed trampoline route: {r}') - if r not in failed_trampoline_routes: - failed_trampoline_routes.append(r) - else: - pass # maybe the route was reused between different MPP parts - continue - else: - raise PaymentFailure(failure_msg.code_name()) + paysession.handle_failed_trampoline_htlc( + htlc_log=htlc_log, failure_msg=failure_msg) else: self.handle_error_code_from_failed_htlc( route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat) @@ -1654,18 +1702,9 @@ def suggest_splits( async def create_routes_for_payment( self, *, + paysession: PaySession, amount_msat: int, # part of payment amount we want routes for now - final_total_msat: int, # total payment amount final receiver will get - invoice_pubkey, - min_cltv_expiry, - r_tags, - invoice_features: int, - payment_hash: bytes, - payment_secret: bytes, - trampoline_fee_level: int, - failed_trampoline_routes: Iterable[Sequence[str]], - use_two_trampolines: bool, - fwd_trampoline_onion=None, + fwd_trampoline_onion: OnionPacket = None, full_path: LNPaymentPath = None, channels: Optional[Sequence[Channel]] = None, ) -> AsyncGenerator[Tuple[SentHtlcInfo, int, Optional[OnionPacket]], None]: @@ -1675,7 +1714,6 @@ async def create_routes_for_payment( We first try to conduct the payment over a single channel. If that fails and mpp is supported by the receiver, we will split the payment.""" - invoice_features = LnFeatures(invoice_features) trampoline_features = LnFeatures.VAR_ONION_OPT local_height = self.network.get_local_height() if channels: @@ -1688,15 +1726,15 @@ async def create_routes_for_payment( random.shuffle(my_active_channels) split_configurations = self.suggest_splits( amount_msat=amount_msat, - final_total_msat=final_total_msat, + final_total_msat=paysession.amount_to_pay, my_active_channels=my_active_channels, - invoice_features=invoice_features, - r_tags=r_tags, + invoice_features=paysession.invoice_features, + r_tags=paysession.r_tags, ) for sc in split_configurations: is_multichan_mpp = len(sc.config.items()) > 1 is_mpp = sum(len(x) for x in list(sc.config.values())) > 1 - if is_mpp and not invoice_features.supports(LnFeatures.BASIC_MPP_OPT): + if is_mpp and not paysession.invoice_features.supports(LnFeatures.BASIC_MPP_OPT): continue if not is_mpp and self.config.TEST_FORCE_MPP: continue @@ -1715,33 +1753,33 @@ async def create_routes_for_payment( # for each trampoline forwarder, construct mpp trampoline for trampoline_node_id, trampoline_parts in per_trampoline_channel_amounts.items(): per_trampoline_amount = sum([x[1] for x in trampoline_parts]) - if trampoline_node_id == invoice_pubkey: + if trampoline_node_id == paysession.invoice_pubkey: trampoline_route = None trampoline_onion = None - per_trampoline_secret = payment_secret + per_trampoline_secret = paysession.payment_secret per_trampoline_amount_with_fees = amount_msat - per_trampoline_cltv_delta = min_cltv_expiry + per_trampoline_cltv_delta = paysession.min_cltv_expiry per_trampoline_fees = 0 else: trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion( amount_msat=per_trampoline_amount, - total_msat=final_total_msat, - min_cltv_expiry=min_cltv_expiry, + total_msat=paysession.amount_to_pay, + min_cltv_expiry=paysession.min_cltv_expiry, my_pubkey=self.node_keypair.pubkey, - invoice_pubkey=invoice_pubkey, - invoice_features=invoice_features, + invoice_pubkey=paysession.invoice_pubkey, + invoice_features=paysession.invoice_features, node_id=trampoline_node_id, - r_tags=r_tags, - payment_hash=payment_hash, - payment_secret=payment_secret, + r_tags=paysession.r_tags, + payment_hash=paysession.payment_hash, + payment_secret=paysession.payment_secret, local_height=local_height, - trampoline_fee_level=trampoline_fee_level, - use_two_trampolines=use_two_trampolines, - failed_routes=failed_trampoline_routes) + trampoline_fee_level=paysession.trampoline_fee_level, + use_two_trampolines=paysession.use_two_trampolines, + failed_routes=paysession.failed_trampoline_routes) # node_features is only used to determine is_tlv per_trampoline_secret = os.urandom(32) per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount - self.logger.info(f'created route with trampoline fee level={trampoline_fee_level}') + self.logger.info(f'created route with trampoline fee level={paysession.trampoline_fee_level}') self.logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in trampoline_route]}') self.logger.info(f'per trampoline fees: {per_trampoline_fees}') for chan_id, part_amount_msat in trampoline_parts: @@ -1764,7 +1802,7 @@ async def create_routes_for_payment( self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}') shi = SentHtlcInfo( route=route, - payment_secret_orig=payment_secret, + payment_secret_orig=paysession.payment_secret, payment_secret_bucket=per_trampoline_secret, amount_msat=part_amount_msat_with_fees, bucket_msat=per_trampoline_amount_with_fees, @@ -1786,25 +1824,25 @@ async def create_routes_for_payment( partial( self.create_route_for_payment, amount_msat=part_amount_msat, - invoice_pubkey=invoice_pubkey, - min_cltv_expiry=min_cltv_expiry, - r_tags=r_tags, - invoice_features=invoice_features, + invoice_pubkey=paysession.invoice_pubkey, + min_cltv_expiry=paysession.min_cltv_expiry, + r_tags=paysession.r_tags, + invoice_features=paysession.invoice_features, my_sending_channels=[channel] if is_multichan_mpp else my_active_channels, full_path=full_path, ) ) shi = SentHtlcInfo( route=route, - payment_secret_orig=payment_secret, - payment_secret_bucket=payment_secret, + payment_secret_orig=paysession.payment_secret, + payment_secret_bucket=paysession.payment_secret, amount_msat=part_amount_msat, - bucket_msat=final_total_msat, + bucket_msat=paysession.amount_to_pay, amount_receiver_msat=part_amount_msat, trampoline_fee_level=None, trampoline_route=None, ) - routes.append((shi, min_cltv_expiry, fwd_trampoline_onion)) + routes.append((shi, paysession.min_cltv_expiry, fwd_trampoline_onion)) except NoPathFound: continue for route in routes: @@ -2159,7 +2197,9 @@ def htlc_fulfilled(self, chan: Channel, payment_hash: bytes, htlc_id: int): q = None if shi := self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id)): payment_key = payment_hash + shi.payment_secret_orig - q = self.sent_htlcs_q.get(payment_key) + paysession = self._paysessions.get(payment_key) + if paysession: + q = paysession.sent_htlcs_q if q: htlc_log = HtlcLog( success=True, @@ -2185,7 +2225,9 @@ def htlc_failed( q = None if shi := self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id)): payment_okey = payment_hash + shi.payment_secret_orig - q = self.sent_htlcs_q.get(payment_okey) + paysession = self._paysessions.get(payment_okey) + if paysession: + q = paysession.sent_htlcs_q if q: # detect if it is part of a bucket # if yes, wait until the bucket completely failed diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index f1f69d583..ee6f970a7 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -31,7 +31,7 @@ from electrum.lnchannel import ChannelState, PeerState, Channel from electrum.lnrouter import LNPathFinder, PathEdge, LNPathInconsistent from electrum.channel_db import ChannelDB -from electrum.lnworker import LNWallet, NoPathFound, SentHtlcInfo +from electrum.lnworker import LNWallet, NoPathFound, SentHtlcInfo, PaySession from electrum.lnmsg import encode_msg, decode_msg from electrum import lnmsg from electrum.logging import console_stderr_handler, Logger @@ -168,7 +168,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que self.enable_htlc_settle = True self.enable_htlc_forwarding = True self.received_mpp_htlcs = dict() - self.sent_htlcs_q = defaultdict(asyncio.Queue) + self._paysessions = dict() self.sent_htlcs_info = dict() self.sent_buckets = defaultdict(set) self.final_onion_forwardings = set() @@ -232,18 +232,22 @@ async def stop(self): await self.channel_db.stopped_event.wait() async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: LnAddr, *, full_path=None): - return [r async for r in self.create_routes_for_payment( - amount_msat=amount_msat, - final_total_msat=amount_msat, - invoice_pubkey=decoded_invoice.pubkey.serialize(), - min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(), - r_tags=decoded_invoice.get_routing_info('r'), - invoice_features=decoded_invoice.get_features(), - trampoline_fee_level=0, - failed_trampoline_routes=[], - use_two_trampolines=False, + paysession = PaySession( payment_hash=decoded_invoice.paymenthash, payment_secret=decoded_invoice.payment_secret, + initial_trampoline_fee_level=0, + invoice_features=decoded_invoice.get_features(), + r_tags=decoded_invoice.get_routing_info('r'), + min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(), + amount_to_pay=amount_msat, + invoice_pubkey=decoded_invoice.pubkey.serialize(), + ) + paysession.use_two_trampolines = False + payment_key = decoded_invoice.paymenthash + decoded_invoice.payment_secret + self._paysessions[payment_key] = paysession + return [r async for r in self.create_routes_for_payment( + amount_msat=amount_msat, + paysession=paysession, full_path=full_path)] get_payments = LNWallet.get_payments @@ -854,9 +858,6 @@ async def pay(): _maybe_send_commitment2 = p2.maybe_send_commitment lnaddr2, pay_req2 = self.prepare_invoice(w2) lnaddr1, pay_req1 = self.prepare_invoice(w1) - # create the htlc queues now (side-effecting defaultdict) - q1 = w1.sent_htlcs_q[lnaddr2.paymenthash + lnaddr2.payment_secret] - q2 = w2.sent_htlcs_q[lnaddr1.paymenthash + lnaddr1.payment_secret] # alice sends htlc BUT NOT COMMITMENT_SIGNED p1.maybe_send_commitment = lambda x: None route1 = (await w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2))[0][0].route @@ -901,9 +902,9 @@ async def pay(): p1.maybe_send_commitment(alice_channel) p2.maybe_send_commitment(bob_channel) - htlc_log1 = await q1.get() + htlc_log1 = await w1._paysessions[lnaddr2.paymenthash + lnaddr2.payment_secret].sent_htlcs_q.get() self.assertTrue(htlc_log1.success) - htlc_log2 = await q2.get() + htlc_log2 = await w2._paysessions[lnaddr1.paymenthash + lnaddr1.payment_secret].sent_htlcs_q.get() self.assertTrue(htlc_log2.success) raise PaymentDone() From 98bda60c0101df28c049d2509447d5182cbfa764 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 11 Aug 2023 20:34:19 +0000 Subject: [PATCH 1142/1143] lnworker: move sent_buckets into PaySession --- electrum/lnworker.py | 70 +++++++++++++++++++---------------- electrum/tests/test_lnpeer.py | 14 ++++--- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 9f3eae07a..b08f8b3df 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -667,6 +667,7 @@ def __init__( min_cltv_expiry: int, amount_to_pay: int, # total payment amount final receiver will get invoice_pubkey: bytes, + uses_trampoline: bool, # whether sender uses trampoline or gossip ): assert payment_hash assert payment_secret @@ -684,9 +685,11 @@ def __init__( self.sent_htlcs_q = asyncio.Queue() # type: asyncio.Queue[HtlcLog] self.start_time = time.time() + self.uses_trampoline = uses_trampoline self.trampoline_fee_level = initial_trampoline_fee_level self.failed_trampoline_routes = [] self.use_two_trampolines = True + self._sent_buckets = dict() # psecret_bucket -> (amount_sent, amount_failed) self._amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees) self._nhtlcs_inflight = 0 @@ -742,13 +745,36 @@ async def wait_for_one_htlc_to_resolve(self) -> HtlcLog: raise Exception(f"amount_inflight={self._amount_inflight}, nhtlcs_inflight={self._nhtlcs_inflight}. both should be >= 0 !") return htlc_log - def add_new_htlc(self, sent_htlc_info: SentHtlcInfo) -> SentHtlcInfo: + def add_new_htlc(self, sent_htlc_info: SentHtlcInfo): self._nhtlcs_inflight += 1 self._amount_inflight += sent_htlc_info.amount_receiver_msat if self._amount_inflight > self.amount_to_pay: # safety belts raise Exception(f"amount_inflight={self._amount_inflight} > amount_to_pay={self.amount_to_pay}") - sent_htlc_info = sent_htlc_info._replace(trampoline_fee_level=self.trampoline_fee_level) - return sent_htlc_info + shi = sent_htlc_info + bkey = shi.payment_secret_bucket + # if we sent MPP to a trampoline, add item to sent_buckets + if self.uses_trampoline and shi.amount_msat != shi.bucket_msat: + if bkey not in self._sent_buckets: + self._sent_buckets[bkey] = (0, 0) + amount_sent, amount_failed = self._sent_buckets[bkey] + amount_sent += shi.amount_receiver_msat + self._sent_buckets[bkey] = amount_sent, amount_failed + + def on_htlc_fail_get_fail_amt_to_propagate(self, sent_htlc_info: SentHtlcInfo) -> Optional[int]: + shi = sent_htlc_info + # check sent_buckets if we use trampoline + bkey = shi.payment_secret_bucket + if self.uses_trampoline and bkey in self._sent_buckets: + amount_sent, amount_failed = self._sent_buckets[bkey] + amount_failed += shi.amount_receiver_msat + self._sent_buckets[bkey] = amount_sent, amount_failed + if amount_sent != amount_failed: + self.logger.info('bucket still active...') + return None + self.logger.info('bucket failed') + return amount_sent + # not using trampoline buckets + return shi.amount_receiver_msat def get_outstanding_amount_to_send(self) -> int: return self.amount_to_pay - self._amount_inflight @@ -795,7 +821,6 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv): self._paysessions = dict() # type: Dict[bytes, PaySession] self.sent_htlcs_info = dict() # type: Dict[SentHtlcKey, SentHtlcInfo] - self.sent_buckets = dict() # payment_key -> (amount_sent, amount_failed) # TODO move into PaySession self.received_mpp_htlcs = dict() # type: Dict[bytes, ReceivedMPPStatus] # payment_key -> ReceivedMPPStatus # detect inflight payments @@ -1397,6 +1422,7 @@ async def pay_to_node( min_cltv_expiry=min_cltv_expiry, amount_to_pay=amount_to_pay, invoice_pubkey=node_pubkey, + uses_trampoline=self.uses_trampoline(), ) self.logs[payment_hash.hex()] = log = [] # TODO incl payment_secret in key (re trampoline forwarding) @@ -1417,10 +1443,9 @@ async def pay_to_node( ) # 2. send htlcs async for sent_htlc_info, cltv_delta, trampoline_onion in routes: - sent_htlc_info = paysession.add_new_htlc(sent_htlc_info) await self.pay_to_route( + paysession=paysession, sent_htlc_info=sent_htlc_info, - payment_hash=payment_hash, min_cltv_expiry=cltv_delta, trampoline_onion=trampoline_onion, ) @@ -1466,8 +1491,8 @@ async def pay_to_node( async def pay_to_route( self, *, + paysession: PaySession, sent_htlc_info: SentHtlcInfo, - payment_hash: bytes, min_cltv_expiry: int, trampoline_onion: bytes = None, ) -> None: @@ -1486,21 +1511,14 @@ async def pay_to_route( chan=chan, amount_msat=shi.amount_msat, total_msat=shi.bucket_msat, - payment_hash=payment_hash, + payment_hash=paysession.payment_hash, min_final_cltv_expiry=min_cltv_expiry, payment_secret=shi.payment_secret_bucket, trampoline_onion=trampoline_onion) - key = (payment_hash, short_channel_id, htlc.htlc_id) + key = (paysession.payment_hash, short_channel_id, htlc.htlc_id) self.sent_htlcs_info[key] = shi - payment_key = payment_hash + shi.payment_secret_bucket - # if we sent MPP to a trampoline, add item to sent_buckets - if self.uses_trampoline() and shi.amount_msat != shi.bucket_msat: - if payment_key not in self.sent_buckets: - self.sent_buckets[payment_key] = (0, 0) - amount_sent, amount_failed = self.sent_buckets[payment_key] - amount_sent += shi.amount_receiver_msat - self.sent_buckets[payment_key] = amount_sent, amount_failed + paysession.add_new_htlc(shi) if self.network.path_finder: # add inflight htlcs to liquidity hints self.network.path_finder.update_inflight_htlcs(shi.route, add_htlcs=True) @@ -1807,7 +1825,7 @@ async def create_routes_for_payment( amount_msat=part_amount_msat_with_fees, bucket_msat=per_trampoline_amount_with_fees, amount_receiver_msat=part_amount_msat, - trampoline_fee_level=None, + trampoline_fee_level=paysession.trampoline_fee_level, trampoline_route=trampoline_route, ) routes.append((shi, per_trampoline_cltv_delta, trampoline_onion)) @@ -2232,7 +2250,6 @@ def htlc_failed( # detect if it is part of a bucket # if yes, wait until the bucket completely failed shi = self.sent_htlcs_info[(payment_hash, chan.short_channel_id, htlc_id)] - amount_receiver_msat = shi.amount_receiver_msat route = shi.route if error_bytes: # TODO "decode_onion_error" might raise, catch and maybe blacklist/penalise someone? @@ -2247,18 +2264,9 @@ def htlc_failed( sender_idx = None self.logger.info(f"htlc_failed {failure_message}") - # check sent_buckets if we use trampoline - payment_bkey = payment_hash + shi.payment_secret_bucket - if self.uses_trampoline() and payment_bkey in self.sent_buckets: - amount_sent, amount_failed = self.sent_buckets[payment_bkey] - amount_failed += amount_receiver_msat - self.sent_buckets[payment_bkey] = amount_sent, amount_failed - if amount_sent != amount_failed: - self.logger.info('bucket still active...') - return - self.logger.info('bucket failed') - amount_receiver_msat = amount_sent - + amount_receiver_msat = paysession.on_htlc_fail_get_fail_amt_to_propagate(shi) + if amount_receiver_msat is None: + return if shi.trampoline_route: route = shi.trampoline_route htlc_log = HtlcLog( diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index ee6f970a7..c9ed07fbd 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -241,6 +241,7 @@ async def create_routes_from_invoice(self, amount_msat: int, decoded_invoice: Ln min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(), amount_to_pay=amount_msat, invoice_pubkey=decoded_invoice.pubkey.serialize(), + uses_trampoline=False, ) paysession.use_two_trampolines = False payment_key = decoded_invoice.paymenthash + decoded_invoice.payment_secret @@ -861,6 +862,7 @@ async def pay(): # alice sends htlc BUT NOT COMMITMENT_SIGNED p1.maybe_send_commitment = lambda x: None route1 = (await w1.create_routes_from_invoice(lnaddr2.get_amount_msat(), decoded_invoice=lnaddr2))[0][0].route + paysession1 = w1._paysessions[lnaddr2.paymenthash + lnaddr2.payment_secret] shi1 = SentHtlcInfo( route=route1, payment_secret_orig=lnaddr2.payment_secret, @@ -873,13 +875,14 @@ async def pay(): ) await w1.pay_to_route( sent_htlc_info=shi1, - payment_hash=lnaddr2.paymenthash, + paysession=paysession1, min_cltv_expiry=lnaddr2.get_min_final_cltv_expiry(), ) p1.maybe_send_commitment = _maybe_send_commitment1 # bob sends htlc BUT NOT COMMITMENT_SIGNED p2.maybe_send_commitment = lambda x: None route2 = (await w2.create_routes_from_invoice(lnaddr1.get_amount_msat(), decoded_invoice=lnaddr1))[0][0].route + paysession2 = w2._paysessions[lnaddr1.paymenthash + lnaddr1.payment_secret] shi2 = SentHtlcInfo( route=route2, payment_secret_orig=lnaddr1.payment_secret, @@ -892,7 +895,7 @@ async def pay(): ) await w2.pay_to_route( sent_htlc_info=shi2, - payment_hash=lnaddr1.paymenthash, + paysession=paysession2, min_cltv_expiry=lnaddr1.get_min_final_cltv_expiry(), ) p2.maybe_send_commitment = _maybe_send_commitment2 @@ -902,9 +905,9 @@ async def pay(): p1.maybe_send_commitment(alice_channel) p2.maybe_send_commitment(bob_channel) - htlc_log1 = await w1._paysessions[lnaddr2.paymenthash + lnaddr2.payment_secret].sent_htlcs_q.get() + htlc_log1 = await paysession1.sent_htlcs_q.get() self.assertTrue(htlc_log1.success) - htlc_log2 = await w2._paysessions[lnaddr1.paymenthash + lnaddr1.payment_secret].sent_htlcs_q.get() + htlc_log2 = await paysession2.sent_htlcs_q.get() self.assertTrue(htlc_log2.success) raise PaymentDone() @@ -1603,9 +1606,10 @@ async def f(): trampoline_fee_level=None, trampoline_route=None, ) + paysession = w1._paysessions[lnaddr.paymenthash + lnaddr.payment_secret] pay = w1.pay_to_route( sent_htlc_info=shi, - payment_hash=lnaddr.paymenthash, + paysession=paysession, min_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), ) await asyncio.gather(pay, p1._message_loop(), p2._message_loop(), p1.htlc_switch(), p2.htlc_switch()) From 13864f7abe103e0b290f97324d4d55b983a57b84 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 11 Aug 2023 21:57:33 +0000 Subject: [PATCH 1143/1143] lnworker: clear paysessions dict --- electrum/lnworker.py | 134 ++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b08f8b3df..e411be0b6 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -693,6 +693,7 @@ def __init__( self._amount_inflight = 0 # what we sent in htlcs (that receiver gets, without fees) self._nhtlcs_inflight = 0 + self.is_active = True def diagnostic_name(self): pkey = sha256(self.payment_key) @@ -779,6 +780,14 @@ def on_htlc_fail_get_fail_amt_to_propagate(self, sent_htlc_info: SentHtlcInfo) - def get_outstanding_amount_to_send(self) -> int: return self.amount_to_pay - self._amount_inflight + def can_be_deleted(self) -> bool: + if self.is_active: + return False + # note: no one is consuming from sent_htlcs_q anymore + nhtlcs_resolved = self.sent_htlcs_q.qsize() + assert nhtlcs_resolved <= self._nhtlcs_inflight + return nhtlcs_resolved == self._nhtlcs_inflight + class LNWallet(LNWorker): @@ -1412,7 +1421,7 @@ async def pay_to_node( raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'') payment_key = payment_hash + payment_secret - #assert payment_key not in self._paysessions # FIXME + assert payment_key not in self._paysessions self._paysessions[payment_key] = paysession = PaySession( payment_hash=payment_hash, payment_secret=payment_secret, @@ -1429,65 +1438,70 @@ async def pay_to_node( # when encountering trampoline forwarding difficulties in the legacy case, we # sometimes need to fall back to a single trampoline forwarder, at the expense # of privacy - while True: - if (amount_to_send := paysession.get_outstanding_amount_to_send()) > 0: - # 1. create a set of routes for remaining amount. - # note: path-finding runs in a separate thread so that we don't block the asyncio loop - # graph updates might occur during the computation - routes = self.create_routes_for_payment( - paysession=paysession, - amount_msat=amount_to_send, - full_path=full_path, - fwd_trampoline_onion=fwd_trampoline_onion, - channels=channels, - ) - # 2. send htlcs - async for sent_htlc_info, cltv_delta, trampoline_onion in routes: - await self.pay_to_route( + try: + while True: + if (amount_to_send := paysession.get_outstanding_amount_to_send()) > 0: + # 1. create a set of routes for remaining amount. + # note: path-finding runs in a separate thread so that we don't block the asyncio loop + # graph updates might occur during the computation + routes = self.create_routes_for_payment( paysession=paysession, - sent_htlc_info=sent_htlc_info, - min_cltv_expiry=cltv_delta, - trampoline_onion=trampoline_onion, + amount_msat=amount_to_send, + full_path=full_path, + fwd_trampoline_onion=fwd_trampoline_onion, + channels=channels, ) - # invoice_status is triggered in self.set_invoice_status when it actually changes. - # It is also triggered here to update progress for a lightning payment in the GUI - # (e.g. attempt counter) - util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) - # 3. await a queue - htlc_log = await paysession.wait_for_one_htlc_to_resolve() # TODO maybe wait a bit, more failures might come - log.append(htlc_log) - if htlc_log.success: - if self.network.path_finder: - # TODO: report every route to liquidity hints for mpp - # in the case of success, we report channels of the - # route as being able to send the same amount in the future, - # as we assume to not know the capacity - self.network.path_finder.update_liquidity_hints(htlc_log.route, htlc_log.amount_msat) - # remove inflight htlcs from liquidity hints - self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False) - return - # htlc failed - if (attempts is not None and len(log) >= attempts) or (attempts is None and time.time() - paysession.start_time > self.PAYMENT_TIMEOUT): - raise PaymentFailure('Giving up after %d attempts'%len(log)) - # if we get a tmp channel failure, it might work to split the amount and try more routes - # if we get a channel update, we might retry the same route and amount - route = htlc_log.route - sender_idx = htlc_log.sender_idx - erring_node_id = route[sender_idx].node_id - failure_msg = htlc_log.failure_msg - code, data = failure_msg.code, failure_msg.data - self.logger.info(f"UPDATE_FAIL_HTLC. code={repr(code)}. " - f"decoded_data={failure_msg.decode_data()}. data={data.hex()!r}") - self.logger.info(f"error reported by {erring_node_id.hex()}") - if code == OnionFailureCode.MPP_TIMEOUT: - raise PaymentFailure(failure_msg.code_name()) - # trampoline - if self.uses_trampoline(): - paysession.handle_failed_trampoline_htlc( - htlc_log=htlc_log, failure_msg=failure_msg) - else: - self.handle_error_code_from_failed_htlc( - route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat) + # 2. send htlcs + async for sent_htlc_info, cltv_delta, trampoline_onion in routes: + await self.pay_to_route( + paysession=paysession, + sent_htlc_info=sent_htlc_info, + min_cltv_expiry=cltv_delta, + trampoline_onion=trampoline_onion, + ) + # invoice_status is triggered in self.set_invoice_status when it actually changes. + # It is also triggered here to update progress for a lightning payment in the GUI + # (e.g. attempt counter) + util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) + # 3. await a queue + htlc_log = await paysession.wait_for_one_htlc_to_resolve() # TODO maybe wait a bit, more failures might come + log.append(htlc_log) + if htlc_log.success: + if self.network.path_finder: + # TODO: report every route to liquidity hints for mpp + # in the case of success, we report channels of the + # route as being able to send the same amount in the future, + # as we assume to not know the capacity + self.network.path_finder.update_liquidity_hints(htlc_log.route, htlc_log.amount_msat) + # remove inflight htlcs from liquidity hints + self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False) + return + # htlc failed + if (attempts is not None and len(log) >= attempts) or (attempts is None and time.time() - paysession.start_time > self.PAYMENT_TIMEOUT): + raise PaymentFailure('Giving up after %d attempts'%len(log)) + # if we get a tmp channel failure, it might work to split the amount and try more routes + # if we get a channel update, we might retry the same route and amount + route = htlc_log.route + sender_idx = htlc_log.sender_idx + erring_node_id = route[sender_idx].node_id + failure_msg = htlc_log.failure_msg + code, data = failure_msg.code, failure_msg.data + self.logger.info(f"UPDATE_FAIL_HTLC. code={repr(code)}. " + f"decoded_data={failure_msg.decode_data()}. data={data.hex()!r}") + self.logger.info(f"error reported by {erring_node_id.hex()}") + if code == OnionFailureCode.MPP_TIMEOUT: + raise PaymentFailure(failure_msg.code_name()) + # trampoline + if self.uses_trampoline(): + paysession.handle_failed_trampoline_htlc( + htlc_log=htlc_log, failure_msg=failure_msg) + else: + self.handle_error_code_from_failed_htlc( + route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat) + finally: + paysession.is_active = False + if paysession.can_be_deleted(): + self._paysessions.pop(payment_key) async def pay_to_route( self, *, @@ -2225,6 +2239,8 @@ def htlc_fulfilled(self, chan: Channel, payment_hash: bytes, htlc_id: int): amount_msat=shi.amount_receiver_msat, trampoline_fee_level=shi.trampoline_fee_level) q.put_nowait(htlc_log) + if paysession.can_be_deleted(): + self._paysessions.pop(payment_key) else: key = payment_hash.hex() self.set_invoice_status(key, PR_PAID) @@ -2278,6 +2294,8 @@ def htlc_failed( sender_idx=sender_idx, trampoline_fee_level=shi.trampoline_fee_level) q.put_nowait(htlc_log) + if paysession.can_be_deleted(): + self._paysessions.pop(payment_okey) else: self.logger.info(f"received unknown htlc_failed, probably from previous session") key = payment_hash.hex()