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. 150
      scripts/joinmarket-qt.py
  2. 130
      scripts/qtsupport.py

150
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,\ from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\ config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\ PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
donation_more_message donation_more_message, BitcoinAmountEdit
from twisted.internet import task from twisted.internet import task
@ -101,6 +101,7 @@ def update_config_for_gui():
if gcn not in [_[0] for _ in gui_items]: if gcn not in [_[0] for _ in gui_items]:
jm_single().config.set("GUI", gcn, gcv) jm_single().config.set("GUI", gcn, gcv)
def checkAddress(parent, addr): def checkAddress(parent, addr):
addr = addr.strip() addr = addr.strip()
if btc.is_bip21_uri(addr): if btc.is_bip21_uri(addr):
@ -114,9 +115,8 @@ def checkAddress(parent, addr):
return return
addr = parsed['address'] addr = parsed['address']
if 'amount' in parsed: if 'amount' in parsed:
parent.widgets[3][1].setText( parent.amountInput.setText(parsed['amount'])
str(btc.sat_to_btc(parsed['amount']))) parent.addressInput.setText(addr)
parent.widgets[0][1].setText(addr)
valid, errmsg = validate_address(str(addr)) valid, errmsg = validate_address(str(addr))
if not valid: if not valid:
JMQtMessageBox(parent, JMQtMessageBox(parent,
@ -140,37 +140,6 @@ def checkAmount(parent, amount_str):
return True 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 = QtHandler()
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s")) handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
log.addHandler(handler) log.addHandler(handler)
@ -295,6 +264,8 @@ class SettingsTab(QDialog):
qt = QCheckBox() qt = QCheckBox()
if val == 'testnet' or val.lower() == 'true': if val == 'testnet' or val.lower() == 'true':
qt.setChecked(True) qt.setChecked(True)
elif t == 'amount':
qt = BitcoinAmountEdit(val)
elif not t: elif not t:
continue continue
else: else:
@ -506,12 +477,44 @@ class SpendTab(QWidget):
donateLayout = self.getDonateLayout() donateLayout = self.getDonateLayout()
innerTopLayout.addLayout(donateLayout, 0, 0, 1, 2) innerTopLayout.addLayout(donateLayout, 0, 0, 1, 2)
self.widgets = getSettingsWidgets()
for i, x in enumerate(self.widgets): recipientLabel = QLabel('Recipient address')
innerTopLayout.addWidget(x[0], i + 1, 0) recipientLabel.setToolTip(
innerTopLayout.addWidget(x[1], i + 1, 1, 1, 2) 'The address you want to send the payment to')
self.widgets[0][1].editingFinished.connect( self.addressInput = QLineEdit()
lambda: checkAddress(self, self.widgets[0][1].text())) 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 = QPushButton('Start')
self.startButton.setToolTip( self.startButton.setToolTip(
'If "checktx" is selected in the Settings, you will be \n' '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.startButton)
buttons.addWidget(self.abortButton) buttons.addWidget(self.abortButton)
self.abortButton.clicked.connect(self.abortTransactions) 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) splitter1 = QSplitter(QtCore.Qt.Vertical)
self.textedit = QTextEdit() self.textedit = QTextEdit()
self.textedit.verticalScrollBar().rangeChanged.connect( self.textedit.verticalScrollBar().rangeChanged.connect(
@ -639,25 +642,12 @@ class SpendTab(QWidget):
log.info("Cannot start join, already running.") log.info("Cannot start join, already running.")
if not self.validateSettings(): if not self.validateSettings():
return return
destaddr = str(self.widgets[0][1].text()).strip()
makercount = int(self.widgets[1][1].text()) destaddr = str(self.addressInput.text().strip())
mixdepth = int(self.widgets[2][1].text()) amount = btc.amount_to_sat(self.amountInput.text())
btc_amount_str = self.widgets[3][1].text() makercount = int(self.numCPInput.text())
if makercount != 0: mixdepth = int(self.mixdepthInput.text())
# 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)
if makercount == 0: if makercount == 0:
try: try:
txid = direct_send(mainWindow.wallet_service, amount, mixdepth, txid = direct_send(mainWindow.wallet_service, amount, mixdepth,
@ -684,6 +674,20 @@ class SpendTab(QWidget):
self.cleanUp() self.cleanUp()
return 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 #note 'amount' is integer, so not interpreted as fraction
#see notes in sample testnet schedule for format #see notes in sample testnet schedule for format
self.spendstate.loaded_schedule = [[mixdepth, amount, makercount, self.spendstate.loaded_schedule = [[mixdepth, amount, makercount,
@ -975,18 +979,28 @@ class SpendTab(QWidget):
self.tumbler_destaddrs = None self.tumbler_destaddrs = None
def validateSettings(self): def validateSettings(self):
valid, errmsg = validate_address(str( valid, errmsg = validate_address(
self.widgets[0][1].text().strip())) str(self.addressInput.text().strip()))
if not valid: if not valid:
JMQtMessageBox(self, errmsg, mbtype='warn', title="Error") JMQtMessageBox(self, errmsg, mbtype='warn', title="Error")
return False 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.", "Mixdepth must be chosen.",
"Amount, in bitcoins, must be provided."] mbtype='warn', title="Error")
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 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: if not mainWindow.wallet_service:
JMQtMessageBox(self, JMQtMessageBox(self,
"There is no wallet loaded.", "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 You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. 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 import QtCore
from PySide2.QtGui import * from PySide2.QtGui import *
from PySide2.QtWidgets 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) from jmclient import (jm_single, validate_address, get_tumble_schedule)
@ -53,8 +53,9 @@ config_types = {'rpc_port': int,
'check_high_fee': int, 'check_high_fee': int,
'max_mix_depth': int, 'max_mix_depth': int,
'order_wait_time': int, 'order_wait_time': int,
"no_daemon": int, 'no_daemon': int,
"daemon_port": int,} 'daemon_port': int,
'absurd_fee_per_kb': 'amount'}
config_tips = { config_tips = {
'blockchain_source': 'options: bitcoin-rpc, regtest (for testing)', 'blockchain_source': 'options: bitcoin-rpc, regtest (for testing)',
'network': 'one of "testnet" or "mainnet"', 'network': 'one of "testnet" or "mainnet"',
@ -99,7 +100,7 @@ config_tips = {
"native": "NOT currently supported, except for PayJoin (command line only)", "native": "NOT currently supported, except for PayJoin (command line only)",
"console_log_level": "one of INFO, DEBUG, WARN, ERROR; INFO is least noisy;\n" + "console_log_level": "one of INFO, DEBUG, WARN, ERROR; INFO is least noisy;\n" +
"consider switching to DEBUG in case of problems.", "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.", "whatever the fee estimate currently says.",
"tx_broadcast": "Options: self, random-peer, not-self (note: random-maker\n" + "tx_broadcast": "Options: self, random-peer, not-self (note: random-maker\n" +
"is not currently supported).\n" + "is not currently supported).\n" +
@ -507,20 +508,115 @@ class MyTreeWidget(QTreeWidget):
item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1 item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1
for column in columns])) for column in columns]))
""" TODO implement this option # TODO implement this option
class SchStaticPage(QWizardPage): #class SchStaticPage(QWizardPage):
def __init__(self, parent): # def __init__(self, parent):
super(SchStaticPage, self).__init__(parent) # super(SchStaticPage, self).__init__(parent)
self.setTitle("Manually create a schedule entry") # 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() layout = QGridLayout()
wdgts = getSettingsWidgets() layout.setContentsMargins(0, 0, 0, 0)
for i, x in enumerate(wdgts): layout.setSpacing(1)
layout.addWidget(x[0], i + 1, 0) self.valueInputBox = QLineEdit()
layout.addWidget(x[1], i + 1, 1, 1, 2) self.editingFinished = self.valueInputBox.editingFinished
wdgts[0][1].editingFinished.connect( layout.addWidget(self.valueInputBox, 0, 0)
lambda: checkAddress(self, wdgts[0][1].text())) 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) 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): class SchDynamicPage1(QWizardPage):
def __init__(self, parent): def __init__(self, parent):

Loading…
Cancel
Save