add support for manual coinjoins
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user