diff --git a/torba/orchstr8/cli.py b/torba/orchstr8/cli.py index 0dc7343e6..523e0c1dd 100644 --- a/torba/orchstr8/cli.py +++ b/torba/orchstr8/cli.py @@ -3,8 +3,8 @@ import argparse import asyncio import aiohttp -from .node import Conductor, get_ledger_from_environment, get_blockchain_node_from_ledger -from .service import ConductorService +from torba.orchstr8.node import Conductor, get_ledger_from_environment, get_blockchain_node_from_ledger +from torba.orchstr8.service import ConductorService def get_argument_parser(): @@ -46,6 +46,7 @@ def main(): return start_app() loop = asyncio.get_event_loop() + asyncio.set_event_loop(loop) ledger = get_ledger_from_environment() if command == 'download': diff --git a/torba/orchstr8/node.py b/torba/orchstr8/node.py index 991ea1241..7e5b834a1 100644 --- a/torba/orchstr8/node.py +++ b/torba/orchstr8/node.py @@ -50,13 +50,19 @@ def get_blockchain_node_from_ledger(ledger_module): ) -def set_logging(ledger_module, level): - logging.getLogger('torba').setLevel(level) - logging.getLogger('torba.client').setLevel(level) - logging.getLogger('torba.server').setLevel(level) - #logging.getLogger('asyncio').setLevel(level) - logging.getLogger('blockchain').setLevel(level) - logging.getLogger(ledger_module.__name__).setLevel(level) +def set_logging(ledger_module, level, handler=None): + modules = [ + 'torba', + 'torba.client', + 'torba.server', + 'blockchain', + ledger_module.__name__ + ] + for module_name in modules: + module = logging.getLogger(module_name) + module.setLevel(level) + if handler is not None: + module.addHandler(handler) class Conductor: @@ -184,6 +190,7 @@ class SPVNode: self.controller = None self.data_path = None self.server = None + self.port = 1984 async def start(self): self.data_path = tempfile.mkdtemp() @@ -191,7 +198,7 @@ class SPVNode: 'DB_DIRECTORY': self.data_path, 'DAEMON_URL': 'http://rpcuser:rpcpassword@localhost:50001/', 'REORG_LIMIT': '100', - 'TCP_PORT': '1984' + 'TCP_PORT': str(self.port) } os.environ.update(conf) self.server = Server(Env(self.coin_class)) @@ -250,6 +257,7 @@ class BlockchainNode: self.protocol = None self.transport = None self._block_expected = 0 + self.port = 50001 def is_expected_block(self, e: BlockHeightEvent): return self._block_expected == e.height @@ -303,7 +311,7 @@ class BlockchainNode: self.daemon_bin, '-datadir={}'.format(self.data_path), '-printtoconsole', '-regtest', '-server', '-txindex', - '-rpcuser=rpcuser', '-rpcpassword=rpcpassword', '-rpcport=50001' + '-rpcuser=rpcuser', '-rpcpassword=rpcpassword', f'-rpcport={self.port}' ) self.log.info(' '.join(command)) self.transport, self.protocol = await loop.subprocess_exec( diff --git a/torba/orchstr8/service.py b/torba/orchstr8/service.py index 23a7ad3f8..d57bf5a26 100644 --- a/torba/orchstr8/service.py +++ b/torba/orchstr8/service.py @@ -2,7 +2,9 @@ import asyncio import logging from aiohttp.web import Application, WebSocketResponse, json_response from aiohttp.http_websocket import WSMsgType, WSCloseCode -from .node import Conductor + +from torba.client.util import satoshis_to_coins +from .node import Conductor, set_logging PORT = 7954 @@ -56,30 +58,15 @@ class ConductorService: await self.app.cleanup() async def start_stack(self, _): - handler = WebSocketLogHandler(self.send_message) - logging.getLogger('blockchain').setLevel(logging.DEBUG) - logging.getLogger('blockchain').addHandler(handler) - logging.getLogger('electrumx').setLevel(logging.DEBUG) - logging.getLogger('electrumx').addHandler(handler) - logging.getLogger('Controller').setLevel(logging.DEBUG) - logging.getLogger('Controller').addHandler(handler) - logging.getLogger('LBRYBlockProcessor').setLevel(logging.DEBUG) - logging.getLogger('LBRYBlockProcessor').addHandler(handler) - logging.getLogger('LBCDaemon').setLevel(logging.DEBUG) - logging.getLogger('LBCDaemon').addHandler(handler) - logging.getLogger('torba').setLevel(logging.DEBUG) - logging.getLogger('torba').addHandler(handler) - logging.getLogger(self.stack.ledger_module.__name__).setLevel(logging.DEBUG) - logging.getLogger(self.stack.ledger_module.__name__).addHandler(handler) - logging.getLogger(self.stack.ledger_module.__electrumx__.split('.')[0]).setLevel(logging.DEBUG) - logging.getLogger(self.stack.ledger_module.__electrumx__.split('.')[0]).addHandler(handler) - #await self.stack.start() + set_logging( + self.stack.ledger_module, logging.DEBUG, WebSocketLogHandler(self.send_message) + ) self.stack.blockchain_started or await self.stack.start_blockchain() - self.send_message({'type': 'service', 'name': 'blockchain'}) + self.send_message({'type': 'service', 'name': 'blockchain', 'port': self.stack.blockchain_node.port}) self.stack.spv_started or await self.stack.start_spv() - self.send_message({'type': 'service', 'name': 'spv'}) + self.send_message({'type': 'service', 'name': 'spv', 'port': self.stack.spv_node.port}) self.stack.wallet_started or await self.stack.start_wallet() - self.send_message({'type': 'service', 'name': 'wallet'}) + self.send_message({'type': 'service', 'name': 'wallet', 'port': ''}) self.stack.wallet_node.ledger.on_header.listen(self.on_status) self.stack.wallet_node.ledger.on_transaction.listen(self.on_status) return json_response({'started': True}) @@ -138,10 +125,10 @@ class ConductorService: self.send_message({ 'type': 'status', 'height': self.stack.wallet_node.ledger.headers.height, - 'balance': await self.stack.wallet_node.account.get_balance(), + 'balance': satoshis_to_coins(await self.stack.wallet_node.account.get_balance()), 'miner': await self.stack.blockchain_node.get_balance() }) def send_message(self, msg): for web_socket in self.app['websockets']: - asyncio.ensure_future(web_socket.send_json(msg)) + self.loop.create_task(web_socket.send_json(msg)) diff --git a/torba/workbench/Makefile b/torba/workbench/Makefile new file mode 100644 index 000000000..524c22557 --- /dev/null +++ b/torba/workbench/Makefile @@ -0,0 +1,5 @@ +all: _blockchain_dock.py _output_dock.py +_blockchain_dock.py: blockchain_dock.ui + pyside2-uic -d blockchain_dock.ui -o _blockchain_dock.py +_output_dock.py: output_dock.ui + pyside2-uic -d output_dock.ui -o _output_dock.py diff --git a/torba/workbench/__init__.py b/torba/workbench/__init__.py new file mode 100644 index 000000000..3449276fd --- /dev/null +++ b/torba/workbench/__init__.py @@ -0,0 +1 @@ +from .application import main diff --git a/torba/workbench/_blockchain_dock.py b/torba/workbench/_blockchain_dock.py new file mode 100644 index 000000000..2a7cc11d8 --- /dev/null +++ b/torba/workbench/_blockchain_dock.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'blockchain_dock.ui', +# licensing of 'blockchain_dock.ui' applies. +# +# Created: Sun Jan 13 02:56:21 2019 +# by: pyside2-uic running on PySide2 5.12.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_BlockchainDock(object): + def setupUi(self, BlockchainDock): + BlockchainDock.setObjectName("BlockchainDock") + BlockchainDock.resize(416, 167) + BlockchainDock.setFloating(False) + BlockchainDock.setFeatures(QtWidgets.QDockWidget.AllDockWidgetFeatures) + self.dockWidgetContents = QtWidgets.QWidget() + self.dockWidgetContents.setObjectName("dockWidgetContents") + self.formLayout = QtWidgets.QFormLayout(self.dockWidgetContents) + self.formLayout.setObjectName("formLayout") + self.generate = QtWidgets.QPushButton(self.dockWidgetContents) + self.generate.setObjectName("generate") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.generate) + self.blocks = QtWidgets.QSpinBox(self.dockWidgetContents) + self.blocks.setMinimum(1) + self.blocks.setMaximum(9999) + self.blocks.setProperty("value", 1) + self.blocks.setObjectName("blocks") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.blocks) + self.transfer = QtWidgets.QPushButton(self.dockWidgetContents) + self.transfer.setObjectName("transfer") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.transfer) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.amount = QtWidgets.QDoubleSpinBox(self.dockWidgetContents) + self.amount.setSuffix("") + self.amount.setMaximum(9999.99) + self.amount.setProperty("value", 10.0) + self.amount.setObjectName("amount") + self.horizontalLayout.addWidget(self.amount) + self.to_label = QtWidgets.QLabel(self.dockWidgetContents) + self.to_label.setObjectName("to_label") + self.horizontalLayout.addWidget(self.to_label) + self.address = QtWidgets.QLineEdit(self.dockWidgetContents) + self.address.setObjectName("address") + self.horizontalLayout.addWidget(self.address) + self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout) + self.invalidate = QtWidgets.QPushButton(self.dockWidgetContents) + self.invalidate.setObjectName("invalidate") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.invalidate) + self.block_hash = QtWidgets.QLineEdit(self.dockWidgetContents) + self.block_hash.setObjectName("block_hash") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.block_hash) + BlockchainDock.setWidget(self.dockWidgetContents) + + self.retranslateUi(BlockchainDock) + QtCore.QMetaObject.connectSlotsByName(BlockchainDock) + + def retranslateUi(self, BlockchainDock): + BlockchainDock.setWindowTitle(QtWidgets.QApplication.translate("BlockchainDock", "Blockchain", None, -1)) + self.generate.setText(QtWidgets.QApplication.translate("BlockchainDock", "generate", None, -1)) + self.blocks.setSuffix(QtWidgets.QApplication.translate("BlockchainDock", " block(s)", None, -1)) + self.transfer.setText(QtWidgets.QApplication.translate("BlockchainDock", "transfer", None, -1)) + self.to_label.setText(QtWidgets.QApplication.translate("BlockchainDock", "to", None, -1)) + self.address.setPlaceholderText(QtWidgets.QApplication.translate("BlockchainDock", "recipient address", None, -1)) + self.invalidate.setText(QtWidgets.QApplication.translate("BlockchainDock", "invalidate", None, -1)) + self.block_hash.setPlaceholderText(QtWidgets.QApplication.translate("BlockchainDock", "block hash", None, -1)) + diff --git a/torba/workbench/_output_dock.py b/torba/workbench/_output_dock.py new file mode 100644 index 000000000..980343735 --- /dev/null +++ b/torba/workbench/_output_dock.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'output_dock.ui', +# licensing of 'output_dock.ui' applies. +# +# Created: Sat Oct 27 16:41:03 2018 +# by: pyside2-uic running on PySide2 5.11.2 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_OutputDock(object): + def setupUi(self, OutputDock): + OutputDock.setObjectName("OutputDock") + OutputDock.resize(700, 397) + OutputDock.setFloating(False) + OutputDock.setFeatures(QtWidgets.QDockWidget.AllDockWidgetFeatures) + self.dockWidgetContents = QtWidgets.QWidget() + self.dockWidgetContents.setObjectName("dockWidgetContents") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.dockWidgetContents) + self.horizontalLayout.setObjectName("horizontalLayout") + self.textEdit = QtWidgets.QTextEdit(self.dockWidgetContents) + self.textEdit.setReadOnly(True) + self.textEdit.setObjectName("textEdit") + self.horizontalLayout.addWidget(self.textEdit) + OutputDock.setWidget(self.dockWidgetContents) + + self.retranslateUi(OutputDock) + QtCore.QMetaObject.connectSlotsByName(OutputDock) + + def retranslateUi(self, OutputDock): + OutputDock.setWindowTitle(QtWidgets.QApplication.translate("OutputDock", "Output", None, -1)) + diff --git a/torba/workbench/application.py b/torba/workbench/application.py new file mode 100644 index 000000000..56b282844 --- /dev/null +++ b/torba/workbench/application.py @@ -0,0 +1,401 @@ +import sys +import json +import math + +from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork, QtWebSockets, QtSvg + +from torba.workbench._output_dock import Ui_OutputDock as OutputDock +from torba.workbench._blockchain_dock import Ui_BlockchainDock as BlockchainDock + + +def dict_to_post_data(d): + query = QtCore.QUrlQuery() + for key, value in d.items(): + query.addQueryItem(str(key), str(value)) + return QtCore.QByteArray(query.toString().encode()) + + +class LoggingOutput(QtWidgets.QDockWidget, OutputDock): + + def __init__(self, title, parent): + super().__init__(parent) + self.setupUi(self) + self.setWindowTitle(title) + + +class BlockchainControls(QtWidgets.QDockWidget, BlockchainDock): + + def __init__(self, parent): + super().__init__(parent) + self.setupUi(self) + self.generate.clicked.connect(self.on_generate) + self.transfer.clicked.connect(self.on_transfer) + + def on_generate(self): + print('generating') + self.parent().run_command('generate', blocks=self.blocks.value()) + + def on_transfer(self): + print('transfering') + self.parent().run_command('transfer', amount=self.amount.value()) + + +class Arrow(QtWidgets.QGraphicsLineItem): + + def __init__(self, start_node, end_node, parent=None, scene=None): + super().__init__(parent, scene) + self.start_node = start_node + self.start_node.connect_arrow(self) + self.end_node = end_node + self.end_node.connect_arrow(self) + self.arrow_head = QtGui.QPolygonF() + self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) + self.setZValue(-1000.0) + self.arrow_color = QtCore.Qt.black + self.setPen(QtGui.QPen( + self.arrow_color, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin + )) + + def boundingRect(self): + extra = (self.pen().width() + 20) / 2.0 + p1 = self.line().p1() + p2 = self.line().p2() + size = QtCore.QSizeF(p2.x() - p1.x(), p2.y() - p1.y()) + return QtCore.QRectF(p1, size).normalized().adjusted(-extra, -extra, extra, extra) + + def shape(self): + path = super().shape() + path.addPolygon(self.arrow_head) + return path + + def update_position(self): + line = QtCore.QLineF( + self.mapFromItem(self.start_node, 0, 0), + self.mapFromItem(self.end_node, 0, 0) + ) + self.setLine(line) + + def paint(self, painter, option, widget=None): + if self.start_node.collidesWithItem(self.end_node): + return + + start_node = self.start_node + end_node = self.end_node + color = self.arrow_color + pen = self.pen() + pen.setColor(self.arrow_color) + arrow_size = 20.0 + painter.setPen(pen) + painter.setBrush(self.arrow_color) + + end_rectangle = end_node.sceneBoundingRect() + start_center = start_node.sceneBoundingRect().center() + end_center = end_rectangle.center() + center_line = QtCore.QLineF(start_center, end_center) + end_polygon = QtGui.QPolygonF(end_rectangle) + p1 = end_polygon.at(0) + + intersect_point = QtCore.QPointF() + for p2 in end_polygon: + poly_line = QtCore.QLineF(p1, p2) + intersect_type, intersect_point = poly_line.intersect(center_line) + if intersect_type == QtCore.QLineF.BoundedIntersection: + break + p1 = p2 + + self.setLine(QtCore.QLineF(intersect_point, start_center)) + line = self.line() + + angle = math.acos(line.dx() / line.length()) + if line.dy() >= 0: + angle = (math.pi * 2.0) - angle + + arrow_p1 = line.p1() + QtCore.QPointF( + math.sin(angle + math.pi / 3.0) * arrow_size, + math.cos(angle + math.pi / 3.0) * arrow_size + ) + arrow_p2 = line.p1() + QtCore.QPointF( + math.sin(angle + math.pi - math.pi / 3.0) * arrow_size, + math.cos(angle + math.pi - math.pi / 3.0) * arrow_size + ) + + self.arrow_head.clear() + for point in [line.p1(), arrow_p1, arrow_p2]: + self.arrow_head.append(point) + + painter.drawLine(line) + painter.drawPolygon(self.arrow_head) + if self.isSelected(): + painter.setPen(QtGui.QPen(color, 1, QtCore.Qt.DashLine)) + line = QtCore.QLineF(line) + line.translate(0, 4.0) + painter.drawLine(line) + line.translate(0, -8.0) + painter.drawLine(line) + + +ONLINE_COLOR = "limegreen" +OFFLINE_COLOR = "lightsteelblue" + + +class NodeItem(QtSvg.QGraphicsSvgItem): + + def __init__(self, context_menu): + super().__init__() + self._port = '' + self._color = OFFLINE_COLOR + self.context_menu = context_menu + self.arrows = set() + self.renderer = QtSvg.QSvgRenderer() + self.update_svg() + self.setSharedRenderer(self.renderer) + #self.setScale(2.0) + #self.setTransformOriginPoint(24, 24) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) + + def get_svg(self): + return self.SVG.format( + port=self.port, + color=self._color + ) + + def update_svg(self): + self.renderer.load(QtCore.QByteArray(self.get_svg().encode())) + self.update() + + @property + def port(self): + return self._port + + @port.setter + def port(self, port): + self._port = port + self.update_svg() + + @property + def online(self): + return self._color == ONLINE_COLOR + + @online.setter + def online(self, online): + if online: + self._color = ONLINE_COLOR + else: + self._color = OFFLINE_COLOR + self.update_svg() + + def connect_arrow(self, arrow): + self.arrows.add(arrow) + + def disconnect_arrow(self, arrow): + self.arrows.discard(arrow) + + def contextMenuEvent(self, event): + self.scene().clearSelection() + self.setSelected(True) + self.myContextMenu.exec_(event.screenPos()) + + def itemChange(self, change, value): + if change == QtWidgets.QGraphicsItem.ItemPositionChange: + for arrow in self.arrows: + arrow.update_position() + return value + + +class BlockchainNode(NodeItem): + SVG = """ + + + + + {port} + {block} + + """ + + def __init__(self, *args): + self._block_height = '' + super().__init__(*args) + + @property + def block_height(self): + return self._block_height + + @block_height.setter + def block_height(self, block_height): + self._block_height = block_height + self.update_svg() + + def get_svg(self): + return self.SVG.format( + port=self.port, + block=self.block_height, + color=self._color + ) + + +class SPVNode(NodeItem): + SVG = """ + + + + + + + {port} + + """ + + def __init__(self, *args): + super().__init__(*args) + + +class WalletNode(NodeItem): + SVG = """ + + + + + + + + {coins} + + """ + + def __init__(self, *args): + self._coins = '--' + super().__init__(*args) + + @property + def coins(self): + return self._coins + + @coins.setter + def coins(self, coins): + self._coins = coins + self.update_svg() + + def get_svg(self): + return self.SVG.format( + coins=self.coins, + color=self._color + ) + + +class Stage(QtWidgets.QGraphicsScene): + + def __init__(self, parent): + super().__init__(parent) + self.blockchain = b = BlockchainNode(None) + b.port = '' + b.block_height = '' + b.setZValue(0) + b.setPos(-25, -100) + self.addItem(b) + self.spv = s = SPVNode(None) + s.port = '' + s.setZValue(0) + self.addItem(s) + s.setPos(-10, -10) + self.wallet = w = WalletNode(None) + w.coins = '' + w.setZValue(0) + w.update_svg() + self.addItem(w) + w.setPos(0, 100) + + self.addItem(Arrow(b, s)) + self.addItem(Arrow(s, w)) + + +class Orchstr8Workbench(QtWidgets.QMainWindow): + + def __init__(self): + super().__init__() + self.stage = Stage(self) + self.view = QtWidgets.QGraphicsView(self.stage) + self.status_bar = QtWidgets.QStatusBar(self) + + self.setWindowTitle('Orchstr8 Workbench') + self.setCentralWidget(self.view) + self.setStatusBar(self.status_bar) + + self.block_height = self.make_status_label('Height: -- ') + self.user_balance = self.make_status_label('User Balance: -- ') + self.mining_balance = self.make_status_label('Mining Balance: -- ') + + self.wallet_log = LoggingOutput('Wallet', self) + self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.wallet_log) + self.spv_log = LoggingOutput('SPV Server', self) + self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.spv_log) + self.blockchain_log = LoggingOutput('Blockchain', self) + self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.blockchain_log) + + self.blockchain_controls = BlockchainControls(self) + self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.blockchain_controls) + + self.network = QtNetwork.QNetworkAccessManager(self) + self.socket = QtWebSockets.QWebSocket() + self.socket.connected.connect(lambda: self.run_command('start')) + self.socket.error.connect(lambda e: print(f'errored: {e}')) + self.socket.textMessageReceived.connect(self.on_message) + self.socket.open('ws://localhost:7954/log') + + def make_status_label(self, text): + label = QtWidgets.QLabel(text) + label.setFrameStyle(QtWidgets.QLabel.Panel | QtWidgets.QLabel.Sunken) + self.status_bar.addPermanentWidget(label) + return label + + def on_message(self, text): + msg = json.loads(text) + if msg['type'] == 'status': + self.stage.wallet.coins = msg['balance'] + self.stage.blockchain.block_height = msg['height'] + self.block_height.setText(f"Height: {msg['height']} ") + self.user_balance.setText(f"User Balance: {msg['balance']} ") + self.mining_balance.setText(f"Mining Balance: {msg['miner']} ") + elif msg['type'] == 'service': + node = { + 'blockchain': self.stage.blockchain, + 'spv': self.stage.spv, + 'wallet': self.stage.wallet + }[msg['name']] + node.online = True + node.port = f":{msg['port']}" + elif msg['type'] == 'log': + log = { + 'blockchain': self.blockchain_log, + 'electrumx': self.spv_log, + 'lbryumx': self.spv_log, + 'Controller': self.spv_log, + 'LBRYBlockProcessor': self.spv_log, + 'LBCDaemon': self.spv_log, + }.get(msg['name'].split('.')[-1], self.wallet_log) + log.textEdit.append(msg['message']) + + def run_command(self, command, **kwargs): + request = QtNetwork.QNetworkRequest(QtCore.QUrl('http://localhost:7954/'+command)) + request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded") + reply = self.network.post(request, dict_to_post_data(kwargs)) + # reply.finished.connect(cb) + reply.error.connect(self.on_command_error) + + @staticmethod + def on_command_error(error): + print('failed executing command:') + print(error) + + +def main(): + app = QtWidgets.QApplication(sys.argv) + workbench = Orchstr8Workbench() + workbench.setGeometry(100, 100, 1200, 600) + workbench.show() + return app.exec_() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/torba/workbench/blockchain_dock.ui b/torba/workbench/blockchain_dock.ui new file mode 100644 index 000000000..2946b839d --- /dev/null +++ b/torba/workbench/blockchain_dock.ui @@ -0,0 +1,104 @@ + + + BlockchainDock + + + + 0 + 0 + 416 + 167 + + + + false + + + QDockWidget::AllDockWidgetFeatures + + + Blockchain + + + + + + + generate + + + + + + + block(s) + + + 1 + + + 9999 + + + 1 + + + + + + + transfer + + + + + + + + + + + + 9999.989999999999782 + + + 10.000000000000000 + + + + + + + to + + + + + + + recipient address + + + + + + + + + invalidate + + + + + + + block hash + + + + + + + + + diff --git a/torba/workbench/output_dock.ui b/torba/workbench/output_dock.ui new file mode 100644 index 000000000..3e1136659 --- /dev/null +++ b/torba/workbench/output_dock.ui @@ -0,0 +1,36 @@ + + + OutputDock + + + + 0 + 0 + 700 + 397 + + + + false + + + QDockWidget::AllDockWidgetFeatures + + + Output + + + + + + + true + + + + + + + + +