add support for manual coinjoins
This commit is contained in:
@@ -115,7 +115,15 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
|
|
||||||
self.add_tx_stats(vbox)
|
self.add_tx_stats(vbox)
|
||||||
vbox.addSpacing(10)
|
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"))
|
self.sign_button = b = QPushButton(_("Sign"))
|
||||||
b.clicked.connect(self.sign)
|
b.clicked.connect(self.sign)
|
||||||
@@ -123,9 +131,6 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
self.broadcast_button = b = QPushButton(_("Broadcast"))
|
self.broadcast_button = b = QPushButton(_("Broadcast"))
|
||||||
b.clicked.connect(self.do_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"))
|
self.save_button = b = QPushButton(_("Save"))
|
||||||
save_button_disabled = not tx.is_complete()
|
save_button_disabled = not tx.is_complete()
|
||||||
b.setDisabled(save_button_disabled)
|
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.setMenu(export_actions_menu)
|
||||||
self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
|
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
|
# Action buttons
|
||||||
self.buttons = []
|
self.buttons = []
|
||||||
if isinstance(tx, PartialTransaction):
|
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]
|
self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button]
|
||||||
# Transaction sharing buttons
|
# Transaction sharing buttons
|
||||||
self.sharing_buttons = [self.export_actions_button, self.save_button]
|
self.sharing_buttons = [self.export_actions_button, self.save_button]
|
||||||
@@ -253,7 +270,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
def merge_sigs(self):
|
def merge_sigs(self):
|
||||||
if not isinstance(self.tx, PartialTransaction):
|
if not isinstance(self.tx, PartialTransaction):
|
||||||
return
|
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:
|
if not text:
|
||||||
return
|
return
|
||||||
tx = self.main_window.tx_from_text(text)
|
tx = self.main_window.tx_from_text(text)
|
||||||
@@ -266,7 +285,26 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
return
|
return
|
||||||
self.update()
|
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):
|
def update(self):
|
||||||
|
self.update_io()
|
||||||
desc = self.desc
|
desc = self.desc
|
||||||
base_unit = self.main_window.base_unit()
|
base_unit = self.main_window.base_unit()
|
||||||
format_amount = self.main_window.format_amount
|
format_amount = self.main_window.format_amount
|
||||||
@@ -326,8 +364,8 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
self.size_label.setText(size_str)
|
self.size_label.setText(size_str)
|
||||||
run_hook('transaction_dialog_update', self)
|
run_hook('transaction_dialog_update', self)
|
||||||
|
|
||||||
def add_io(self, vbox):
|
def update_io(self):
|
||||||
vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs())))
|
self.inputs_header.setText(_("Inputs") + ' (%d)'%len(self.tx.inputs()))
|
||||||
ext = QTextCharFormat()
|
ext = QTextCharFormat()
|
||||||
rec = QTextCharFormat()
|
rec = QTextCharFormat()
|
||||||
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
|
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
|
||||||
@@ -349,7 +387,8 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
def format_amount(amt):
|
def format_amount(amt):
|
||||||
return self.main_window.format_amount(amt, whitespaces=True)
|
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.setFont(QFont(MONOSPACE_FONT))
|
||||||
i_text.setReadOnly(True)
|
i_text.setReadOnly(True)
|
||||||
cursor = i_text.textCursor()
|
cursor = i_text.textCursor()
|
||||||
@@ -368,9 +407,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
cursor.insertText(format_amount(txin.value_sats()), ext)
|
cursor.insertText(format_amount(txin.value_sats()), ext)
|
||||||
cursor.insertBlock()
|
cursor.insertBlock()
|
||||||
|
|
||||||
vbox.addWidget(i_text)
|
self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs()))
|
||||||
vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs())))
|
o_text = self.outputs_textedit
|
||||||
o_text = QTextEditWithDefaultSize()
|
o_text.clear()
|
||||||
o_text.setFont(QFont(MONOSPACE_FONT))
|
o_text.setFont(QFont(MONOSPACE_FONT))
|
||||||
o_text.setReadOnly(True)
|
o_text.setReadOnly(True)
|
||||||
cursor = o_text.textCursor()
|
cursor = o_text.textCursor()
|
||||||
@@ -381,7 +420,6 @@ class TxDialog(QDialog, MessageBoxMixin):
|
|||||||
cursor.insertText('\t', ext)
|
cursor.insertText('\t', ext)
|
||||||
cursor.insertText(format_amount(v), ext)
|
cursor.insertText(format_amount(v), ext)
|
||||||
cursor.insertBlock()
|
cursor.insertBlock()
|
||||||
vbox.addWidget(o_text)
|
|
||||||
|
|
||||||
def add_tx_stats(self, vbox):
|
def add_tx_stats(self, vbox):
|
||||||
hbox_stats = QHBoxLayout()
|
hbox_stats = QHBoxLayout()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable,
|
|||||||
Callable, List, Dict, Set, TYPE_CHECKING)
|
Callable, List, Dict, Set, TYPE_CHECKING)
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
import itertools
|
||||||
|
|
||||||
from . import ecc, bitcoin, constants, segwit_addr, bip32
|
from . import ecc, bitcoin, constants, segwit_addr, bip32
|
||||||
from .bip32 import BIP32Node
|
from .bip32 import BIP32Node
|
||||||
@@ -1537,6 +1538,27 @@ class PartialTransaction(Transaction):
|
|||||||
txout.combine_with_other_txout(other_txout)
|
txout.combine_with_other_txout(other_txout)
|
||||||
self.invalidate_ser_cache()
|
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]:
|
def inputs(self) -> Sequence[PartialTxInput]:
|
||||||
return self._inputs
|
return self._inputs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user