Skip to content

Commit

Permalink
Yield generator script can be launched in Qt
Browse files Browse the repository at this point in the history
A status bar widget allows access to configure,
and start/stop a yield generator (maker) bot
in the background. While running, taker functions
are disabled as well as wallet changing functions.
Other functionality is still available.
  • Loading branch information
AdamISZ committed Jan 10, 2020
1 parent 252a869 commit 7d7989f
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 39 deletions.
7 changes: 7 additions & 0 deletions jmbase/jmbase/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ class JMMsgSignatureVerify(JMCommand):
(b'fullmsg', Unicode()),
(b'hostid', Unicode())]

class JMShutdown(JMCommand):
""" Requests shutdown of the current
message channel connections (to be used
when the client is shutting down).
"""
arguments = []

"""TAKER specific commands
"""

Expand Down
2 changes: 1 addition & 1 deletion jmclient/jmclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
wallet_display, get_utxos_enabled_disabled)
from .wallet_service import WalletService
from .maker import Maker, P2EPMaker
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain, ygstart

# Set default logging handler to avoid "No handler found" warnings.

Expand Down
9 changes: 9 additions & 0 deletions jmclient/jmclient/client_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ def on_p2ep_tx_received(self, nick, txhex):
self.make_tx([nick], txhex)
return {"accepted": True}

def request_mc_shutdown(self):
""" To ensure that lingering message channel
connections are shut down when the client itself
is shutting down.
"""
d = self.callRemote(commands.JMShutdown)
self.defaultCallbacks(d)
return {'accepted': True}

class JMMakerClientProtocol(JMClientProtocol):
def __init__(self, factory, maker, nick_priv=None):
self.factory = factory
Expand Down
10 changes: 7 additions & 3 deletions jmclient/jmclient/yieldgenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,11 @@ def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffe
wallet_service.sync_wallet(fast=not options.recoversync)
wallet_service.startService()

maker = ygclass(wallet_service, [options.txfee, cjfee_a, cjfee_r,
options.ordertype, options.minsize])
ygstart(wallet_service, [options.txfee, cjfee_a, cjfee_r,
options.ordertype, options.minsize], ygclass=ygclass)

def ygstart(wallet_service, makerconfig, rs=True, ygclass=YieldGeneratorBasic):
maker = ygclass(wallet_service, makerconfig)
jlog.info('starting yield generator')
clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER")

Expand All @@ -264,4 +267,5 @@ def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffe
startLogging(sys.stdout)
start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
jm_single().config.getint("DAEMON", "daemon_port"),
clientfactory, daemon=daemon)
clientfactory, rs=rs, daemon=daemon)
return clientfactory
6 changes: 6 additions & 0 deletions jmdaemon/jmdaemon/daemon_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ def on_JM_MSGSIGNATURE_VERIFY(self, verif_result, nick, fullmsg, hostid):
self.mcc.on_verified_privmsg(nick, fullmsg, hostid)
return {'accepted': True}

@JMShutdown.responder
def on_JM_SHUTDOWN(self):
print("reached on shutdown in jmdaemonserverprotocol")
self.mc_shutdown()
self.jm_state = 0
return {'accepted': True}
"""Taker specific responders
"""

Expand Down
7 changes: 4 additions & 3 deletions jmdaemon/jmdaemon/irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def run(self):
def shutdown(self):
self.tx_irc_client.quit()
self.give_up = True
self.myRS.stopService()

def _pubmsg(self, msg):
self.tx_irc_client._pubmsg(msg)
Expand Down Expand Up @@ -156,8 +157,8 @@ def build_irc(self):
use_tls = False
ircEndpoint = TorSocksEndpoint(torEndpoint, self.serverport[0],
self.serverport[1], tls=use_tls)
myRS = ClientService(ircEndpoint, factory)
myRS.startService()
self.myRS = ClientService(ircEndpoint, factory)
self.myRS.startService()
else:
try:
factory = TxIRCFactory(self)
Expand Down Expand Up @@ -200,7 +201,7 @@ def connectionMade(self):
return irc.IRCClient.connectionMade(self)

def connectionLost(self, reason=protocol.connectionDone):
if self.wrapper.on_disconnect:
if not self.wrapper.give_up and self.wrapper.on_disconnect:
reactor.callLater(0.0, self.wrapper.on_disconnect, self.wrapper)
return irc.IRCClient.connectionLost(self, reason)

Expand Down
118 changes: 87 additions & 31 deletions scripts/joinmarket-qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@
get_tumble_log, restart_wait, tumbler_filter_orders_callback,\
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
get_default_max_relative_fee, RetryableStorageError, add_base_options, ygstart
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, StatusBarButton, read_QIcon, MakerDialog

