From 7eb7eb8674da784c814baa0d3981cfc44e37a310 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 31 Oct 2019 06:25:32 +0100 Subject: [PATCH] add support for manual coinjoins --- electrum/gui/qt/transaction_dialog.py | 64 +++++++++++++++++++++------ electrum/transaction.py | 22 +++++++++ 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 35b470237..066c8b204 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -115,7 +115,15 @@ class TxDialog(QDialog, MessageBoxMixin): self.add_tx_stats(vbox) vbox.addSpacing(10) - self.add_io(vbox) + + self.inputs_header = QLabel() + vbox.addWidget(self.inputs_header) + self.inputs_textedit = QTextEditWithDefaultSize() + vbox.addWidget(self.inputs_textedit) + self.outputs_header = QLabel() + vbox.addWidget(self.outputs_header) + self.outputs_textedit = QTextEditWithDefaultSize() + vbox.addWidget(self.outputs_textedit) self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) @@ -123,9 +131,6 @@ class TxDialog(QDialog, MessageBoxMixin): self.broadcast_button = b = QPushButton(_("Broadcast")) b.clicked.connect(self.do_broadcast) - self.merge_sigs_button = b = QPushButton(_("Merge sigs from")) - b.clicked.connect(self.merge_sigs) - self.save_button = b = QPushButton(_("Save")) save_button_disabled = not tx.is_complete() b.setDisabled(save_button_disabled) @@ -154,10 +159,22 @@ class TxDialog(QDialog, MessageBoxMixin): self.export_actions_button.setMenu(export_actions_menu) self.export_actions_button.setPopupMode(QToolButton.InstantPopup) + partial_tx_actions_menu = QMenu() + ptx_merge_sigs_action = QAction(_("Merge signatures from"), self) + ptx_merge_sigs_action.triggered.connect(self.merge_sigs) + partial_tx_actions_menu.addAction(ptx_merge_sigs_action) + ptx_join_txs_action = QAction(_("Join inputs/outputs"), self) + ptx_join_txs_action.triggered.connect(self.join_tx_with_another) + partial_tx_actions_menu.addAction(ptx_join_txs_action) + self.partial_tx_actions_button = QToolButton() + self.partial_tx_actions_button.setText(_("Combine with other")) + self.partial_tx_actions_button.setMenu(partial_tx_actions_menu) + self.partial_tx_actions_button.setPopupMode(QToolButton.InstantPopup) + # Action buttons self.buttons = [] if isinstance(tx, PartialTransaction): - self.buttons.append(self.merge_sigs_button) + self.buttons.append(self.partial_tx_actions_button) self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button] # Transaction sharing buttons self.sharing_buttons = [self.export_actions_button, self.save_button] @@ -253,7 +270,9 @@ class TxDialog(QDialog, MessageBoxMixin): def merge_sigs(self): if not isinstance(self.tx, PartialTransaction): return - text = text_dialog(self, _('Input raw transaction'), _("Transaction:"), _("Load transaction")) + text = text_dialog(self, _('Input raw transaction'), + _("Transaction to merge signatures from") + ":", + _("Load transaction")) if not text: return tx = self.main_window.tx_from_text(text) @@ -266,7 +285,26 @@ class TxDialog(QDialog, MessageBoxMixin): return self.update() + def join_tx_with_another(self): + if not isinstance(self.tx, PartialTransaction): + return + text = text_dialog(self, _('Input raw transaction'), + _("Transaction to join with") + " (" + _("add inputs and outputs") + "):", + _("Load transaction")) + if not text: + return + tx = self.main_window.tx_from_text(text) + if not tx: + return + try: + self.tx.join_with_other_psbt(tx) + except Exception as e: + self.show_error(_("Error joining partial transactions") + ":\n" + repr(e)) + return + self.update() + def update(self): + self.update_io() desc = self.desc base_unit = self.main_window.base_unit() format_amount = self.main_window.format_amount @@ -326,8 +364,8 @@ class TxDialog(QDialog, MessageBoxMixin): self.size_label.setText(size_str) run_hook('transaction_dialog_update', self) - def add_io(self, vbox): - vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs()))) + def update_io(self): + self.inputs_header.setText(_("Inputs") + ' (%d)'%len(self.tx.inputs())) ext = QTextCharFormat() rec = QTextCharFormat() rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True))) @@ -349,7 +387,8 @@ class TxDialog(QDialog, MessageBoxMixin): def format_amount(amt): return self.main_window.format_amount(amt, whitespaces=True) - i_text = QTextEditWithDefaultSize() + i_text = self.inputs_textedit + i_text.clear() i_text.setFont(QFont(MONOSPACE_FONT)) i_text.setReadOnly(True) cursor = i_text.textCursor() @@ -368,9 +407,9 @@ class TxDialog(QDialog, MessageBoxMixin): cursor.insertText(format_amount(txin.value_sats()), ext) cursor.insertBlock() - vbox.addWidget(i_text) - vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs()))) - o_text = QTextEditWithDefaultSize() + 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) cursor = o_text.textCursor() @@ -381,7 +420,6 @@ class TxDialog(QDialog, MessageBoxMixin): cursor.insertText('\t', ext) cursor.insertText(format_amount(v), ext) cursor.insertBlock() - vbox.addWidget(o_text) def add_tx_stats(self, vbox): hbox_stats = QHBoxLayout() diff --git a/electrum/transaction.py b/electrum/transaction.py index b0bb01da2..0c9d00b62 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -36,6 +36,7 @@ from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, Callable, List, Dict, Set, TYPE_CHECKING) from collections import defaultdict from enum import IntEnum +import itertools from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node @@ -1537,6 +1538,27 @@ class PartialTransaction(Transaction): txout.combine_with_other_txout(other_txout) self.invalidate_ser_cache() + def join_with_other_psbt(self, other_tx: 'PartialTransaction') -> None: + """Adds inputs and outputs from other_tx into this one.""" + if not isinstance(other_tx, PartialTransaction): + raise Exception('Can only join partial transactions.') + # make sure there are no duplicate prevouts + prevouts = set() + for txin in itertools.chain(self.inputs(), other_tx.inputs()): + prevout_str = txin.prevout.to_str() + if prevout_str in prevouts: + raise Exception(f"Duplicate inputs! " + f"Transactions that spend the same prevout cannot be joined.") + prevouts.add(prevout_str) + # copy global PSBT section + self.xpubs.update(other_tx.xpubs) + self._unknown.update(other_tx._unknown) + # copy and add inputs and outputs + self.add_inputs(list(other_tx.inputs())) + self.add_outputs(list(other_tx.outputs())) + self.remove_signatures() + self.invalidate_ser_cache() + def inputs(self) -> Sequence[PartialTxInput]: return self._inputs