Browse Source

Implement BIP78 payjoin in JoinMarketQt GUI

Co-authored-by: Adam Gibson <ekaggata@gmail.com>
master
Kristaps Kaupe 5 years ago
parent
commit
2401c83c45
No known key found for this signature in database
GPG Key ID: D47B1B4232B55437
  1. 13
      jmclient/jmclient/payjoin.py
  2. 166
      scripts/joinmarket-qt.py
  3. 7
      scripts/qtsupport.py
  4. 2
      scripts/sendpayment.py
  5. 6
      test/payjoinclient.py

13
jmclient/jmclient/payjoin.py

@ -19,6 +19,7 @@ import jmbitcoin as btc
from .wallet import PSBTWalletMixin, SegwitLegacyWallet, SegwitWallet
from .wallet_service import WalletService
from .taker_utils import direct_send
from jmclient import RegtestBitcoinCoreInterface
"""
For some documentation see:
@ -426,7 +427,7 @@ def get_max_additional_fee_contribution(manager):
return max_additional_fee_contribution
def send_payjoin(manager, accept_callback=None,
info_callback=None, tls_whitelist=None):
info_callback=None):
""" Given a JMPayjoinManager object `manager`, initialised with the
payment request data from the server, use its wallet_service to construct
a payment transaction, with coins sourced from mixdepth `mixdepth`,
@ -435,10 +436,6 @@ def send_payjoin(manager, accept_callback=None,
the original payment transaction (None defaults to terminal/CLI processing),
and are as defined in `taker_utils.direct_send`.
If `tls_whitelist` is a list of bytestrings, they are treated as hostnames
for which tls certificate verification is ignored. Obviously this is ONLY for
testing.
Returns:
(True, None) in case of payment setup successful (response will be delivered
asynchronously) - the `manager` object can be inspected for more detail.
@ -456,6 +453,12 @@ def send_payjoin(manager, accept_callback=None,
if not payment_psbt:
return (False, "could not create non-payjoin payment")
# TLS whitelist is for regtest testing, it is treated as hostnames for
# which tls certificate verification is ignored.
tls_whitelist = None
if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
tls_whitelist = ["127.0.0.1"]
manager.set_payment_tx_and_psbt(payment_psbt)
# add delayed call to broadcast this after 1 minute

166
scripts/joinmarket-qt.py

@ -76,7 +76,8 @@ from jmclient import load_program_config, get_network, update_persist_config,\
wallet_generate_recover_bip39, wallet_display, get_utxos_enabled_disabled,\
NO_ROUNDING, get_max_cj_fee_values, get_default_max_absolute_fee, \
get_default_max_relative_fee, RetryableStorageError, add_base_options, \
BTCEngine, BTC_P2SH_P2WPKH, FidelityBondMixin, wallet_change_passphrase
BTCEngine, BTC_P2SH_P2WPKH, FidelityBondMixin, wallet_change_passphrase, \
parse_payjoin_setup, send_payjoin
from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
@ -103,47 +104,6 @@ def update_config_for_gui():
jm_single().config.set("GUI", gcn, gcv)
def checkAddress(parent, addr):
addr = addr.strip()
if btc.is_bip21_uri(addr):
try:
parsed = btc.decode_bip21_uri(addr)
except ValueError as e:
JMQtMessageBox(parent,
"Bitcoin URI not valid.\n" + str(e),
mbtype='warn',
title="Error")
return
addr = parsed['address']
if 'amount' in parsed:
parent.amountInput.setText(parsed['amount'])
parent.addressInput.setText(addr)
valid, errmsg = validate_address(str(addr))
if not valid:
JMQtMessageBox(parent,
"Bitcoin address not valid.\n" + errmsg,
mbtype='warn',
title="Error")
def checkAmount(parent, amount_str):
if not amount_str:
return False
try:
amount_sat = btc.amount_to_sat(amount_str)
except ValueError as e:
JMQtMessageBox(parent, e.args[0], title="Error", mbtype="warn")
return False
if amount_sat < DUST_THRESHOLD:
JMQtMessageBox(parent,
"Amount " + btc.amount_to_str(amount_sat) +
" is below dust threshold " +
btc.amount_to_str(DUST_THRESHOLD) + ".",
mbtype='warn',
title="Error")
return False
return True
handler = QtHandler()
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
@ -332,6 +292,86 @@ class SpendTab(QWidget):
#tracks which mode the spend tab is run in
self.spendstate = SpendStateMgr(self.toggleButtons)
self.spendstate.reset() #trigger callback to 'ready' state
# needed to be saved for parse_payjoin_setup()
self.bip21_uri = None
def switchToBIP78Payjoin(self, endpoint_url):
self.numCPLabel.setVisible(False)
self.numCPInput.setVisible(False)
self.pjEndpointInput.setText(endpoint_url)
self.pjEndpointLabel.setVisible(True)
self.pjEndpointInput.setVisible(True)
# while user is attempting a payjoin, address
# cannot be edited; to back out, they hit Abort.
self.addressInput.setEnabled(False)
self.abortButton.setEnabled(True)
def switchToJoinmarket(self):
self.pjEndpointLabel.setVisible(False)
self.pjEndpointInput.setVisible(False)
self.pjEndpointInput.setText('')
self.numCPLabel.setVisible(True)
self.numCPInput.setVisible(True)
def clearFields(self, ignored):
self.switchToJoinmarket()
self.addressInput.setText('')
self.amountInput.setText('')
self.addressInput.setEnabled(True)
self.pjEndpointInput.setEnabled(True)
self.mixdepthInput.setEnabled(True)
self.amountInput.setEnabled(True)
self.startButton.setEnabled(True)
self.abortButton.setEnabled(False)
def checkAddress(self, addr):
addr = addr.strip()
if btc.is_bip21_uri(addr):
try:
parsed = btc.decode_bip21_uri(addr)
except ValueError as e:
JMQtMessageBox(self,
"Bitcoin URI not valid.\n" + str(e),
mbtype='warn',
title="Error")
return
self.bip21_uri = addr
addr = parsed['address']
if 'amount' in parsed:
self.amountInput.setText(parsed['amount'])
if 'pj' in parsed:
self.switchToBIP78Payjoin(parsed['pj'])
else:
self.switchToJoinmarket()
else:
self.bip21_uri = None
self.addressInput.setText(addr)
valid, errmsg = validate_address(str(addr))
if not valid:
JMQtMessageBox(self,
"Bitcoin address not valid.\n" + errmsg,
mbtype='warn',
title="Error")
def checkAmount(self, amount_str):
if not amount_str:
return False
try:
amount_sat = btc.amount_to_sat(amount_str)
except ValueError as e:
JMQtMessageBox(self, e.args[0], title="Error", mbtype="warn")
return False
if amount_sat < DUST_THRESHOLD:
JMQtMessageBox(self,
"Amount " + btc.amount_to_str(amount_sat) +
" is below dust threshold " +
btc.amount_to_str(DUST_THRESHOLD) + ".",
mbtype='warn',
title="Error")
return False
return True
def generateTumbleSchedule(self):
if not mainWindow.wallet_service:
@ -490,25 +530,32 @@ class SpendTab(QWidget):
donateLayout = self.getDonateLayout()
innerTopLayout.addLayout(donateLayout, 0, 0, 1, 2)
recipientLabel = QLabel('Recipient address')
recipientLabel = QLabel('Recipient address / URI')
recipientLabel.setToolTip(
'The address you want to send the payment to')
'The address or bitcoin: URI you want to send the payment to')
self.addressInput = QLineEdit()
self.addressInput.editingFinished.connect(
lambda: checkAddress(self, self.addressInput.text()))
lambda: self.checkAddress(self.addressInput.text()))
innerTopLayout.addWidget(recipientLabel, 1, 0)
innerTopLayout.addWidget(self.addressInput, 1, 1, 1, 2)
numCPLabel = QLabel('Number of counterparties')
numCPLabel.setToolTip(
self.numCPLabel = QLabel('Number of counterparties')
self.numCPLabel.setToolTip(
'How many other parties to send to; if you enter 4\n' +
', there will be 5 participants, including you.\n' +
'Enter 0 to send direct without coinjoin.')
self.numCPInput = QLineEdit('9')
self.numCPInput.setValidator(QIntValidator(0, 20))
innerTopLayout.addWidget(numCPLabel, 2, 0)
innerTopLayout.addWidget(self.numCPLabel, 2, 0)
innerTopLayout.addWidget(self.numCPInput, 2, 1, 1, 2)
self.pjEndpointLabel = QLabel('PayJoin endpoint')
self.pjEndpointLabel.setVisible(False)
self.pjEndpointInput = QLineEdit()
self.pjEndpointInput.setVisible(False)
innerTopLayout.addWidget(self.pjEndpointLabel, 2, 0)
innerTopLayout.addWidget(self.pjEndpointInput, 2, 1, 1, 2)
mixdepthLabel = QLabel('Mixdepth')
mixdepthLabel.setToolTip(
'The mixdepth of the wallet to send the payment from')
@ -670,8 +717,9 @@ class SpendTab(QWidget):
return
makercount = int(self.numCPInput.text())
mixdepth = int(self.mixdepthInput.text())
bip78url = self.pjEndpointInput.text()
if makercount == 0:
if makercount == 0 and not bip78url:
try:
txid = direct_send(mainWindow.wallet_service, amount, mixdepth,
destaddr, accept_callback=self.checkDirectSend,
@ -698,9 +746,24 @@ class SpendTab(QWidget):
self.cleanUp()
return
if bip78url:
manager = parse_payjoin_setup(self.bip21_uri,
mainWindow.wallet_service, mixdepth, "joinmarket-qt")
# disable form fields until payment is done
self.addressInput.setEnabled(False)
self.pjEndpointInput.setEnabled(False)
self.mixdepthInput.setEnabled(False)
self.amountInput.setEnabled(False)
self.startButton.setEnabled(False)
d = task.deferLater(reactor, 0.0, send_payjoin, manager,
accept_callback=self.checkDirectSend,
info_callback=self.infoDirectSend)
d.addCallback(self.clearFields)
return
# for coinjoin sends no point to send below dust threshold, likely
# there will be no makers for such amount.
if amount != 0 and not checkAmount(self, amount):
if amount != 0 and not self.checkAmount(amount):
return
if makercount < jm_single().config.getint(
@ -974,6 +1037,9 @@ class SpendTab(QWidget):
b.setEnabled(s)
def abortTransactions(self):
if self.pjEndpointInput.isVisible():
self.clearFields(None)
else:
self.taker.aborted = True
self.giveUp()

7
scripts/qtsupport.py

@ -611,10 +611,17 @@ class BitcoinAmountEdit(QWidget):
self.valueInputBox.setText(str(btc_to_sat(btc_amount)))
def setText(self, text):
if text:
if self.unitChooser.currentIndex() == 0:
self.valueInputBox.setText(str(sat_to_btc(text)))
else:
self.valueInputBox.setText(str(text))
else:
self.valueInputBox.setText('')
def setEnabled(self, enabled):
self.valueInputBox.setEnabled(enabled)
self.unitChooser.setEnabled(enabled)
def text(self):
if len(self.valueInputBox.text()) == 0:

2
scripts/sendpayment.py

@ -325,7 +325,7 @@ def main():
elif bip78url:
# TODO sanity check wallet type is segwit
manager = parse_payjoin_setup(args[1], wallet_service, options.mixdepth)
reactor.callWhenRunning(send_payjoin, manager, tls_whitelist=["127.0.0.1"])
reactor.callWhenRunning(send_payjoin, manager)
reactor.run()
return

6
test/payjoinclient.py

@ -59,9 +59,5 @@ if __name__ == "__main__":
# the sync call here will now be a no-op:
wallet_service.startService()
manager = parse_payjoin_setup(bip21uri, wallet_service, mixdepth)
if usessl == 0:
tlshostnames = None
else:
tlshostnames = [b"127.0.0.1"]
reactor.callWhenRunning(send_payjoin, manager, tls_whitelist=tlshostnames)
reactor.callWhenRunning(send_payjoin, manager)
reactor.run()

Loading…
Cancel
Save