from twisted.internet import task

Expand Down Expand Up @@ -1295,6 +1295,11 @@ def __init__(self, reactor):
# was already shown
self.syncmsg = ""

# makerDialog keeps track of state related
# to running in Maker mode:
self.makerDialog = None
self.maker_running = False

self.reactor = reactor
self.initUI()

Expand All @@ -1309,42 +1314,93 @@ def closeEvent(self, event):
else:
event.ignore()

def makerManager(self):
action_fn = self.stopMaker if self.maker_running else self.startMaker
self.makerDialog = MakerDialog(action_fn, self.maker_running)

def toggle_non_maker_function(self):
""" While maker is running we prevent actions
to do coinjoins as taker or to load or alter the wallet
or change settings.
TODO These restrictions can be relaxed after analysis.
"""
for action in [self.loadAction, self.generateAction,
self.recoverAction]:
action.setEnabled(not self.maker_running)
for tab in [self.centralWidget().widget(x) for x in [1,2]]:
tab.setEnabled(not self.maker_running)

def startMaker(self):
if not self.wallet_service:
return (False, "Wallet is not loaded and synced, cannot start maker.")
mle = self.makerDialog.maker_settings_le
offertype = 'swreloffer' if mle[0][1].currentText() == "Relative fee" else 'swabsoffer'
cjabsfee = int(mle[1][1].text())
cjrelfee = float(mle[2][1].text())
txfee = int(mle[3][1].text())
minsize = int(mle[4][1].text())
self.makerfactory = ygstart(self.wallet_service, [txfee, cjabsfee, cjrelfee,
offertype, minsize], rs=False)
self.maker_running = True
self.setMakerBtn()
self.toggle_non_maker_function()
self.makerDialog.close()


def setMakerBtn(self):
if self.maker_running:
self.makerbtn.setIcon(read_QIcon("greencircle.png"))
self.makerbtn.setToolTip("Maker running: click to manage")
else:
self.makerbtn.setIcon(read_QIcon("reddiamond.png"))
self.makerbtn.setToolTip("Click to start maker.")

def stopMaker(self):
self.makerfactory.proto_client.request_mc_shutdown()
self.maker_running = False
self.setMakerBtn()
self.toggle_non_maker_function()
self.makerDialog.close()

def initUI(self):
self.statusBar().showMessage("Ready")
self.makerbtn = StatusBarButton(read_QIcon("reddiamond.png"),
"Click to start maker", self.makerManager)
self.statusBar().addPermanentWidget(self.makerbtn)
self.setGeometry(300, 300, 250, 150)
loadAction = QAction('&Load', self)
loadAction.setStatusTip('Load wallet from file')
loadAction.triggered.connect(self.selectWallet)
generateAction = QAction('&Generate', self)
generateAction.setStatusTip('Generate new wallet')
generateAction.triggered.connect(self.generateWallet)
recoverAction = QAction('&Recover', self)
recoverAction.setStatusTip('Recover wallet from seed phrase')
recoverAction.triggered.connect(self.recoverWallet)
showSeedAction = QAction('&Show seed', self)
showSeedAction.setStatusTip('Show wallet seed phrase')
showSeedAction.triggered.connect(self.showSeedDialog)
exportPrivAction = QAction('&Export keys', self)
exportPrivAction.setStatusTip('Export all private keys to a file')
exportPrivAction.triggered.connect(self.exportPrivkeysJson)
quitAction = QAction(QIcon('exit.png'), '&Quit', self)
quitAction.setShortcut('Ctrl+Q')
quitAction.setStatusTip('Quit application')
quitAction.triggered.connect(qApp.quit)

aboutAction = QAction('About Joinmarket', self)
aboutAction.triggered.connect(self.showAboutDialog)
self.loadAction = QAction('&Load', self)
self.loadAction.setStatusTip('Load wallet from file')
self.loadAction.triggered.connect(self.selectWallet)
self.generateAction = QAction('&Generate', self)
self.generateAction.setStatusTip('Generate new wallet')
self.generateAction.triggered.connect(self.generateWallet)
self.recoverAction = QAction('&Recover', self)
self.recoverAction.setStatusTip('Recover wallet from seed phrase')
self.recoverAction.triggered.connect(self.recoverWallet)
self.showSeedAction = QAction('&Show seed', self)
self.showSeedAction.setStatusTip('Show wallet seed phrase')
self.showSeedAction.triggered.connect(self.showSeedDialog)
self.exportPrivAction = QAction('&Export keys', self)
self.exportPrivAction.setStatusTip('Export all private keys to a file')
self.exportPrivAction.triggered.connect(self.exportPrivkeysJson)
self.quitAction = QAction(QIcon('exit.png'), '&Quit', self)
self.quitAction.setShortcut('Ctrl+Q')
self.quitAction.setStatusTip('Quit application')
self.quitAction.triggered.connect(qApp.quit)

