Skip to content

Commit

Permalink
BitcoinAmountEdit Qt widget
Browse files Browse the repository at this point in the history
  • Loading branch information
kristapsk committed May 19, 2020
1 parent 66bef00 commit 55f7f5f
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 86 deletions.
152 changes: 83 additions & 69 deletions scripts/joinmarket-qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
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

Expand All @@ -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):
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -295,6 +264,8 @@ def getSettingsFields(self, section, names):
qt = QCheckBox()
if val == 'testnet' or val.lower() == 'true':
qt.setChecked(True)
elif t == 'amount':
qt = BitcoinAmountEdit(val)
elif not t:
continue
else:
Expand Down Expand Up @@ -506,12 +477,44 @@ def initUI(self):

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'
Expand All @@ -527,7 +530,7 @@ def initUI(self):
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(
Expand Down Expand Up @@ -639,25 +642,12 @@ def startSingle(self):
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,
Expand All @@ -684,6 +674,20 @@ def qt_directsend_callback(rtxd, rtxid, confs):
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,
Expand Down Expand Up @@ -975,18 +979,28 @@ def cleanUp(self):
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.",
Expand Down
130 changes: 113 additions & 17 deletions scripts/qtsupport.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
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)


Expand Down Expand Up @@ -53,8 +53,9 @@
'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"',
Expand Down Expand Up @@ -99,7 +100,7 @@
"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" +
Expand Down Expand Up @@ -507,20 +508,115 @@ def filter(self, p, columns):
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(str(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):
Expand Down

0 comments on commit 55f7f5f

Please sign in to comment.