diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8ee7704bd85f..c99ffae2f340 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -64,7 +64,7 @@ from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice from electrum.transaction import (Transaction, PartialTxInput, - PartialTransaction, PartialTxOutput) + PartialTransaction, PartialTxOutput, PayjoinTransaction) from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, sweep_preparations, InternalAddressCorruption) @@ -80,7 +80,7 @@ from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit from .qrcodewidget import QRCodeWidget, QRDialog from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit -from .transaction_dialog import show_transaction +from .transaction_dialog import show_transaction, PreviewTxDialog from .fee_slider import FeeSlider, FeeComboBox from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog, WindowModalDialog, ChoicesLayout, HelpLabel, Buttons, @@ -95,7 +95,6 @@ from .update_checker import UpdateCheck, UpdateCheckThread from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog -from .transaction_dialog import PreviewTxDialog if TYPE_CHECKING: from . import ElectrumGui @@ -1566,6 +1565,7 @@ def do_save_invoice(self): def do_pay(self): invoice = self.read_invoice() + print('main -invoice: ', invoice)# if not invoice: return self.wallet.save_invoice(invoice) @@ -1585,7 +1585,7 @@ def do_pay_invoice(self, invoice: 'Invoice'): self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat()) elif invoice.type == PR_TYPE_ONCHAIN: assert isinstance(invoice, OnchainInvoice) - self.pay_onchain_dialog(self.get_coins(), invoice.outputs) + self.pay_onchain_dialog(self.get_coins(), invoice.outputs, payjoin=invoice.bip78_payjoin) else: raise Exception('unknown invoice type') @@ -1605,6 +1605,7 @@ def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]: def pay_onchain_dialog(self, inputs: Sequence[PartialTxInput], outputs: List[PartialTxOutput], *, + payjoin=None, external_keypairs=None) -> None: # trustedcoin requires this if run_hook('abort_send', self): @@ -1632,7 +1633,8 @@ def pay_onchain_dialog(self, inputs: Sequence[PartialTxInput], # shortcut to advanced preview (after "enough funds" check!) if self.config.get('advanced_preview'): self.preview_tx_dialog(make_tx=make_tx, - external_keypairs=external_keypairs) + external_keypairs=external_keypairs, + payjoin=payjoin) return cancelled, is_send, password, tx = d.run() @@ -1646,11 +1648,13 @@ def sign_done(success): external_keypairs=external_keypairs) else: self.preview_tx_dialog(make_tx=make_tx, - external_keypairs=external_keypairs) + external_keypairs=external_keypairs, + payjoin=payjoin) - def preview_tx_dialog(self, *, make_tx, external_keypairs=None): + def preview_tx_dialog(self, *, make_tx, external_keypairs=None, payjoin=None): + print('preview_tx_dialog: ', payjoin)# d = PreviewTxDialog(make_tx=make_tx, external_keypairs=external_keypairs, - window=self) + window=self, payjoin=payjoin) d.show() def broadcast_or_show(self, tx: Transaction): @@ -1726,7 +1730,46 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.on_error) + +<<<<<<< Updated upstream +======= + def exchange_psbt_http(self, payjoin): + """ """ + import requests, copy + assert payjoin.is_complete() + + print(payjoin.to_json()) + print(payjoin.serialize_as_base64()) + + for txin in payjoin.inputs(): + print(txin) + print(txin.utxo) + print(txin.utxo.outputs()) + print + self.utxo.outputs()[self.prevout.out_idx] + + + + + url = 'https://testnet.demo.btcpayserver.org/BTC/pj' + payload = payjoin.serialize_as_base64() + headers = {'content-type': 'text/plain', + 'content-length': str(len(payload)) + } + print(headers) + """ + try: + r = requests.post(url, data=payload, headers=headers) + except: + pass + + print(payload) + print(r.status_code) + print(r.headers) + print(r.text) + """ +>>>>>>> Stashed changes def mktx_for_open_channel(self, funding_sat): coins = self.get_coins(nonlocal_only=True) make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel(coins=coins, @@ -1869,7 +1912,7 @@ def pay_to_URI(self, URI): if not URI: return try: - out = util.parse_URI(URI, self.on_pr) + out = util.parse_bip21_uri(URI, self.on_pr) except InvalidBitcoinURI as e: self.show_error(_("Error parsing URI") + f":\n{e}") return @@ -1895,7 +1938,21 @@ def pay_to_URI(self, URI): if amount: self.amount_e.setAmount(amount) self.amount_e.textEdited.emit("") - + self._set_payjoin_availability(self.payto_URI) + + def _set_payjoin_availability(self, out): + """ """ + pj = out.get('pj') + pjos = out.get('pjos') + print('pj in main:', pj)# + print('pjos in main:', pjos)# + if pj: + self.pj_available = True + self.pj = pj + self.pjos = pjos + else: + self.pj_available = False + print('pj_available in main:', self.pj_available)# def do_clear(self): self.max_button.setChecked(False) @@ -1903,6 +1960,9 @@ def do_clear(self): self.payto_URI = None self.payto_e.is_pr = False self.set_onchain(False) + self.pj_available = False + self.pj = None + self.pjos = None for e in [self.payto_e, self.message_e, self.amount_e]: e.setText('') e.setFrozen(False) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 2a2ab46cb7ee..8d99f73b72be 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -45,7 +45,8 @@ 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, PayjoinTransaction, + PayJoinProposalValidationException, PayJoinExchangeException) from electrum.logging import get_logger from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, @@ -92,7 +93,7 @@ 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): + def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None, payjoin=None): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' @@ -105,6 +106,10 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.config = parent.config self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved + + self.payjoin = PayjoinTransaction(payjoin) + self.payjoin_finished = False + self.saved = False self.desc = desc self.setMinimumWidth(950) @@ -217,7 +222,46 @@ def set_tx(self, tx: 'Transaction'): # note: this might fetch prev txs over the network. tx.add_info_from_wallet(self.wallet) - def do_broadcast(self): + def do_payjoin(self) -> None: + def sign_done(success): + self.payjoin_finished = True + if self.tx.get_fee_rate() < self.payjoin._minfeerate: + self.set_tx(original_tx) + _logger.warning("The receiver used a too low fee rate.") + self.show_error( + _("Error creating a payjoin") + ":\n" + + _("The receiver used a too low fee rate") + "\n" + + _("Sending the original transaction")) + self.update() + self.main_window.pop_top_level_window(self) + self.do_broadcast() + + self.main_window.push_top_level_window(self) + original_tx = copy.deepcopy(self.tx) + _logger.info(f"Starting Payjoin Session") + try: + self.payjoin.set_tx(self.tx) + print('tx1: ',self.tx.to_json())# + self.payjoin.do_payjoin() + print('tx2: ', self.payjoin.payjoin_proposal.to_json()) # + self.payjoin.payjoin_proposal.add_info_from_wallet(self.wallet) + print('tx3: ', self.payjoin.payjoin_proposal.to_json()) # + self.payjoin.validate_payjoin_proposal() + except (PayJoinProposalValidationException, PayJoinExchangeException) as e: + _logger.warning(repr(e)) + self.payjoin_cb.setChecked(False) + self.show_error(_("Error creating a payjoin") + ":\n" + str(e) + "\n" + + _("Sending the original transaction")) + self.do_broadcast() + return + tx = self.payjoin.payjoin_proposal + self.set_tx(tx) + self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs) + + def do_broadcast(self) -> None: + if self.payjoin_cb.isChecked() and self.payjoin.is_available() and not self.payjoin_finished: + self.do_payjoin() + return self.main_window.push_top_level_window(self) try: self.main_window.broadcast_transaction(self.tx) @@ -641,6 +685,12 @@ def add_tx_stats(self, vbox): self.block_height_label = TxDetailLabel() vbox_right.addWidget(self.block_height_label) + + self.payjoin_cb = QCheckBox(_('PayJoin')) + self.payjoin_cb.setChecked(bool(self.config.get('use_payjoin', True))) + vbox_right.addWidget(self.payjoin_cb) + #visibility + vbox_right.addStretch(1) hbox_stats.addLayout(vbox_right, 50) @@ -653,6 +703,7 @@ def add_tx_stats(self, vbox): # set visibility after parenting can be determined by Qt self.rbf_label.setVisible(self.finalized) self.rbf_cb.setVisible(not self.finalized) + self.payjoin_cb.setVisible(self.payjoin.is_available()) self.locktime_final_label.setVisible(self.finalized) self.locktime_setter_widget.setVisible(not self.finalized) @@ -687,10 +738,11 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if class PreviewTxDialog(BaseTxDialog, TxEditor): - def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow'): + def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow', payjoin): TxEditor.__init__(self, window=window, make_tx=make_tx, is_sweep=bool(external_keypairs)) BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False, - finalized=False, external_keypairs=external_keypairs) + finalized=False, external_keypairs=external_keypairs, payjoin=payjoin) + print('pj_available in Preview dialog: ', payjoin)# BlockingWaitingDialog(window, _("Preparing transaction..."), lambda: self.update_tx(fallback_to_zero_fee=True)) self.update() diff --git a/electrum/invoices.py b/electrum/invoices.py index c7879fc7e51b..eb76258fd561 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -120,6 +120,7 @@ class OnchainInvoice(Invoice): outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: List[PartialTxOutput] bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str] requestor = attr.ib(type=str, kw_only=True) # type: Optional[str] + bip78_payjoin = attr.ib(type=Dict, kw_only=True, default=None) # type: Optional[Dict] def get_address(self) -> str: assert len(self.outputs) == 1 diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 56b8b62a2f2e..43f2b70046c6 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -40,7 +40,7 @@ from electrum.i18n import _ from electrum.logging import Logger -from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled, UserFacingException +from electrum.util import parse_bip21_uri, InvalidBitcoinURI, UserCancelled, UserFacingException from electrum.plugin import hook, DeviceUnpairableError from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase diff --git a/electrum/transaction.py b/electrum/transaction.py index 42f45a2c38f1..80b1d59b7ac7 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -38,6 +38,10 @@ from enum import IntEnum import itertools import binascii +import requests +import copy + +#from .lnutil import make_htlc_tx_output from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node @@ -241,6 +245,11 @@ def witness_elements(self)-> Sequence[bytes]: n = vds.read_compact_size() return list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) + def is_segwit(self, *, guess_for_address=False) -> bool: + if self.witness not in (b'\x00', b'', None): + return True + return False + class BCDataStream(object): """Workalike python implementation of Bitcoin's CDataStream class.""" @@ -1151,9 +1160,11 @@ def utxo(self): @utxo.setter def utxo(self, value: Optional[Transaction]): - self._utxo = value - self.validate_data() - self.ensure_there_is_only_one_utxo() + if value: + self._utxo = value + self.validate_data() + self._witness_utxo = None + self.ensure_there_is_only_one_utxo() @property def witness_utxo(self): @@ -1161,9 +1172,11 @@ def witness_utxo(self): @witness_utxo.setter def witness_utxo(self, value: Optional[TxOutput]): - self._witness_utxo = value - self.validate_data() - self.ensure_there_is_only_one_utxo() + if value: + self._witness_utxo = value + self.validate_data() + self._utxo = None + self.ensure_there_is_only_one_utxo() def to_json(self): d = super().to_json() @@ -1345,8 +1358,14 @@ def is_complete(self) -> bool: return True if self.is_coinbase_input(): return True - if self.script_sig is not None and not Transaction.is_segwit_input(self): + if self.script_sig is not None and not self.is_segwit(): + return True + if self.witness is not None and self.is_segwit(): + return True + """ + if self.witness is not None and Transaction.is_segwit_input(self): return True + """ signatures = list(self.part_sigs.values()) s = len(signatures) # note: The 'script_type' field is currently only set by the wallet, @@ -1370,7 +1389,7 @@ def clear_fields_when_finalized(): self.redeem_script = None self.witness_script = None - if self.script_sig is not None and self.witness is not None: + if self.script_sig is not None or self.witness is not None: clear_fields_when_finalized() return # already finalized if self.is_complete(): @@ -1447,12 +1466,31 @@ def calc_if_p2sh_segwit_now(): self._is_p2sh_segwit = calc_if_p2sh_segwit_now() return self._is_p2sh_segwit + def is_segwit(self, *, guess_for_address=False) -> bool: + if super().is_segwit(): + return True + if self.is_native_segwit() or self.is_p2sh_segwit(): + return True + if self.is_native_segwit() is False and self.is_p2sh_segwit() is False: + return False + if self.witness_script: + return True + _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) + def already_has_some_signatures(self) -> bool: """Returns whether progress has been made towards completing this input.""" return (self.part_sigs or self.script_sig is not None or self.witness is not None) + def remove_signature(self): + self.part_sigs = {} + self.script_sig = None + self.witness = None + class PartialTxOutput(TxOutput, PSBTSection): def __init__(self, *args, **kwargs): @@ -1783,6 +1821,12 @@ def get_fee(self) -> Optional[int]: except MissingTxInputAmount: return None + def get_fee_rate(self) -> Optional[int]: + if self.get_fee() is None: + return None + return self.get_fee() / self.estimated_size() + + def serialize_preimage(self, txin_index: int, *, bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: nVersion = int_to_hex(self.version, 4) @@ -1876,6 +1920,10 @@ def _serialize_as_base64(self) -> str: raw_bytes = self.serialize_as_bytes() return base64.b64encode(raw_bytes).decode('ascii') + def serialize_as_base64(self, force_psbt = True) -> str: + raw_bytes = self.serialize_as_bytes(force_psbt = force_psbt) + return base64.b64encode(raw_bytes).decode('ascii') + def update_signatures(self, signatures: Sequence[str]): """Add new signatures to a transaction @@ -1984,6 +2032,195 @@ def remove_signatures(self): self.invalidate_ser_cache() +class PayJoinExchangeException(Exception): + pass + +class PayJoinProposalValidationException(Exception): + pass + +class PayjoinTransaction(): + VERSION = 1 + VSIZE_SENDER_TYPE ={'p2wpkh':68, + 'p2pkh':148, + 'p2wpkh - p2sh':91 + } + + def __init__(self, payjoin_link=None): + self.pj = payjoin_link.get('pj') if payjoin_link else None + self.pjos = payjoin_link.get('pjos') if payjoin_link else None + + self._additionalfeeoutputindex = None + self._maxadditionalfeecontribution = None + self._minfeerate = None + self._disableoutputsubstitution = None + + self.payjoin_original = None + self.payjoin_proposal = None + + def is_available(self) -> bool: + return self.pj is not None and isinstance(self.pj, str) + + def set_tx(self, tx: PartialTransaction) -> None: + self.payjoin_original = copy.deepcopy(tx) + self.payjoin_proposal_received = False + self.define_sender_params() + self.prepare_payjoin_original() + + def define_sender_params(self): + def examine_change_output(): + for i, txout in enumerate(self.payjoin_original.outputs()): + if (txout.is_mine and txout.is_change): + return i + def examine_input_vsize(tx): + txin = tx._inputs[0] + return self.VSIZE_SENDER_TYPE.get(txin.script_type) + # set min fee rate to an int that is rounded down + self._minfeerate = int(self.payjoin_original.get_fee_rate()) + self._disableoutputsubstitution = 'true' if self.pjos == 0 else 'false' + self._additionalfeeoutputindex = examine_change_output() + # use a fee rate that is rounded to a full int + self.original_psbt_fee_rate = round(self.payjoin_original.get_fee_rate()) + self.vsize_input_type = examine_input_vsize(self.payjoin_original) + self._maxadditionalfeecontribution = int(self.original_psbt_fee_rate * self.vsize_input_type) + + def prepare_payjoin_original(self): + assert not self.payjoin_proposal_received + assert self.payjoin_original.is_complete() + self.payjoin_original.prepare_for_export_for_coinjoin() + if self.payjoin_original.is_segwit(): + self.payjoin_original.convert_all_utxos_to_witness_utxos() + + + def do_payjoin(self): + self.exchange_payjoin_original() + self.payjoin_proposal = PartialTransaction.from_raw_psbt(self.payjoin_proposal_b64) + self.payjoin_proposal_received = True + self.payjoin_proposal.invalidate_ser_cache() + + """ + @staticmethod + def input_vsize(tx: PartialTransaction) -> int: + txin = tx._inputs[0] + is_segwit = tx.is_segwit_input(txin) + input_weight = tx.estimated_input_weight(txin, is_segwit) + return tx.virtual_size_from_weight(input_weight) + """ + + def exchange_payjoin_original(self) -> None: + """ """ + url = self.pj + payload = self.payjoin_original.serialize_as_base64() + headers = {'content-type': 'text/plain', + 'content-length': str(len(payload)) + } + query_string = '?v=' + str(self.VERSION) + query_string += '&additionalfeeoutputindex=' + str(self._additionalfeeoutputindex) + query_string += '&maxadditionalfeecontribution=' + str(self._maxadditionalfeecontribution) + query_string += '&minfeerate=' + str(self._minfeerate) + query_string += '&disableoutputsubstitution=' + self._disableoutputsubstitution + url += query_string + _logger.warning(f"url: {url}")# + session = requests.Session() + if self.pj.endswith('.onion'): + session.proxies = {'http': 'socks5h://localhost:9050', + 'https': 'socks5h://localhost:9050', + } + try: + r = session.post(url, data=payload, headers=headers) + assert r.status_code==200 + self.payjoin_proposal_b64 = r.text + except AssertionError: + raise PayJoinExchangeException(f"Exchange of payjoin failed. {r.status_code}: {r.text}") + except Exception as e: + raise PayJoinExchangeException(f"Exchange of payjoin failed. {repr(e)}") + + + def validate_payjoin_proposal(self) -> None: + + # check transaction version + if self.payjoin_original.version != self.payjoin_proposal.version: + raise PayJoinProposalValidationException(f"The transactin version in payjoin proposal was modified.") + # check transaction locktime + if self.payjoin_original.locktime != self.payjoin_proposal.locktime: + raise PayJoinProposalValidationException(f"The transactin locktime in payjoin proposal was modified.") + + # check whether all inputs from the original psbt are present in the proposal + for txin in self.payjoin_original.inputs(): + sender_input_index = [i for i, x in enumerate(self.payjoin_proposal.inputs()) if txin.prevout.to_str() == x.prevout.to_str()] + if len(sender_input_index) != 1: + raise PayJoinProposalValidationException(f"Inputs from the original payjoin missing in payjoin proposal.") + sender_input = self.payjoin_proposal._inputs[sender_input_index[0]] + if sender_input.is_complete(): + raise PayJoinProposalValidationException(f"Inputs from the original payjoin is already finalized.") + if txin.nsequence != sender_input.nsequence: + raise PayJoinProposalValidationException(f"Inputs nsequence from the original payjoin was modified.") + + sequences = set() + # check wheter the new Inputs are finalized and utxo data is filled + for txin in self.payjoin_proposal.inputs(): + if not txin.prevout.to_str() in [x.prevout.to_str() for x in self.payjoin_original.inputs()]: + if not txin.is_complete(): + raise PayJoinProposalValidationException(f"Newly added input is not finalized.") + sequences.add(txin.nsequence) + + # check that all inputs use the same sequence number + if len(sequences) != 1: + raise PayJoinProposalValidationException(f"Payjoin roposal introduced different sequence numbers.") + + #TODO: check the order of inputs and script Type + + # check the absolute fee was not decreased + if self.payjoin_original.get_fee() > self.payjoin_proposal.get_fee(): + raise PayJoinProposalValidationException(f"The total fee was decreased in the payjoin proposal.") + + # check change output + change_output_o = self.payjoin_original._outputs[self._additionalfeeoutputindex] + change_output_p = None + for txout in self.payjoin_proposal.outputs(): + if change_output_o.scriptpubkey.hex() == txout.scriptpubkey.hex(): + change_output_p = txout + break + # check that the change output still exists + if not change_output_p: + raise PayJoinProposalValidationException(f"Change outputs is missing in the payjoin proposal.") + # check that not too much fees were subtracted + add_fee = change_output_o.value - change_output_p.value + if add_fee > self._maxadditionalfeecontribution: + raise PayJoinProposalValidationException(f"More fee's were added then defined in the payjoin.") + print('add fee change', add_fee)# + print('add fee transaction', (self.payjoin_proposal.get_fee() - self.payjoin_original.get_fee())) # + # check + if add_fee > (self.payjoin_proposal.get_fee() - self.payjoin_original.get_fee()): # should be equal + raise PayJoinProposalValidationException(f"Too much fees were subtracted.") + # check the case that no additional input was added but the fee raised + if add_fee > (self.original_psbt_fee_rate * self.vsize_input_type * + (len(self.payjoin_proposal.inputs()) - len(self.payjoin_original.inputs()))): + raise PayJoinProposalValidationException(f"Fee's were added but no receiver Inputs.") + + # check additional sender outputs are present in the proposal + for txout in self.payjoin_original.outputs(): + if txout.is_change: + continue + if txout.is_mine: + sender_o = [x for x in self.payjoin_proposal.outputs() if txout.scriptpubkey.hex() == x.scriptpubkey.hex()] + if len(sender_o) != 1: + raise PayJoinProposalValidationException(f"Sender outputs from the original payjoin missing in payjoin proposal.") + # check that the amount from the sender that is not fee output was not decreased + if sender_o[0].value < txout.value: + raise PayJoinProposalValidationException(f"The ouput amount for us was decreased in the payjoin.") + + # if output substitution if forbidden, check the outputs and values for the receiver + if self.pjos == 0: + for txout in self.payjoin_proposal.outputs(): + if txout.is_mine or txout.is_change: + continue + receiver_o = [x for x in self.payjoin_original.outputs() if txout.scriptpubkey.hex() == x.scriptpubkey.hex()] + if len(receiver_o) != 1: + raise PayJoinProposalValidationException(f"Receiver modified the payment outputs, but it was forbidden.") + if receiver_o[0].value < txout.value: + raise PayJoinProposalValidationException(f"The amount of the payment ouput was decreased.") + + def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: if len(xfp) != 4: raise Exception(f'unexpected xfp length. xfp={xfp}') diff --git a/electrum/util.py b/electrum/util.py index 21333d37634e..f3c0b5bea530 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -822,8 +822,7 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional 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_bip21_uri(uri: str, on_pr: Callable = None, *, loop=None) -> dict: """Raises InvalidBitcoinURI on malformed URI.""" from . import bitcoin from .bitcoin import COIN @@ -887,7 +886,14 @@ def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: out['sig'] = bh2u(bitcoin.base_decode(out['sig'], base=58)) except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e - + if 'pj' in out: + try: + out['pj'] = str(out.get('pj')) + out['pjos'] = int(out.get('pjos',1)) + print(out['pj'])# + print(out['pjos'])# + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'pj' field: {repr(e)}") from e r = out.get('r') sig = out.get('sig') name = out.get('name') diff --git a/electrum/wallet.py b/electrum/wallet.py index 07b1d2a1f3a0..07b5ac5324fe 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -693,9 +693,13 @@ def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> amount = sum(x.value for x in outputs) timestamp = None exp = None + bip78_payjoin = dict() if URI: timestamp = URI.get('time') exp = URI.get('exp') + bip78_payjoin['pj'] = URI.get('pj') + bip78_payjoin['pjos'] = URI.get('pjos') + timestamp = timestamp or int(time.time()) exp = exp or 0 invoice = OnchainInvoice( @@ -708,6 +712,7 @@ def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> exp=exp, bip70=None, requestor=None, + bip78_payjoin=bip78_payjoin, ) return invoice