self.aboutAction = QAction('About Joinmarket', self)
self.aboutAction.triggered.connect(self.showAboutDialog)

menubar = self.menuBar()
walletMenu = menubar.addMenu('&Wallet')
walletMenu.addAction(loadAction)
walletMenu.addAction(generateAction)
walletMenu.addAction(recoverAction)
walletMenu.addAction(showSeedAction)
walletMenu.addAction(exportPrivAction)
walletMenu.addAction(quitAction)
walletMenu.addAction(self.loadAction)
walletMenu.addAction(self.generateAction)
walletMenu.addAction(self.recoverAction)
walletMenu.addAction(self.showSeedAction)
walletMenu.addAction(self.exportPrivAction)
walletMenu.addAction(self.quitAction)
aboutMenu = menubar.addMenu('&About')
aboutMenu.addAction(aboutAction)
aboutMenu.addAction(self.aboutAction)

self.show()

Expand Down
93 changes: 92 additions & 1 deletion scripts/qtsupport.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,105 @@
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, os
from functools import lru_cache
from PySide2 import QtCore
from PySide2.QtGui import *
from PySide2.QtWidgets import *


from jmclient import (jm_single, validate_address, get_tumble_schedule)

class MakerDialog(QDialog):

def __init__(self, action_fn, running):
""" Parameter action_fn:
each time the user opens the dialog they will
pass a function to be connected to the action-button.
"""
super(MakerDialog, self).__init__()
# these QLineEdit objects will contain the settings
# for the maker as chosen by the user:
self.maker_settings_le = []
self.action_fn = action_fn
self.running = running
self.initUI()

def initUI(self):
self.setModal(1)
tp = "Running: " if self.running else "(Not running): "
self.setWindowTitle(tp + "Manage yield generator")
self.setLayout(self.get_maker_dialog())
self.show()

def get_maker_dialog(self):
sN = ['CJ Offer type', 'CJ Absolute fee (sats)',
'CJ Relative fee (decimal)',
'Tx fee contribution (sats)',
'Minimum CJ size (sats)']
#Tooltips
sH = ["abc" for x in range(len(sN))]
#types
sT = [str, int, float, int, int]
# defaults (TODO: keep up to date with edits)
sD = ['Relative fee', '500', '0.0002', '100', '1000000']
for x in zip(sN, sH, sT, sD):
ql = QLabel(x[0])
ql.setToolTip(x[1])
if x[0] == 'CJ Offer type':
qle = QComboBox()
qle.addItem("Relative fee")
qle.addItem("Absolute fee")
else:
qle = QLineEdit(x[3])
if self.running:
qle.setEnabled(False)

# TODO: apply validators
#if x[2] == int:
# qle.setValidator(QIntValidator(*x[4]))
#if x[2] == float:
# qle.setValidator(QDoubleValidator(*x[4]))
self.maker_settings_le.append((ql, qle))
layout = QGridLayout(self)
layout.setSpacing(4)
for i, x in enumerate(self.maker_settings_le):
layout.addWidget(x[0], i + 1, 0)
layout.addWidget(x[1], i + 1, 1, 1, 2)
btnbox = QDialogButtonBox()
btnbox.setStandardButtons(QDialogButtonBox.Cancel)
btnname = "Stop" if self.running else "Start"
btnbox.addButton(btnname, QDialogButtonBox.ActionRole)
layout.addWidget(btnbox, i +2, 0)
btnbox.rejected.connect(self.close)
btnbox.buttons()[1].clicked.connect(self.action_fn)
return layout

def icon_path(icon_basename):
return os.path.join('icons', icon_basename)

@lru_cache(maxsize=1000)
def read_QIcon(icon_basename):
return QIcon(icon_path(icon_basename))

class StatusBarButton(QPushButton):
def __init__(self, icon, tooltip, func):
QPushButton.__init__(self, icon, '')
self.setToolTip(tooltip)
self.setFlat(True)
self.setMaximumWidth(25)
self.clicked.connect(self.onPress)
self.func = func
self.setIconSize(QtCore.QSize(25,25))
self.setCursor(QCursor(QtCore.Qt.PointingHandCursor))

def onPress(self, checked=False):
'''Drops the unwanted PyQt5 "checked" argument'''
self.func()

def keyPressEvent(self, e):
if e.key() == QtCore.Qt.Key_Return:
self.func()

GREEN_BG = "QWidget {background-color:#80ff80;}"
RED_BG = "QWidget {background-color:#ffcccc;}"
Expand Down

0 comments on commit 7d7989f

Please sign in to comment.