Browse Source

Merge #530: Qt: widget for entering amounts, supports both BTC and sats

d1323f6 BitcoinAmountEdit Qt widget (Kristaps Kaupe)
master
Adam Gibson 6 years ago
parent
commit
7748da54e0
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 152
      scripts/joinmarket-qt.py
  2. 130
      scripts/qtsupport.py

152
scripts/joinmarket-qt.py

@ -79,7 +79,7 @@ from jmclient import load_program_config, get_network, update_persist_config,\
from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
donation_more_message
donation_more_message, BitcoinAmountEdit
from twisted.internet import task
@ -101,6 +101,7 @@ def update_config_for_gui():
if gcn not in [_[0] for _ in gui_items]:
jm_single().config.set("GUI", gcn, gcv)
def checkAddress(parent, addr):
addr = addr.strip()
if btc.is_bip21_uri(addr):
@ -114,9 +115,8 @@ def checkAddress(parent, addr):
return
addr = parsed['address']
if 'amount' in parsed:
parent.widgets[3][1].setText(
str(btc.sat_to_btc(parsed['amount'])))
parent.widgets[0][1].setText(addr)
parent.amountInput.setText(parsed['amount'])
parent.addressInput.setText(addr)
valid, errmsg = validate_address(str(addr))
if not valid:
JMQtMessageBox(parent,
@ -140,37 +140,6 @@ def checkAmount(parent, amount_str):
return True
def getSettingsWidgets():
results = []
sN = ['Recipient address', 'Number of counterparties', 'Mixdepth',
'Amount (BTC or sat)']
sH = ['The address you want to send the payment to',
'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.',
'The mixdepth of the wallet to send the payment from',
'The amount to send, either BTC (if contains dot) or satoshis.\n' +
'If you enter 0, a SWEEP transaction\nwill be performed,' +
' spending all the coins \nin the given mixdepth.']
sT = [str, int, int, float]
#todo maxmixdepth
sMM = ['', (2, 20),
(0, jm_single().config.getint("GUI", "max_mix_depth") - 1),
(0.00000001, 100.0, 8)]
sD = ['', '9', '0', '']
for x in zip(sN, sH, sT, sD, sMM):
ql = QLabel(x[0])
ql.setToolTip(x[1])
qle = QLineEdit(x[3])
if x[2] == int:
qle.setValidator(QIntValidator(*x[4]))
if x[2] == float:
qdv = QDoubleValidator(*x[4])
qle.setValidator(qdv)
results.append((ql, qle))
return results
handler = QtHandler()
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
log.addHandler(handler)
@ -295,6 +264,8 @@ class SettingsTab(QDialog):
qt = QCheckBox()
if val == 'testnet' or val.lower() == 'true':
qt.setChecked(True)
elif t == 'amount':
qt = BitcoinAmountEdit(val)
elif not t:
continue
else:
@ -506,12 +477,44 @@ class SpendTab(QWidget):
donateLayout = self.getDonateLayout()
innerTopLayout.addLayout(donateLayout, 0, 0, 1, 2)
self.widgets = getSettingsWidgets()
for i, x in enumerate(self.widgets):
innerTopLayout.addWidget(x[0], i + 1, 0)
innerTopLayout.addWidget(x[1], i + 1, 1, 1, 2)
self.widgets[0][1].editingFinished.connect(
lambda: checkAddress(self, self.widgets[0][1].text()))
recipientLabel = QLabel('Recipient address')
recipientLabel.setToolTip(
'The address you want to send the payment to')
self.addressInput = QLineEdit()
self.addressInput.editingFinished.connect(
lambda: checkAddress(self, self.addressInput.text()))
innerTopLayout.addWidget(recipientLabel, 1, 0)
innerTopLayout.addWidget(self.addressInput, 1, 1, 1, 2)
numCPLabel = QLabel('Number of counterparties')
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.numCPInput, 2, 1, 1, 2)
mixdepthLabel = QLabel('Mixdepth')
mixdepthLabel.setToolTip(
'The mixdepth of the wallet to send the payment from')
self.mixdepthInput = QLineEdit('0')
self.mixdepthInput.setValidator(QIntValidator(
0, jm_single().config.getint("GUI", "max_mix_depth") - 1))
innerTopLayout.addWidget(mixdepthLabel, 3, 0)
innerTopLayout.addWidget(self.mixdepthInput, 3, 1, 1, 2)
amountLabel = QLabel('Amount')
amountLabel.setToolTip(
'The amount to send.\n' +
'If you enter 0, a SWEEP transaction\nwill be performed,' +
' spending all the coins \nin the given mixdepth.')
self.amountInput = BitcoinAmountEdit('')
innerTopLayout.addWidget(amountLabel, 4, 0)
innerTopLayout.addWidget(self.amountInput, 4, 1, 1, 2)
self.startButton = QPushButton('Start')
self.startButton.setToolTip(
'If "checktx" is selected in the Settings, you will be \n'
@ -527,7 +530,7 @@ class SpendTab(QWidget):
buttons.addWidget(self.startButton)
buttons.addWidget(self.abortButton)
self.abortButton.clicked.connect(self.abortTransactions)
innerTopLayout.addLayout(buttons, len(self.widgets) + 1, 0, 1, 2)
innerTopLayout.addLayout(buttons, 5, 0, 1, 2)
splitter1 = QSplitter(QtCore.Qt.Vertical)
self.textedit = QTextEdit()
self.textedit.verticalScrollBar().rangeChanged.connect(
@ -639,25 +642,12 @@ class SpendTab(QWidget):
log.info("Cannot start join, already running.")
if not self.validateSettings():
return
destaddr = str(self.widgets[0][1].text()).strip()
makercount = int(self.widgets[1][1].text())
mixdepth = int(self.widgets[2][1].text())
btc_amount_str = self.widgets[3][1].text()
if makercount != 0:
# for coinjoin sends no point to send below dust threshold,
# there will be no makers for such amount.
if (btc_amount_str != '0' and
not checkAmount(self, btc_amount_str)):
return
if makercount < jm_single().config.getint(
"POLICY", "minimum_makers"):
JMQtMessageBox(self, "Number of counterparties (" + str(
makercount) + ") below minimum_makers (" + str(
jm_single().config.getint("POLICY", "minimum_makers")) +
") in configuration.",
title="Error", mbtype="warn")
return
amount = btc.amount_to_sat(btc_amount_str)
destaddr = str(self.addressInput.text().strip())
amount = btc.amount_to_sat(self.amountInput.text())
makercount = int(self.numCPInput.text())
mixdepth = int(self.mixdepthInput.text())
if makercount == 0:
try:
txid = direct_send(mainWindow.wallet_service, amount, mixdepth,
@ -684,6 +674,20 @@ class SpendTab(QWidget):
self.cleanUp()
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):
return
if makercount < jm_single().config.getint(
"POLICY", "minimum_makers"):
JMQtMessageBox(self, "Number of counterparties (" + str(
makercount) + ") below minimum_makers (" + str(
jm_single().config.getint("POLICY", "minimum_makers")) +
") in configuration.",
title="Error", mbtype="warn")
return
#note 'amount' is integer, so not interpreted as fraction
#see notes in sample testnet schedule for format
self.spendstate.loaded_schedule = [[mixdepth, amount, makercount,
@ -975,18 +979,28 @@ class SpendTab(QWidget):
self.tumbler_destaddrs = None
def validateSettings(self):
valid, errmsg = validate_address(str(
self.widgets[0][1].text().strip()))
valid, errmsg = validate_address(
str(self.addressInput.text().strip()))
if not valid:
JMQtMessageBox(self, errmsg, mbtype='warn', title="Error")
return False
errs = ["Non-zero number of counterparties must be provided.",
if len(self.numCPInput.text()) == 0:
JMQtMessageBox(
self,
"Non-zero number of counterparties must be provided.",
mbtype='warn', title="Error")
return False
if len(self.mixdepthInput.text()) == 0:
JMQtMessageBox(
self,
"Mixdepth must be chosen.",
"Amount, in bitcoins, must be provided."]
for i in range(1, 4):
if len(self.widgets[i][1].text()) == 0:
JMQtMessageBox(self, errs[i - 1], mbtype='warn', title="Error")
return False
mbtype='warn', title="Error")
return False
if len(self.amountInput.text()) == 0:
JMQtMessageBox(
self,
"Amount, in bitcoins, must be provided.",
mbtype='warn', title="Error")
if not mainWindow.wallet_service:
JMQtMessageBox(self,
"There is no wallet loaded.",

130
scripts/qtsupport.py

@ -17,12 +17,12 @@ Qt files for the wizard for initiating a tumbler run.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import math, re, logging
import math, re, logging, string
from PySide2 import QtCore
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from jmbitcoin.amount import amount_to_sat, btc_to_sat, sat_to_btc
from jmclient import (jm_single, validate_address, get_tumble_schedule)
@ -53,8 +53,9 @@ config_types = {'rpc_port': int,
'check_high_fee': int,
'max_mix_depth': int,
'order_wait_time': int,
"no_daemon": int,
"daemon_port": int,}
'no_daemon': int,
'daemon_port': int,
'absurd_fee_per_kb': 'amount'}
config_tips = {
'blockchain_source': 'options: bitcoin-rpc, regtest (for testing)',
'network': 'one of "testnet" or "mainnet"',
@ -99,7 +100,7 @@ config_tips = {
"native": "NOT currently supported, except for PayJoin (command line only)",
"console_log_level": "one of INFO, DEBUG, WARN, ERROR; INFO is least noisy;\n" +
"consider switching to DEBUG in case of problems.",
"absurd_fee_per_kb": "maximum satoshis/kilobyte you are willing to pay,\n" +
"absurd_fee_per_kb": "maximum amount per kilobyte you are willing to pay,\n" +
"whatever the fee estimate currently says.",
"tx_broadcast": "Options: self, random-peer, not-self (note: random-maker\n" +
"is not currently supported).\n" +
@ -507,20 +508,115 @@ class MyTreeWidget(QTreeWidget):
item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1
for column in columns]))
""" TODO implement this option
class SchStaticPage(QWizardPage):
def __init__(self, parent):
super(SchStaticPage, self).__init__(parent)
self.setTitle("Manually create a schedule entry")
# TODO implement this option
#class SchStaticPage(QWizardPage):
# def __init__(self, parent):
# super(SchStaticPage, self).__init__(parent)
# self.setTitle("Manually create a schedule entry")
# layout = QGridLayout()
# wdgts = getSettingsWidgets()
# for i, x in enumerate(wdgts):
# layout.addWidget(x[0], i + 1, 0)
# layout.addWidget(x[1], i + 1, 1, 1, 2)
# wdgts[0][1].editingFinished.connect(
# lambda: checkAddress(self, wdgts[0][1].text()))
# self.setLayout(layout)
class BitcoinAmountBTCValidator(QDoubleValidator):
def __init__(self):
super().__init__(0.00000000, 20999999.9769, 8)
self.setLocale(QtCore.QLocale.c())
# Only numbers and "." as a decimal separator must be allowed,
# no thousands separators, as per BIP21
self.allowed = set(string.digits + ".")
def validate(self, arg__1, arg__2):
if not arg__1:
return QValidator.Intermediate
if not set(arg__1) <= self.allowed:
return QValidator.Invalid
return super().validate(arg__1, arg__2)
class BitcoinAmountSatValidator(QIntValidator):
def __init__(self):
super().__init__(0, 2147483647)
self.setLocale(QtCore.QLocale.c())
self.allowed = set(string.digits)
def validate(self, arg__1, arg__2):
if not arg__1:
return QValidator.Intermediate
if not set(arg__1) <= self.allowed:
return QValidator.Invalid
return super().validate(arg__1, arg__2)
class BitcoinAmountEdit(QWidget):
def __init__(self, default_value):
super().__init__()
layout = QGridLayout()
wdgts = getSettingsWidgets()
for i, x in enumerate(wdgts):
layout.addWidget(x[0], i + 1, 0)
layout.addWidget(x[1], i + 1, 1, 1, 2)
wdgts[0][1].editingFinished.connect(
lambda: checkAddress(self, wdgts[0][1].text()))
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(1)
self.valueInputBox = QLineEdit()
self.editingFinished = self.valueInputBox.editingFinished
layout.addWidget(self.valueInputBox, 0, 0)
self.unitChooser = QComboBox()
self.unitChooser.setInsertPolicy(QComboBox.NoInsert)
self.unitChooser.addItems(["BTC", "sat"])
self.unitChooser.currentIndexChanged.connect(self.onUnitChanged)
self.BTCValidator = BitcoinAmountBTCValidator()
self.SatValidator = BitcoinAmountSatValidator()
self.setModeBTC()
layout.addWidget(self.unitChooser, 0, 1)
if default_value:
self.valueInputBox.setText(str(sat_to_btc(amount_to_sat(
default_value))))
self.setLayout(layout)
"""
def setModeBTC(self):
self.valueInputBox.setPlaceholderText("0.00000000")
self.valueInputBox.setMaxLength(17)
self.valueInputBox.setValidator(self.BTCValidator)
def setModeSat(self):
self.valueInputBox.setPlaceholderText("0")
self.valueInputBox.setMaxLength(16)
self.valueInputBox.setValidator(self.SatValidator)
# index: 0 - BTC, 1 - sat
def onUnitChanged(self, index):
if index == 0:
# switch from sat to BTC
sat_amount = self.valueInputBox.text()
self.setModeBTC()
if sat_amount:
self.valueInputBox.setText('%.8f' % sat_to_btc(sat_amount))
else:
# switch from BTC to sat
btc_amount = self.valueInputBox.text()
self.setModeSat()
if btc_amount:
self.valueInputBox.setText(str(btc_to_sat(btc_amount)))
def setText(self, text):
if self.unitChooser.currentIndex() == 0:
self.valueInputBox.setText(str(sat_to_btc(text)))
else:
self.valueInputBox.setText(str(text))
def text(self):
if len(self.valueInputBox.text()) == 0:
return ''
elif self.unitChooser.currentIndex() == 0:
return str(btc_to_sat(self.valueInputBox.text()))
else:
return self.valueInputBox.text()
class SchDynamicPage1(QWizardPage):
def __init__(self, parent):

Loading…
Cancel
Save