diff --git a/doc/img/00.png b/docs/img/00.png similarity index 100% rename from doc/img/00.png rename to docs/img/00.png diff --git a/doc/img/01.png b/docs/img/01.png similarity index 100% rename from doc/img/01.png rename to docs/img/01.png diff --git a/doc/img/02.png b/docs/img/02.png similarity index 100% rename from doc/img/02.png rename to docs/img/02.png diff --git a/doc/img/03.png b/docs/img/03.png similarity index 100% rename from doc/img/03.png rename to docs/img/03.png diff --git a/doc/img/04.png b/docs/img/04.png similarity index 100% rename from doc/img/04.png rename to docs/img/04.png diff --git a/doc/img/05.png b/docs/img/05.png similarity index 100% rename from doc/img/05.png rename to docs/img/05.png diff --git a/doc/img/06.png b/docs/img/06.png similarity index 100% rename from doc/img/06.png rename to docs/img/06.png diff --git a/doc/img/07.png b/docs/img/07.png similarity index 100% rename from doc/img/07.png rename to docs/img/07.png diff --git a/doc/img/08.png b/docs/img/08.png similarity index 100% rename from doc/img/08.png rename to docs/img/08.png diff --git a/doc/img/09.png b/docs/img/09.png similarity index 100% rename from doc/img/09.png rename to docs/img/09.png diff --git a/doc/img/10.png b/docs/img/10.png similarity index 100% rename from doc/img/10.png rename to docs/img/10.png diff --git a/img/icon_delete.png b/img/icon_delete.png new file mode 100644 index 0000000..81e5e51 Binary files /dev/null and b/img/icon_delete.png differ diff --git a/img/icon_edit.png b/img/icon_edit.png new file mode 100644 index 0000000..15184ea Binary files /dev/null and b/img/icon_edit.png differ diff --git a/img/icon_greenConn.png b/img/icon_greenConn.png new file mode 100644 index 0000000..8719126 Binary files /dev/null and b/img/icon_greenConn.png differ diff --git a/img/icon_lastBlock.png b/img/icon_lastBlock.png new file mode 100644 index 0000000..a782fba Binary files /dev/null and b/img/icon_lastBlock.png differ diff --git a/img/icon_orangeConn.png b/img/icon_orangeConn.png new file mode 100644 index 0000000..e212f85 Binary files /dev/null and b/img/icon_orangeConn.png differ diff --git a/img/icon_redConn.png b/img/icon_redConn.png new file mode 100644 index 0000000..064af96 Binary files /dev/null and b/img/icon_redConn.png differ diff --git a/img/icon_refresh.png b/img/icon_refresh.png new file mode 100644 index 0000000..6cb5506 Binary files /dev/null and b/img/icon_refresh.png differ diff --git a/pet4l.py b/pet4l.py index 0b746f9..129297d 100644 --- a/pet4l.py +++ b/pet4l.py @@ -1,31 +1,50 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -from PyQt5.QtWidgets import QApplication -from PyQt5.Qt import Qt, QPixmap, QSplashScreen, QProgressBar, QColor, QPalette, QLabel -from mainApp import App - - -if __name__ == '__main__': - # Create App - app = QApplication(sys.argv) - - if getattr( sys, 'frozen', False ) : - # running in a bundle - imgDir = os.path.join(sys._MEIPASS, 'img') - - else: - # running live - imgDir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'img') - - ### -------------- - - # Create QMainWindow Widget - ex = App(imgDir) - - # Execute App - sys.exit(app.exec_()) - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +import sys + + +if __name__ == '__main__': + # parse input if there's `--clear[?]Data` flags + import argparse + parser = argparse.ArgumentParser(description='PET4L') + parser.add_argument('--clearAppData', dest='clearAppData', action='store_true', + help='clear all previously saved application data') + parser.set_defaults(clearAppData=False) + args = parser.parse_args() + + if getattr(sys, 'frozen', False): + # running in a bundle + sys.path.append(os.path.join(sys._MEIPASS, 'src')) + imgDir = os.path.join(sys._MEIPASS, 'img') + + # if linux export qt plugins path + if sys.platform == 'linux': + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(sys._MEIPASS, 'PyQt5', 'Qt', 'plugins') + + else: + # running live + sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src')) + imgDir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'img') + + from PyQt5.QtWidgets import QApplication + from mainApp import App + + # Create App + app = QApplication(sys.argv) + + ### -------------- + + # Create QMainWindow Widget + ex = App(imgDir, args) + + # Execute App + app.exec_() + try: + app.deleteLater() + except Exception as e: + print(e) + + sys.exit() + + diff --git a/specPet4l.spec b/specPet4l.spec index c606fcb..74c19d6 100644 --- a/specPet4l.spec +++ b/specPet4l.spec @@ -1,129 +1,154 @@ -# -*- mode: python -*- -import sys -import os -import os.path -import simplejson as json - -os_type = sys.platform -block_cipher = None -base_dir = os.path.dirname(os.path.realpath('__file__')) - -# look for version string -version_str = '' -with open(os.path.join(base_dir, 'src', 'version.txt')) as version_file: - version_data = json.load(version_file) -version_file.close() -version_str = version_data["number"] + version_data["tag"] - -add_files = [('src/version.txt', '.'), ('img', 'img')] - -lib_path = next(p for p in sys.path if 'site-packages' in p) -if os_type == 'win32': - qt5_path = os.path.join(lib_path, 'PyQt5\\Qt\\bin') - sys.path.append(qt5_path) - # add file vcruntime140.dll manually, due to not including by pyinstaller - found = False - for p in os.environ["PATH"].split(os.pathsep): - file_name = os.path.join(p, "vcruntime140.dll") - if os.path.exists(file_name): - found = True - add_files.append((file_name, '')) - print('Adding file ' + file_name) - break - if not found: - raise Exception('File vcruntime140.dll not found in the system path.') - -# add bitcoin library data file -add_files.append( (os.path.join(lib_path, 'bitcoin/english.txt'),'bitcoin') ) - -a = Analysis(['pet4l.py'], - pathex=[base_dir, 'src', 'src/qt'], - binaries=[], - datas=add_files, - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher) - -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) - -exe = EXE(pyz, - a.scripts, - exclude_binaries=True, - name='pet4l', - debug=False, - strip=False, - upx=True, - console=False, - icon=os.path.join(base_dir, 'img', 'spmt.%s' % ('icns' if os_type=='darwin' else 'ico')) ) - -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - name='app') - -if os_type == 'darwin': - app = BUNDLE(coll, - name='pet4l.app', - icon=os.path.join(base_dir, 'img', 'spmt.icns'), - bundle_identifier=None, - info_plist={'NSHighResolutionCapable': 'True'}) - - -# Prepare bundles -dist_path = os.path.join(base_dir, 'dist') -app_path = os.path.join(dist_path, 'app') -os.chdir(dist_path) - -# Copy Readme Files -from shutil import copyfile -print('Copying README.md') -copyfile(os.path.join(base_dir, 'README.md'), 'README.md') - - -if os_type == 'win32': - # Copy Qt5 Platforms - os.system('xcopy app\PyQt5\Qt\plugins\platforms app\platforms\ /i') - os.chdir(base_dir) - # Rename dist Dir - dist_path_win = os.path.join(base_dir, 'PET4L-v' + version_str + '-Win64') - os.rename(dist_path, dist_path_win) - # Compress dist Dir - print('Compressing Windows App Folder') - os.system('"C:\\Program Files\\7-Zip\\7z.exe" a %s %s -mx0' % (dist_path_win + '.zip', dist_path_win)) - - -if os_type == 'linux': - os.chdir(base_dir) - # Rename dist Dir - dist_path_linux = os.path.join(base_dir, 'PET4L-v' + version_str) - os.rename(dist_path, dist_path_linux) - # Compress dist Dir - print('Compressing Linux App Folder') - os.system('tar -zcvf %s -C %s %s' % ('PET4L-v' + version_str + '-x86_64-gnu_linux.tar.gz', - base_dir, 'PET4L-v' + version_str)) - - -if os_type == 'darwin': - os.chdir(base_dir) - # Rename dist Dir - dist_path_mac = os.path.join(base_dir, 'PET4L-v' + version_str + '-MacOSX') - os.rename(dist_path, dist_path_mac) - # Remove 'app' folder - print("Removin 'app' folder") - os.chdir(dist_path_mac) - os.system('rm -rf app') - os.chdir(base_dir) - # Compress dist Dir - print('Compressing Mac App Folder') - os.system('tar -zcvf %s -C %s %s' % ('PET4L-v' + version_str + '-MacOSX.tar.gz', - base_dir, 'PET4L-v' + version_str + '-MacOSX')) - - +# -*- mode: python -*- +import sys +import os.path as os_path +import simplejson as json + +os_type = sys.platform +block_cipher = None +base_dir = os_path.dirname(os_path.realpath('__file__')) + +def libModule(module, source, dest): + m = __import__(module) + module_path = os_path.dirname(m.__file__) + del m + print("libModule %s" % str(( os_path.join(module_path, source), dest ))) + return ( os_path.join(module_path, source), dest ) + +# look for version string +version_str = '' +with open(os_path.join(base_dir, 'src', 'version.txt')) as version_file: + version_data = json.load(version_file) +version_file.close() +version_str = version_data["number"] + version_data["tag"] + +add_files = [('src/version.txt', '.'), ('img', 'img')] +add_files.append( libModule('bitcoin', 'english.txt','bitcoin') ) + +if os_type == 'win32': + import ctypes.util + l = ctypes.util.find_library('libusb-1.0.dll') + if l: + add_files.append( (l, '.') ) + + +a = Analysis(['pet4l.py'], + pathex=[base_dir, 'src', 'src/qt'], + binaries=[], + datas=add_files, + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[ 'numpy', + 'cryptography', + 'lib2to3', + 'pkg_resources', + 'distutils', + 'Crypto', + 'pyi_rth_qt5', + 'pytest', + 'scipy', + 'pycparser', + 'pydoc', + 'PyQt5.QtHelp', + 'PyQt5.QtMultimedia', + 'PyQt5.QtNetwork', + 'PyQt5.QtOpenGL', + 'PyQt5.QtPrintSupport', + 'PyQt5.QtQml', + 'PyQt5.QtQuick', + 'PyQt5.QtQuickWidgets', + 'PyQt5.QtSensors', + 'PyQt5.QtSerialPort', + 'PyQt5.QtSql', + 'PyQt5.QtSvg', + 'PyQt5.QtTest', + 'PyQt5.QtWebEngine', + 'PyQt5.QtWebEngineCore', + 'PyQt5.QtWebEngineWidgets', + 'PyQt5.QtXml', + 'win32com', + 'xml.dom.domreg', + ], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher) + +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) + +exe = EXE(pyz, + a.scripts, + exclude_binaries=True, + name='PET4L', + debug=False, + strip=False, + upx=False, + console=False, + icon=os_path.join(base_dir, 'img', 'spmt.%s' % ('icns' if os_type=='darwin' else 'ico')) ) + +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + name='app') + +if os_type == 'darwin': + app = BUNDLE(coll, + name='PET4L.app', + icon=os_path.join(base_dir, 'img', 'spmt.icns'), + bundle_identifier=None, + info_plist={'NSHighResolutionCapable': 'True'}) + + +# Prepare bundles +dist_path = os_path.join(base_dir, 'dist') +app_path = os_path.join(dist_path, 'app') +os.chdir(dist_path) + +# Copy Readme Files +from shutil import copyfile, copytree +print('Copying README.md') +copyfile(os_path.join(base_dir, 'README.md'), 'README.md') +copytree(os_path.join(base_dir, 'docs'), 'docs') + +if os_type == 'win32': + # Copy Qt5 Platforms + os.system('xcopy app\PyQt5\Qt\plugins\platforms app\platforms\ /i') + os.chdir(base_dir) + # Rename dist Dir + dist_path_win = os_path.join(base_dir, 'PET4L-v' + version_str + '-Win64') + os.rename(dist_path, dist_path_win) + # Compress dist Dir + print('Compressing Windows App Folder') + os.system('"C:\\Program Files\\7-Zip\\7z.exe" a %s %s -mx0' % (dist_path_win + '.zip', dist_path_win)) + + +if os_type == 'linux': + os.chdir(base_dir) + # Rename dist Dir + dist_path_linux = os_path.join(base_dir, 'PET4L-v' + version_str + '-gnu_linux') + os.rename(dist_path, dist_path_linux) + # Compress dist Dir + print('Compressing Linux App Folder') + os.system('tar -zcvf %s -C %s %s' % ('PET4L-v' + version_str + '-x86_64-gnu_linux.tar.gz', + base_dir, 'PET4L-v' + version_str + '-gnu_linux')) + + +if os_type == 'darwin': + os.chdir(base_dir) + # Rename dist Dir + dist_path_mac = os_path.join(base_dir, 'PET4L-v' + version_str + '-MacOSX') + os.rename(dist_path, dist_path_mac) + # Remove 'app' folder + print("Removin 'app' folder") + os.chdir(dist_path_mac) + os.system('rm -rf app') + os.chdir(base_dir) + # Compress dist Dir + print('Compressing Mac App Folder') + os.system('tar -zcvf %s -C %s %s' % ('PET4L-v' + version_str + '-MacOSX.tar.gz', + base_dir, 'PET4L-v' + version_str + '-MacOSX')) + + diff --git a/src/apiClient.py b/src/apiClient.py index b4440ba..f17931e 100644 --- a/src/apiClient.py +++ b/src/apiClient.py @@ -1,98 +1,42 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import requests -from misc import getCallerName, getFunctionName, printException - -class ApiClient: - - def __init__(self): - self.url = "http://chainz.cryptoid.info/pivx/api.dws" - self.parameters = {"key": "b62b40b5091e"} - - - - - def checkResponse(self, parameters): - resp = requests.get(self.url, params=parameters) - if resp.status_code == 200: - data = resp.json() - return data - else: - print("Invalid response from API provider") - print("Status code: %s" % str(resp.status_code)) - return None - - - - - def getAddressUtxos(self, address): - try: - self.parameters['q'] = 'unspent' - self.parameters['active'] = address - return self.checkResponse(self.parameters) - except Exception as e: - err_msg = "error in getAddressUtxos" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def getBalance(self, address): - try: - self.parameters['q'] = 'getbalance' - self.parameters['a'] = address - return self.checkResponse(self.parameters) - except Exception as e: - err_msg = "error in getAddressUtxos" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def getStatus(self): - try: - self.parameters['q'] = 'getblockcount' - resp = requests.get(self.url, self.parameters) - return resp.status_code - - except Exception as e: - err_msg = "Unable to connect to API provider" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - return 0 - - - - - def getStatusMess(self, statusCode): - message = { - 0: "No response from server", - 200: "OK! Connected"} - - if statusCode in message: - return message[statusCode] - - return "Not Connected! Status: %s" % str(statusCode) - - - - - def getBlockCount(self): - try: - self.parameters['q'] = 'getblockcount' - return self.checkResponse(self.parameters) - except Exception as e: - err_msg = "error in getBlockCount" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def getBlockHash(self, blockNum): - try: - self.parameters['q'] = 'getblockhash' - self.parameters['height'] = str(blockNum) - return self.checkResponse(self.parameters) - except Exception as e: - err_msg = "error in getBlockHash" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from blockbookClient import BlockBookClient +from cryptoIDClient import CryptoIDClient + +from misc import getCallerName, getFunctionName, printException, printError + +def process_api_exceptions(func): + + def process_api_exceptions_int(*args, **kwargs): + client = args[0] + try: + return func(*args, **kwargs) + except Exception as e: + message = "Primary API source not responding. Trying secondary" + printException(getCallerName(True), getFunctionName(True), message, str(e)) + try: + client.api = CryptoIDClient(client.isTestnet) + return func(*args, **kwargs) + + except Exception as e: + printError(getCallerName(True), getFunctionName(True), str(e)) + return None + + return process_api_exceptions_int + +class ApiClient: + + def __init__(self, isTestnet=False): + self.isTestnet = isTestnet + self.api = BlockBookClient(isTestnet) + + + @process_api_exceptions + def getAddressUtxos(self, address): + return self.api.getAddressUtxos(address) + + + @process_api_exceptions + def getBalance(self, address): + return self.api.getBalance(address) + diff --git a/src/blockbookClient.py b/src/blockbookClient.py new file mode 100644 index 0000000..de40a96 --- /dev/null +++ b/src/blockbookClient.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import requests + +from misc import getCallerName, getFunctionName, printException + + + +def process_blockbook_exceptions(func): + + def process_blockbook_exceptions_int(*args, **kwargs): + client = args[0] + try: + return func(*args, **kwargs) + except Exception as e: + if client.isTestnet: + new_url = "https://testnet.pivx.link" + else: + new_url = "https://explorer.pivx.link" + message = "BlockBook Client exception on %s\nTrying backup server %s" % (client.url, new_url) + printException(getCallerName(True), getFunctionName(True), message, str(e)) + + try: + client.url = new_url + return func(*args, **kwargs) + + except Exception: + raise + + return process_blockbook_exceptions_int + + + + +class BlockBookClient: + + def __init__(self, isTestnet=False): + self.isTestnet = isTestnet + if isTestnet: + self.url = "https://blockbook-testnet.pivx.link" + else: + self.url = "https://blockbook.pivx.link" + + + + def checkResponse(self, method, param=""): + url = self.url + "/api/%s" % method + if param != "": + url += "/%s" % param + resp = requests.get(url, data={}, verify=True) + if resp.status_code == 200: + data = resp.json() + return data + raise Exception("Invalid response") + + + + @process_blockbook_exceptions + def getAddressUtxos(self, address): + utxos = self.checkResponse("utxo", address) + # Add script for cryptoID legacy + for u in utxos: + u["script"] = "" + return utxos + + + + @process_blockbook_exceptions + def getBalance(self, address): + return self.checkResponse("address", address)["balance"] + diff --git a/src/constants.py b/src/constants.py index a8657cf..276224c 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,18 +1,46 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path - -APPDATA_DIRNAME = ".PET4L-DATA" -MPATH = "44'/77'/" -WIF_PREFIX = 212 # 212 = d4 -MAGIC_BYTE = 30 -TESTNET_WIF_PREFIX = 239 -TESTNET_MAGIC_BYTE = 139 -DEFAULT_PROTOCOL_VERSION = 70913 -MINIMUM_FEE = 0.0001 # minimum PIV/kB -starting_width = 1033 -starting_height = 785 -home_dir = os.path.expanduser('~') -user_dir = os.path.join(home_dir, APPDATA_DIRNAME) -log_File = os.path.join(user_dir, 'lastLogs.html') \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +import os.path + +APPDATA_DIRNAME = ".PET4L-DATA" + +MPATH_LEDGER = "44'/77'/" +MPATH_TREZOR = "44'/119'/" +MPATH_TESTNET = "44'/1'/" +WIF_PREFIX = 212 # 212 = d4 +MAGIC_BYTE = 30 +TESTNET_WIF_PREFIX = 239 +TESTNET_MAGIC_BYTE = 139 +DEFAULT_PROTOCOL_VERSION = 70915 +MINIMUM_FEE = 0.0001 # minimum PIV/kB +starting_width = 1033 +starting_height = 785 +home_dir = os.path.expanduser('~') +user_dir = os.path.join(home_dir, APPDATA_DIRNAME) +log_File = os.path.join(user_dir, 'debug.log') +database_File = os.path.join(user_dir, 'application.db') + +DefaultCache = { + "lastAddress": "", + "window_width": starting_width, + "window_height": starting_height, + "splitter_x": 342, + "splitter_y": 133, + "console_hidden": False, + "useSwiftX": False, + "selectedHW_index": 0, + "selectedRPC_index": 0, + "isTestnetRPC": False + } + +trusted_RPC_Servers = [ + ["https", "amsterdam.randomzebra.party:8080", "spmtUser_ams", "WUss6sr8956S5Paex254"], + ["https", "losangeles.randomzebra.party:8080", "spmtUser_la", "8X88u7TuefPm7mQaJY52"], + ["https", "singapore.randomzebra.party:8080", "spmtUser_sing", "ZyD936tm9dvqmMP8A777"]] + + +HW_devices = [ + # (model name, api index) + ("LEDGER Nano S", 0), +] diff --git a/src/cryptoIDClient.py b/src/cryptoIDClient.py new file mode 100644 index 0000000..7506212 --- /dev/null +++ b/src/cryptoIDClient.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from random import choice +import requests + +from misc import getCallerName, getFunctionName, printException + +api_keys = ["b62b40b5091e", "f1d66708a077", "ed85c85c0126", "ccc60d06f737"] + + +def process_cryptoID_exceptions(func): + + def process_cryptoID_exceptions_int(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + message = "CryptoID Client exception" + printException(getCallerName(True), getFunctionName(True), message, str(e)) + return None + + return process_cryptoID_exceptions_int + + + +def UTXOS_cryptoID_to_trezor(utxos): + # convert JSON labels + new_utxos = [] + for u in utxos: + new_u = {} + new_u["txid"] = u["tx_hash"] + new_u["vout"] = u["tx_ouput_n"] + new_u["satoshis"] = u["value"] + new_u["confirmations"] = u["confirmations"] + new_u["script"] = u["script"] + new_utxos.append(new_u) + + return new_utxos + + +class CryptoIDClient: + + def __init__(self, isTestnet=False): + if isTestnet: + raise Exception("\nNo CryptoID Testnet server\n") + self.isTestnet = False + self.url = "http://chainz.cryptoid.info/pivx/api.dws" + self.parameters = {} + + + + def checkResponse(self, parameters): + key = choice(api_keys) + parameters['key'] = key + resp = requests.get(self.url, params=parameters) + if resp.status_code == 200: + data = resp.json() + return data + return None + + + + @process_cryptoID_exceptions + def getAddressUtxos(self, address): + self.parameters = {} + self.parameters['q'] = 'unspent' + self.parameters['active'] = address + res = self.checkResponse(self.parameters) + if res is None: + return None + else: + return UTXOS_cryptoID_to_trezor(res['unspent_outputs']) + + + + @process_cryptoID_exceptions + def getBalance(self, address): + self.parameters = {} + self.parameters['q'] = 'getbalance' + self.parameters['a'] = address + return self.checkResponse(self.parameters) + diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..0f18485 --- /dev/null +++ b/src/database.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import logging +import sqlite3 +import threading + +from constants import database_File, trusted_RPC_Servers +from misc import printDbg, getCallerName, getFunctionName, printException + + +class Database(): + + ''' + class methods + ''' + def __init__(self, app): + printDbg("DB: Initializing...") + self.app = app + self.file_name = database_File + self.lock = threading.Lock() + self.isOpen = False + self.conn = None + printDbg("DB: Initialized") + + + + def open(self): + printDbg("DB: Opening...") + if self.isOpen: + raise Exception("Database already open") + + with self.lock: + try: + if self.conn is None: + self.conn = sqlite3.connect(self.file_name) + + self.initTables() + self.conn.commit() + self.conn.close() + self.conn = None + self.isOpen = True + printDbg("DB: Database open") + + except Exception as e: + err_msg = 'SQLite initialization error' + printException(getCallerName(), getFunctionName(), err_msg, e) + + + + def close(self): + printDbg("DB: closing...") + if not self.isOpen: + err_msg = "Database already closed" + printException(getCallerName(), "close()", err_msg, "") + return + + with self.lock: + try: + if self.conn is not None: + self.conn.close() + + self.conn = None + self.isOpen = False + printDbg("DB: Database closed") + + except Exception as e: + err_msg = 'SQLite closing error' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + + + def getCursor(self): + if self.isOpen: + self.lock.acquire() + try: + if self.conn is None: + self.conn = sqlite3.connect(self.file_name) + return self.conn.cursor() + + except Exception as e: + err_msg = 'SQLite error getting cursor' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + self.lock.release() + + else: + raise Exception("Database closed") + + + + def releaseCursor(self, rollingBack=False, vacuum=False): + if self.isOpen: + try: + if self.conn is not None: + # commit + if rollingBack: + self.conn.rollback() + + else: + self.conn.commit() + if vacuum: + self.conn.execute('vacuum') + + # close connection + self.conn.close() + + self.conn = None + + except Exception as e: + err_msg = 'SQLite error releasing cursor' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + finally: + self.lock.release() + + else: + raise Exception("Database closed") + + + + def initTables(self): + printDbg("DB: Initializing tables...") + try: + cursor = self.conn.cursor() + + # Tables for RPC Servers + cursor.execute("CREATE TABLE IF NOT EXISTS PUBLIC_RPC_SERVERS(" + " id INTEGER PRIMARY KEY, protocol TEXT, host TEXT," + " user TEXT, pass TEXT)") + + cursor.execute("CREATE TABLE IF NOT EXISTS CUSTOM_RPC_SERVERS(" + " id INTEGER PRIMARY KEY, protocol TEXT, host TEXT," + " user TEXT, pass TEXT)") + + self.initTable_RPC(cursor) + + # Tables for Utxos + cursor.execute("CREATE TABLE IF NOT EXISTS UTXOS(" + " tx_hash TEXT, tx_ouput_n INTEGER," + " satoshis INTEGER, confirmations INTEGER, script TEXT, raw_tx TEXT, receiver TEXT," + " PRIMARY KEY (tx_hash, tx_ouput_n))") + + printDbg("DB: Tables initialized") + + + except Exception as e: + err_msg = 'error initializing tables' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + + + def initTable_RPC(self, cursor): + s = trusted_RPC_Servers + # Insert Default public trusted servers + cursor.execute("INSERT OR REPLACE INTO PUBLIC_RPC_SERVERS VALUES" + " (?, ?, ?, ?, ?)," + " (?, ?, ?, ?, ?)," + " (?, ?, ?, ?, ?);", + (0, s[0][0], s[0][1], s[0][2], s[0][3], + 1, s[1][0], s[1][1], s[1][2], s[1][3], + 2, s[2][0], s[2][1], s[2][2], s[2][3])) + + # Insert Local wallet + cursor.execute("INSERT OR IGNORE INTO CUSTOM_RPC_SERVERS VALUES" + " (?, ?, ?, ?, ?);", + (0, "http", "127.0.0.1:51473", "rpcUser", "rpcPass")) + + + ''' + General methods + ''' + + def clearTable(self, table_name): + printDbg("DB: Clearing table %s..." % table_name) + cleared_RPC = False + try: + cursor = self.getCursor() + cursor.execute("DELETE FROM %s" % table_name) + # in case, reload default RPC and emit changed signal + if table_name == 'CUSTOM_RPC_SERVERS': + self.initTable_RPC(cursor) + cleared_RPC = True + printDbg("DB: Table %s cleared" % table_name) + + except Exception as e: + err_msg = 'error clearing %s in database' % table_name + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + finally: + self.releaseCursor(vacuum=True) + if cleared_RPC: + self.app.sig_changed_rpcServers.emit() + + + + def removeTable(self, table_name): + printDbg("DB: Dropping table %s..." % table_name) + try: + cursor = self.getCursor() + cursor.execute("DROP TABLE IF EXISTS %s" % table_name) + printDbg("DB: Table %s removed" % table_name) + + except Exception as e: + err_msg = 'error removing table %s from database' % table_name + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + finally: + self.releaseCursor(vacuum=True) + + + + ''' + RPC servers methods + ''' + + def addRPCServer(self, protocol, host, user, passwd): + printDbg("DB: Adding new RPC server...") + added_RPC = False + try: + cursor = self.getCursor() + + cursor.execute("INSERT INTO CUSTOM_RPC_SERVERS (protocol, host, user, pass) " + "VALUES (?, ?, ?, ?)", + (protocol, host, user, passwd) + ) + added_RPC = True + printDbg("DB: RPC server added") + + except Exception as e: + err_msg = 'error adding RPC server entry to DB' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + finally: + self.releaseCursor() + if added_RPC: + self.app.sig_changed_rpcServers.emit() + + + + def editRPCServer(self, protocol, host, user, passwd, id): + printDbg("DB: Editing RPC server with id %d" % id) + changed_RPC = False + try: + cursor = self.getCursor() + + cursor.execute("UPDATE CUSTOM_RPC_SERVERS " + "SET protocol = ?, host = ?, user = ?, pass = ?" + "WHERE id = ?", + (protocol, host, user, passwd, id) + ) + changed_RPC = True + + except Exception as e: + err_msg = 'error editing RPC server entry to DB' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + finally: + self.releaseCursor() + if changed_RPC: + self.app.sig_changed_rpcServers.emit() + + + + def getRPCServers(self, custom, id=None): + tableName = "CUSTOM_RPC_SERVERS" if custom else "PUBLIC_RPC_SERVERS" + if id is not None: + printDbg("DB: Getting RPC server with id %d from table %s" % (id, tableName)) + else: + printDbg("DB: Getting all RPC servers from table %s" % tableName) + try: + cursor = self.getCursor() + if id is None: + cursor.execute("SELECT * FROM %s" % tableName) + else: + cursor.execute("SELECT * FROM %s WHERE id = ?" % tableName, (id,)) + rows = cursor.fetchall() + + except Exception as e: + err_msg = 'error getting RPC servers from database' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + rows = [] + finally: + self.releaseCursor() + + server_list = [] + for row in rows: + server = {} + server["id"] = row[0] + server["protocol"] = row[1] + server["host"] = row[2] + server["user"] = row[3] + server["password"] = row[4] + server["isCustom"] = custom + server_list.append(server) + + if id is not None: + return server_list[0] + + return server_list + + + + def removeRPCServer(self, id): + printDbg("DB: Remove RPC server with id %d" % id) + removed_RPC = False + try: + cursor = self.getCursor() + cursor.execute("DELETE FROM CUSTOM_RPC_SERVERS" + " WHERE id=?", (id,)) + removed_RPC = True + + except Exception as e: + err_msg = 'error removing RPC servers from database' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + finally: + self.releaseCursor(vacuum=True) + if removed_RPC: + self.app.sig_changed_rpcServers.emit() + + + + + ''' + UTXOS methods + ''' + + def rewards_from_rows(self, rows): + rewards = [] + + for row in rows: + # fetch masternode item + utxo = {} + utxo['txid'] = row[0] + utxo['vout'] = row[1] + utxo['satoshis'] = row[2] + utxo['confirmations'] = row[3] + utxo['script'] = row[4] + utxo['raw_tx'] = row[5] + utxo['receiver'] = row[6] + # add to list + rewards.append(utxo) + + return rewards + + + + def addReward(self, utxo): + logging.debug("DB: Adding reward") + try: + cursor = self.getCursor() + + cursor.execute("INSERT INTO UTXOS " + "VALUES (?, ?, ?, ?, ?, ?, ?)", + (utxo['txid'], utxo['vout'], utxo['satoshis'], + utxo['confirmations'], utxo['script'], utxo['raw_tx'], utxo['receiver']) + ) + + except Exception as e: + err_msg = 'error adding reward UTXO to DB' + printException(getCallerName(), getFunctionName(), err_msg, e) + + finally: + self.releaseCursor() + + + + def deleteReward(self, tx_hash, tx_ouput_n): + logging.debug("DB: Deleting reward") + try: + cursor = self.getCursor() + cursor.execute("DELETE FROM UTXOS WHERE tx_hash = ? AND tx_ouput_n = ?", (tx_hash, tx_ouput_n)) + + except Exception as e: + err_msg = 'error deleting UTXO from DB' + printException(getCallerName(), getFunctionName(), err_msg, e.args) + finally: + self.releaseCursor(vacuum=True) + + + + def getReward(self, tx_hash, tx_ouput_n): + logging.debug("DB: Getting reward") + try: + cursor = self.getCursor() + + cursor.execute("SELECT * FROM UTXOS" + " WHERE tx_hash = ? AND tx_ouput_n = ?", (tx_hash, tx_ouput_n)) + rows = cursor.fetchall() + + except Exception as e: + err_msg = 'error getting reward %s-%d' % (tx_hash, tx_ouput_n) + printException(getCallerName(), getFunctionName(), err_msg, e) + rows = [] + finally: + self.releaseCursor() + + return self.rewards_from_rows(rows)[0] + + + + def getRewardsList(self, receiver=None): + try: + cursor = self.getCursor() + + if receiver is None: + printDbg("DB: Getting rewards of all masternodes") + cursor.execute("SELECT * FROM UTXOS") + else: + printDbg("DB: Getting rewards of %s" % receiver) + cursor.execute("SELECT * FROM UTXOS WHERE receiver = ?", (receiver,)) + rows = cursor.fetchall() + + except Exception as e: + err_msg = 'error getting rewards list for %s' % receiver + printException(getCallerName(), getFunctionName(), err_msg, e) + rows = [] + finally: + self.releaseCursor() + + return self.rewards_from_rows(rows) + diff --git a/src/hwdevice.py b/src/hwdevice.py index d47b90b..9f686a6 100644 --- a/src/hwdevice.py +++ b/src/hwdevice.py @@ -1,308 +1,137 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from btchip.btchip import btchip, getDongle, BTChipException -from btchip.btchipUtils import compress_public_key, bitcoinTransaction, bitcoinInput, bitcoinOutput -from bitcoin import bin_hash160 -from time import sleep -from misc import printDbg, printException, printOK, getCallerName, getFunctionName, splitString -from constants import MPATH -from PyQt5.QtWidgets import QMessageBox -from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.Qt import QObject -from threads import ThreadFuns -from utils import extract_pkh_from_locking_script, compose_tx_locking_script -from pivx_hashlib import pubkey_to_address, single_sha256 - - -def process_ledger_exceptions(func): - - def process_ledger_exceptions_int(*args, **kwargs): - try: - return func(*args, **kwargs) - except BTChipException as e: - printDbg('Error while communicating with Ledger hardware wallet.') - if (e.sw in (0x6d00, 0x6700)): - e.message += '\n\nMake sure the PIVX app is running on your Ledger device.' - elif (e.sw == 0x6982): - e.message += '\n\nMake sure you have entered the PIN on your Ledger device.' - raise - return process_ledger_exceptions_int - - - - -class HWdevice(QObject): - # signal: sig1 (thread) is done - emitted by signMessageFinish - sig1done = pyqtSignal(str) - # signal: sigtx (thread) is done - emitted by signTxFinish - sigTxdone = pyqtSignal(bytearray, str) - - def __init__(self, *args, **kwargs): - QObject.__init__(self, *args, **kwargs) - # Device Lock for threads - printDbg("Creating HW device class") - self.initDevice() - - - def initDevice(self): - try: - self.dongle = getDongle(False) - printOK('Ledger Nano S drivers found') - self.chip = btchip(self.dongle) - printDbg("Ledger Initialized") - self.initialized = True - ver = self.chip.getFirmwareVersion() - printOK("Ledger HW device connected [v. %s]" % str(ver.get('version'))) - - except Exception as e: - err_msg = 'error Initializing Ledger' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - self.initialized = False - if hasattr(self, 'dongle'): - self.dongle.close() - - - - # Status codes: - # 0 - not connected - # 1 - not in pivx app - # 2 - fine - @process_ledger_exceptions - def getStatusCode(self): - try: - if self.initialized: - if not self.checkApp(): - statusCode = 1 - else: - statusCode = 2 - else: - statusCode = 0 - except Exception as e: - err_msg = 'error in getStatusCode' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - statusCode = 0 - return statusCode - - - - - @process_ledger_exceptions - def getStatusMess(self, statusCode = None): - if statusCode == None or not statusCode in [0, 1, 2]: - statusCode = self.getStatusCode() - messages = { - 0: 'Unable to connect to the device', - 1: 'Open PIVX app on Ledger device', - 2: 'HW DEVICE CONNECTED!'} - return messages[statusCode] - - - - - @process_ledger_exceptions - def checkApp(self): - printDbg("Checking app") - try: - firstAddress = self.chip.getWalletPublicKey(MPATH + "0'/0/0").get('address')[12:-2] - if firstAddress[0] == 'D': - printOK("found PIVX app on ledger device") - return True - except Exception as e: - err_msg = 'error in checkApp' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - return False - - - - - @process_ledger_exceptions - def prepare_transfer_tx(self, caller, bip32_path, utxos_to_spend, dest_address, tx_fee, rawtransactions): - # For each UTXO create a Ledger 'trusted input' - self.trusted_inputs = [] - # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) - self.arg_inputs = [] - self.amount = 0 - for idx, utxo in enumerate(utxos_to_spend): - - self.amount += int(utxo['value']) - raw_tx = bytearray.fromhex(rawtransactions[utxo['tx_hash']]) - - if not raw_tx: - raise Exception("Can't find raw transaction for txid: " + rawtransactions[utxo['tx_hash']]) - - # parse the raw transaction, so that we can extract the UTXO locking script we refer to - prev_transaction = bitcoinTransaction(raw_tx) - - utxo_tx_index = utxo['tx_ouput_n'] - if utxo_tx_index < 0 or utxo_tx_index > len(prev_transaction.outputs): - raise Exception('Incorrect value of outputIndex for UTXO %s' % str(idx)) - - trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) - self.trusted_inputs.append(trusted_input) - - # Hash check - curr_pubkey = compress_public_key(self.chip.getWalletPublicKey(bip32_path)['publicKey']) - pubkey_hash = bin_hash160(curr_pubkey) - pubkey_hash_from_script = extract_pkh_from_locking_script(prev_transaction.outputs[utxo_tx_index].script) - if pubkey_hash != pubkey_hash_from_script: - text = "Error: different public key hashes for the BIP32 path and the UTXO" - text += "locking script. Your signed transaction will not be validated by the network.\n" - text += "pubkey_hash: %s\n" % str(pubkey_hash) - text += "pubkey_hash_from_script: %s\n" % str(pubkey_hash_from_script) - printDbg(text) - - self.arg_inputs.append({ - 'locking_script': prev_transaction.outputs[utxo['tx_ouput_n']].script, - 'pubkey': curr_pubkey, - 'bip32_path': bip32_path, - 'outputIndex': utxo['tx_ouput_n'], - 'txid': utxo['tx_hash'] - }) - - self.amount -= int(tx_fee) - self.amount = int(self.amount) - arg_outputs = [{'address': dest_address, 'valueSat': self.amount}] # there will be multiple outputs soon - self.new_transaction = bitcoinTransaction() # new transaction object to be used for serialization at the last stage - self.new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) - - try: - for o in arg_outputs: - output = bitcoinOutput() - output.script = compose_tx_locking_script(o['address']) - output.amount = int.to_bytes(o['valueSat'], 8, byteorder='little') - self.new_transaction.outputs.append(output) - except Exception: - raise - - # join all outputs - will be used by Ledger for signing transaction - self.all_outputs_raw = self.new_transaction.serializeOutputs() - - self.mBox2 = QMessageBox(caller) - messageText = "Check display of your hardware device


" - messageText += "From bip32_path: %s

" % str(bip32_path) - messageText += "To PIVX Address: %s

" % dest_address - messageText += "PIV amount: %s
" % str(round(self.amount / 1e8, 8)) - messageText += "plus PIV for fee: %s

" % str(round(int(tx_fee) / 1e8, 8)) - self.mBox2.setText(messageText) - self.mBox2.setIconPixmap(caller.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) - self.mBox2.setWindowTitle("CHECK YOUR LEDGER") - self.mBox2.setStandardButtons(QMessageBox.NoButton) - self.mBox2.setMaximumWidth(500) - self.mBox2.show() - - ThreadFuns.runInThread(self.signTxSign, (), self.signTxFinish) - - - - @process_ledger_exceptions - def scanForAddress(self, path): - printOK("Scanning for Address of path %s" % str(path)) - - try: - curr_addr = self.chip.getWalletPublicKey(path).get('address')[12:-2] - - except Exception as e: - err_msg = 'error in scanForAddress' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - return None - return curr_addr - - - - - @process_ledger_exceptions - def scanForBip32(self, account, address, starting_spath=0, spath_count=10, isTestnet=False): - found = False - spath = -1 - - printOK("Scanning for Bip32 path of address: %s" % address) - for i in range(starting_spath, starting_spath+spath_count): - curr_path = MPATH + "%d'/0/%d" % (account, i) - printDbg("checking path... %s" % curr_path) - try: - if not isTestnet: - curr_addr = self.chip.getWalletPublicKey(curr_path).get('address')[12:-2] - else: - pubkey = compress_public_key(self.chip.getWalletPublicKey(curr_path).get('publicKey')).hex() - curr_addr = pubkey_to_address(pubkey, isTestnet) - - if curr_addr == address: - found = True - spath = i - break - - sleep(0.01) - - except Exception as e: - err_msg = 'error in scanForBip32' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - return (found, spath) - - - - - @process_ledger_exceptions - def scanForPubKey(self, account, spath): - printOK("Scanning for Address of path_id %s on account n° %s" % (str(spath), str(account))) - curr_path = MPATH + "%d'/0/%d" % (account, spath) - try: - nodeData = self.chip.getWalletPublicKey(curr_path) - - except Exception as e: - err_msg = 'error in scanForPubKey' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - return None - - return compress_public_key(nodeData.get('publicKey')).hex() - - - - - @process_ledger_exceptions - def signTxSign(self, ctrl): - try: - starting = True - # sign all inputs on Ledger and add inputs in the self.new_transaction object for serialization - for idx, new_input in enumerate(self.arg_inputs): - self.chip.startUntrustedTransaction(starting, idx, self.trusted_inputs, new_input['locking_script']) - - self.chip.finalizeInputFull(self.all_outputs_raw) - - sig = self.chip.untrustedHashSign(new_input['bip32_path'], lockTime=0) - - new_input['signature'] = sig - inputTx = bitcoinInput() - inputTx.prevOut = bytearray.fromhex(new_input['txid'])[::-1] + int.to_bytes(new_input['outputIndex'], 4, byteorder='little') - - inputTx.script = bytearray([len(sig)]) + sig + bytearray([0x21]) + new_input['pubkey'] - - inputTx.sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) - - self.new_transaction.inputs.append(inputTx) - - starting = False - - self.new_transaction.lockTime = bytearray([0, 0, 0, 0]) - self.tx_raw = bytearray(self.new_transaction.serialize()) - - except Exception as e: - printException(getCallerName(), getFunctionName(), "Signature Exception", e.args) - self.tx_raw = None - - - - - @process_ledger_exceptions - def signTxFinish(self): - self.mBox2.accept() - try: - if self.tx_raw is not None: - # Signal to be catched by FinishSend on TabRewards - self.sigTxdone.emit(self.tx_raw, str(round(self.amount / 1e8, 8))) - else: - printOK("Transaction refused by the user") - - except Exception as e: - printDbg(e) - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import logging + +from PyQt5.QtCore import QObject, pyqtSignal + +from constants import HW_devices +from ledgerClient import LedgerApi +from misc import printOK, printDbg +from time import sleep + + +def check_api_init(func): + def func_int(*args, **kwargs): + hwDevice = args[0] + if hwDevice.api is None: + logging.warning("%s: hwDevice.api is None" % func.__name__) + raise Exception("HW device: client not initialized") + return func(*args, **kwargs) + + return func_int + + + +class HWdevice(QObject): + # signal: sig1 (thread) is done - emitted by signMessageFinish + sig1done = pyqtSignal(str) + # signal: sig_disconnected -emitted with DisconnectedException + sig_disconnected = pyqtSignal(str) + + def __init__(self, main_wnd, *args, **kwargs): + printDbg("HW: Initializing Class...") + QObject.__init__(self, *args, **kwargs) + self.main_wnd = main_wnd + self.api = None + printOK("HW: Class initialized") + + + def initDevice(self, hw_index): + printDbg("HW: initializing hw device with index %d" % hw_index) + if hw_index >= len(HW_devices): + raise Exception("Invalid HW index") + + # Select API + self.api = LedgerApi() + + # Init device & connect signals + self.api.initDevice() + self.sig1done = self.api.sig1done + self.sig_disconnected.connect(self.main_wnd.clearHWstatus) + printOK("HW: hw device with index %d initialized" % hw_index) + + + @check_api_init + def clearDevice(self, message=''): + printDbg("HW: Clearing HW device...") + self.api.closeDevice() + self.sig_disconnected.emit(message) + printOK("HW: device cleared") + + + # Status codes: + # 0 - not connected + # 1 - not initialized + # 2 - fine + @check_api_init + def getStatus(self): + printDbg("HW: checking device status...") + printOK("Status: %d" % self.api.status) + return self.api.model, self.api.status, self.api.messages[self.api.status] + + + def prepare_transfer_tx(self, caller, bip32_path, utxos_to_spend, dest_address, tx_fee, useSwiftX=False, isTestnet=False): + rewardsArray = [] + mnode = {} + mnode['path'] = bip32_path + mnode['utxos'] = utxos_to_spend + rewardsArray.append(mnode) + self.prepare_transfer_tx_bulk(caller, rewardsArray, dest_address, tx_fee, useSwiftX, isTestnet) + + + @check_api_init + def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, useSwiftX=False, isTestnet=False): + printDbg("HW: Preparing transfer TX") + self.api.prepare_transfer_tx_bulk(caller, rewardsArray, dest_address, tx_fee, useSwiftX, isTestnet) + + + @check_api_init + def scanForAddress(self, hwAcc, spath, intExt=0, isTestnet=False): + printOK("HW: Scanning for Address n. %d on account n. %d" % (spath, hwAcc)) + return self.api.scanForAddress(hwAcc, spath, intExt, isTestnet) + + + @check_api_init + def scanForBip32(self, account, address, starting_spath=0, spath_count=10, isTestnet=False): + printOK("HW: Scanning for Bip32 path of address: %s" % address) + found = False + spath = -1 + + for i in range(starting_spath, starting_spath + spath_count): + printDbg("HW: checking path... %d'/0/%d" % (account, i)) + curr_addr = self.api.scanForAddress(account, i, isTestnet) + + if curr_addr == address: + found = True + spath = i + break + + sleep(0.01) + + return (found, spath) + + + @check_api_init + def scanForPubKey(self, account, spath, isTestnet=False): + printOK("HW: Scanning for PubKey of address n. %d on account n. %d" % (spath, account)) + return self.api.scanForPubKey(account, spath, isTestnet) + + + @check_api_init + def signMess(self, caller, path, message, isTestnet=False): + printDbg("HW: Signing message...") + self.api.signMess(caller, path, message, isTestnet) + printOK("HW: Message signed") + + + @check_api_init + def signTxSign(self, ctrl): + printDbg("HW: Signing TX...") + self.api.signTxSign(ctrl) + + + @check_api_init + def signTxFinish(self): + printDbg("HW: Finishing TX signature...") + self.api.signTxFinish() + printDbg("HW: TX signed") diff --git a/src/ledgerClient.py b/src/ledgerClient.py new file mode 100644 index 0000000..5789e40 --- /dev/null +++ b/src/ledgerClient.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from bitcoin import bin_hash160 +from btchip.btchip import btchip, getDongle, BTChipException +from btchip.btchipUtils import compress_public_key, bitcoinTransaction, bitcoinInput, bitcoinOutput +import threading + +from PyQt5.QtCore import Qt, QObject, pyqtSignal +from PyQt5.QtWidgets import QMessageBox, QApplication + +from constants import MPATH_LEDGER as MPATH, MPATH_TESTNET, HW_devices +from misc import printDbg, printException, printOK, getCallerName, getFunctionName, splitString, DisconnectedException +from pivx_hashlib import pubkey_to_address, single_sha256 +from threads import ThreadFuns +from utils import extract_pkh_from_locking_script, compose_tx_locking_script + + +def process_ledger_exceptions(func): + def process_ledger_exceptions_int(*args, **kwargs): + hwDevice = args[0] + try: + return func(*args, **kwargs) + + except BTChipException as e: + printDbg('Error while communicating with Ledger hardware wallet.') + e.message = 'Error while communicating with Ledger hardware wallet.' + if (e.sw in (0x6f01, 0x6d00, 0x6700, 0x6faa)): + e.message = 'Make sure the PIVX app is open on your Ledger device.' + e.message += '
If there is a program (such as Ledger Bitcoin Wallet) interfering with the USB communication, close it first.' + elif (e.sw == 0x6982): + e.message = 'Enter the PIN on your Ledger device.' + printException(getCallerName(True), getFunctionName(True), e.message, e.args) + raise DisconnectedException(e.message, hwDevice) + + except Exception as e: + e.message = "Ledger - generic exception" + if str(e.args[0]) == 'read error': + e.message = 'Read Error. Click "Connect" to reconnect HW device' + printException(getCallerName(True), getFunctionName(True), e.message, str(e)) + raise DisconnectedException(e.message, hwDevice) + + return process_ledger_exceptions_int + + +class LedgerApi(QObject): + # signal: sig1 (thread) is done - emitted by signMessageFinish + sig1done = pyqtSignal(str) + # signal: sigtx (thread) is done - emitted by signTxFinish + sigTxdone = pyqtSignal(bytearray, str) + # signal: sigtx (thread) is done (aborted) - emitted by signTxFinish + sigTxabort = pyqtSignal() + # signal: tx_progress percent - emitted by perepare_transfer_tx_bulk + tx_progress = pyqtSignal(int) + # signal: sig_progress percent - emitted by signTxSign + sig_progress = pyqtSignal(int) + + + def __init__(self, *args, **kwargs): + QObject.__init__(self, *args, **kwargs) + self.model = [x[0] for x in HW_devices].index("LEDGER Nano S") + self.messages = [ + 'Device not initialized.', + 'Unable to connect to the device. Please check that the PIVX app on the device is open, and try again.', + 'Hardware device connected.' + ] + # Device Lock for threads + self.lock = threading.RLock() + self.status = 0 + self.dongle = None + printDbg("Creating HW device class") + + + + + @process_ledger_exceptions + def initDevice(self): + printDbg("Initializing Ledger") + with self.lock: + self.status = 0 + self.dongle = getDongle(False) + printOK('Ledger Nano S drivers found') + self.chip = btchip(self.dongle) + printDbg("Ledger Initialized") + self.status = 1 + ver = self.chip.getFirmwareVersion() + printOK("Ledger HW device connected [v. %s]" % str(ver.get('version'))) + # Check device is unlocked + bip32_path = MPATH + "%d'/0/%d" % (0, 0) + _ = self.chip.getWalletPublicKey(bip32_path) + self.status = 2 + self.sig_progress.connect(self.updateSigProgress) + + + + def closeDevice(self): + printDbg("Closing LEDGER client") + self.status = 0 + with self.lock: + if self.dongle is not None: + try: + self.dongle.close() + except: + pass + self.dongle = None + + + @process_ledger_exceptions + def append_inputs_to_TX(self, utxo, bip32_path): + self.amount += int(utxo['satoshis']) + raw_tx = bytearray.fromhex(utxo['raw_tx']) + + # parse the raw transaction, so that we can extract the UTXO locking script we refer to + prev_transaction = bitcoinTransaction(raw_tx) + + utxo_tx_index = utxo['vout'] + if utxo_tx_index < 0 or utxo_tx_index > len(prev_transaction.outputs): + raise Exception('Incorrect value of outputIndex for UTXO %s-%d' % + (utxo['raw_tx'], utxo['vout'])) + + trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) + self.trusted_inputs.append(trusted_input) + + # Hash check + curr_pubkey = compress_public_key(self.chip.getWalletPublicKey(bip32_path)['publicKey']) + pubkey_hash = bin_hash160(curr_pubkey) + pubkey_hash_from_script = extract_pkh_from_locking_script(prev_transaction.outputs[utxo_tx_index].script) + if pubkey_hash != pubkey_hash_from_script: + text = "Error: The hashes for the public key for the BIP32 path (%s), and the UTXO locking script do not match." % bip32_path + text += "Your signed transaction will not be validated by the network.\n" + text += "pubkey_hash: %s\n" % pubkey_hash.hex() + text += "pubkey_hash_from_script: %s\n" % pubkey_hash_from_script.hex() + printDbg(text) + + self.arg_inputs.append({ + 'locking_script': prev_transaction.outputs[utxo['vout']].script, + 'pubkey': curr_pubkey, + 'bip32_path': bip32_path, + 'outputIndex': utxo['vout'], + 'txid': utxo['txid'] + }) + + + + @process_ledger_exceptions + def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, useSwiftX=False, isTestnet=False): + with self.lock: + # For each UTXO create a Ledger 'trusted input' + self.trusted_inputs = [] + # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) + self.arg_inputs = [] + self.amount = 0 + num_of_sigs = sum([len(mnode['utxos']) for mnode in rewardsArray]) + curr_utxo_checked = 0 + + for mnode in rewardsArray: + # Add proper HW path (for current device) on each utxo + if isTestnet: + mnode['path'] = MPATH_TESTNET + mnode['path'] + else: + mnode['path'] = MPATH + mnode['path'] + + # Create a TX input with each utxo + for utxo in mnode['utxos']: + self.append_inputs_to_TX(utxo, mnode['path']) + # completion percent emitted + curr_utxo_checked += 1 + completion = int(95 * curr_utxo_checked / num_of_sigs) + self.tx_progress.emit(completion) + + self.amount -= int(tx_fee) + self.amount = int(self.amount) + arg_outputs = [{'address': dest_address, 'valueSat': self.amount}] # there will be multiple outputs soon + self.new_transaction = bitcoinTransaction() # new transaction object to be used for serialization at the last stage + self.new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) + + self.tx_progress.emit(99) + + for o in arg_outputs: + output = bitcoinOutput() + output.script = compose_tx_locking_script(o['address']) + output.amount = int.to_bytes(o['valueSat'], 8, byteorder='little') + self.new_transaction.outputs.append(output) + + self.tx_progress.emit(100) + + # join all outputs - will be used by Ledger for signing transaction + self.all_outputs_raw = self.new_transaction.serializeOutputs() + + self.mBox2 = QMessageBox(caller) + self.messageText = "

Confirm transaction on your device, with the following details:

" + # messageText += "From bip32_path: %s

" % str(bip32_path) + self.messageText += "

Payment to:
%s

" % dest_address + self.messageText += "

Net amount:
%s PIV

" % str(round(self.amount / 1e8, 8)) + if useSwiftX: + self.messageText += "

Fees (SwiftX flat rate):
%s PIV

" % str(round(int(tx_fee) / 1e8, 8)) + else: + self.messageText += "

Fees:
%s PIV

" % str(round(int(tx_fee) / 1e8, 8)) + messageText = self.messageText + "Signature Progress: 0 %" + self.mBox2.setText(messageText) + self.mBox2.setIconPixmap(caller.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) + self.mBox2.setWindowTitle("CHECK YOUR LEDGER") + self.mBox2.setStandardButtons(QMessageBox.NoButton) + self.mBox2.setMaximumWidth(500) + self.mBox2.show() + + ThreadFuns.runInThread(self.signTxSign, (), self.signTxFinish) + + + + @process_ledger_exceptions + def scanForAddress(self, hwAcc, spath, intExt=0, isTestnet=False): + with self.lock: + if not isTestnet: + curr_path = MPATH + "%d'/%d/%d" % (hwAcc, intExt, spath) + curr_addr = self.chip.getWalletPublicKey(curr_path).get('address')[12:-2] + else: + curr_path = MPATH_TESTNET + "%d'/%d/%d" % (hwAcc, intExt, spath) + pubkey = compress_public_key(self.chip.getWalletPublicKey(curr_path).get('publicKey')).hex() + curr_addr = pubkey_to_address(pubkey, isTestnet) + + return curr_addr + + + + @process_ledger_exceptions + def scanForPubKey(self, account, spath, isTestnet=False): + hwpath = "%d'/0/%d" % (account, spath) + if isTestnet: + curr_path = MPATH_TESTNET + hwpath + else: + curr_path = MPATH + hwpath + + with self.lock: + nodeData = self.chip.getWalletPublicKey(curr_path) + + return compress_public_key(nodeData.get('publicKey')).hex() + + + + @process_ledger_exceptions + def signMess(self, caller, hwpath, message, isTestnet=False): + if isTestnet: + path = MPATH_TESTNET + hwpath + else: + path = MPATH + hwpath + # Ledger doesn't accept characters other that ascii printable: + # https://ledgerhq.github.io/btchip-doc/bitcoin-technical.html#_sign_message + message = message.encode('ascii', 'ignore') + message_sha = splitString(single_sha256(message).hex(), 32); + + # Connection pop-up + mBox = QMessageBox(caller.ui) + warningText = "Another application (such as Ledger Wallet app) has probably taken over " + warningText += "the communication with the Ledger device.

To continue, close that application and " + warningText += "click the Retry button.\nTo cancel, click the Abort button" + mBox.setText(warningText) + mBox.setWindowTitle("WARNING") + mBox.setStandardButtons(QMessageBox.Retry | QMessageBox.Abort); + + # Ask confirmation + with self.lock: + info = self.chip.signMessagePrepare(path, message) + + while info['confirmationNeeded'] and info['confirmationType'] == 34: + ans = mBox.exec_() + + if ans == QMessageBox.Abort: + raise Exception("Reconnect HW device") + + # we need to reconnect the device + self.initDevice() + info = self.chip.signMessagePrepare(path, message) + + printOK('Signing Message') + self.mBox = QMessageBox(caller.ui) + messageText = "Check display of your hardware device\n\n" + "- masternode message hash:\n\n%s\n\n-path:\t%s\n" % ( + message_sha, path) + self.mBox.setText(messageText) + self.mBox.setIconPixmap(caller.ui.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) + self.mBox.setWindowTitle("CHECK YOUR LEDGER") + self.mBox.setStandardButtons(QMessageBox.NoButton) + self.mBox.show() + + # Sign message + ThreadFuns.runInThread(self.signMessageSign, (), self.signMessageFinish) + + + + @process_ledger_exceptions + def signMessageSign(self, ctrl): + self.signature = None + with self.lock: + try: + self.signature = self.chip.signMessageSign() + except: + pass + + + + def signMessageFinish(self): + with self.lock: + self.mBox.accept() + if self.signature != None: + if len(self.signature) > 4: + rLength = self.signature[3] + r = self.signature[4: 4 + rLength] + if len(self.signature) > 4 + rLength + 1: + sLength = self.signature[4 + rLength + 1] + if len(self.signature) > 4 + rLength + 2: + s = self.signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + + work = bytes(chr(27 + 4 + (self.signature[0] & 0x01)), "utf-8") + r + s + printOK("Message signed") + sig1 = work.hex() + else: + printDbg('client.signMessageSign() returned invalid response (code 3): ' + self.signature.hex()) + sig1 = "None" + else: + printDbg('client.signMessageSign() returned invalid response (code 2): ' + self.signature.hex()) + sig1 = "None" + else: + printDbg('client.signMessageSign() returned invalid response (code 1): ' + self.signature.hex()) + sig1 = "None" + else: + printOK("Signature refused by the user") + sig1 = "None" + + self.sig1done.emit(sig1) + + + + @process_ledger_exceptions + def signTxSign(self, ctrl): + self.tx_raw = None + with self.lock: + starting = True + curr_input_signed = 0 + # sign all inputs on Ledger and add inputs in the self.new_transaction object for serialization + for idx, new_input in enumerate(self.arg_inputs): + try: + self.chip.startUntrustedTransaction(starting, idx, self.trusted_inputs, new_input['locking_script']) + + self.chip.finalizeInputFull(self.all_outputs_raw) + + sig = self.chip.untrustedHashSign(new_input['bip32_path'], lockTime=0) + except BTChipException as e: + if e.args[0] != "Invalid status 6985": + raise e + # Signature refused by the user + return + + new_input['signature'] = sig + inputTx = bitcoinInput() + inputTx.prevOut = bytearray.fromhex(new_input['txid'])[::-1] + int.to_bytes(new_input['outputIndex'], 4, + byteorder='little') + + inputTx.script = bytearray([len(sig)]) + sig + bytearray([0x21]) + new_input['pubkey'] + + inputTx.sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) + + self.new_transaction.inputs.append(inputTx) + + starting = False + + # signature percent emitted + curr_input_signed += 1 + completion = int(100 * curr_input_signed / len(self.arg_inputs)) + self.sig_progress.emit(completion) + + self.new_transaction.lockTime = bytearray([0, 0, 0, 0]) + self.tx_raw = bytearray(self.new_transaction.serialize()) + self.sig_progress.emit(100) + + + + def signTxFinish(self): + self.mBox2.accept() + + if self.tx_raw is not None: + # Signal to be catched by FinishSend on TabRewards / dlg_sewwpAll + self.sigTxdone.emit(self.tx_raw, str(round(self.amount / 1e8, 8))) + else: + printOK("Transaction refused by the user") + self.sigTxabort.emit() + + + + def updateSigProgress(self, percent): + messageText = self.messageText + "Signature Progress: " + str(percent) + " %" + self.mBox2.setText(messageText) + QApplication.processEvents() diff --git a/src/mainApp.py b/src/mainApp.py index 663af05..a2510ff 100644 --- a/src/mainApp.py +++ b/src/mainApp.py @@ -1,90 +1,132 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -import signal -from misc import getVersion, printDbg -from constants import starting_height, starting_width, user_dir -from PyQt5.Qt import QMainWindow, QIcon, QAction -from mainWindow import MainWindow -from qt.dlg_configureRPCserver import ConfigureRPCserver_dlg - -class ServiceExit(Exception): - """ - Custom exception which is used to trigger the clean exit - of all running threads and the main program. - """ - pass - - -def service_shutdown(signum, frame): - print('Caught signal %d' % signum) - raise ServiceExit - - - -class App(QMainWindow): - - def __init__(self, imgDir): - super().__init__() - # Register the signal handlers - signal.signal(signal.SIGTERM, service_shutdown) - signal.signal(signal.SIGINT, service_shutdown) - # Get version and title - self.version = getVersion() - self.title = 'PET4L - PIVX Emergency Tool For Ledger - v.%s-%s' % (self.version['number'], self.version['tag']) - # Create the userdir if it doesn't exist - if not os.path.exists(user_dir): - os.makedirs(user_dir) - # Initialize user interface - self.initUI(imgDir) - - - def initUI(self, imgDir): - # Set title and geometry - self.setWindowTitle(self.title) - self.resize(starting_width, starting_height) - # Set Icon - spmtIcon_file = os.path.join(imgDir, 'spmtLogo_shield.png') - self.spmtIcon = QIcon(spmtIcon_file) - self.setWindowIcon(self.spmtIcon) - # Add RPC server menu - mainMenu = self.menuBar() - confMenu = mainMenu.addMenu('Setup') - self.rpcConfMenu = QAction(self.spmtIcon, 'Local RPC Server...', self) - self.rpcConfMenu.triggered.connect(self.onEditRPCServer) - confMenu.addAction(self.rpcConfMenu) - # Create main window - self.mainWindow = MainWindow(self, imgDir) - self.setCentralWidget(self.mainWindow) - # Show - self.show() - self.activateWindow() - - - - - def closeEvent(self, *args, **kwargs): - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - # Terminate the running threads. - # Set the shutdown flag on each thread to trigger a clean shutdown of each thread. - self.mainWindow.myRpcWd.shutdown_flag.set() - print("Saving stuff & closing...") - if getattr(self.mainWindow.hwdevice, 'dongle', None) is not None: - self.mainWindow.hwdevice.dongle.close() - print("Dongle closed") - print("Bye Bye.") - return QMainWindow.closeEvent(self, *args, **kwargs) - - - - def onEditRPCServer(self): - # Create Dialog - try: - ui = ConfigureRPCserver_dlg(self) - if ui.exec(): - printDbg("Configuring RPC Server...") - except Exception as e: - print(e) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import logging +import os +import signal +import sys + +from PyQt5.QtCore import pyqtSignal, QSettings +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog + +from database import Database +from misc import printDbg, initLogs, saveCacheSettings, readCacheSettings, getVersion +from mainWindow import MainWindow +from constants import user_dir +from qt.dlg_configureRPCservers import ConfigureRPCservers_dlg + +class ServiceExit(Exception): + """ + Custom exception which is used to trigger the clean exit + of all running threads and the main program. + """ + pass + + +def service_shutdown(signum, frame): + print('Caught signal %d' % signum) + raise ServiceExit + + + +class App(QMainWindow): + # Signal emitted from database + sig_changed_rpcServers = pyqtSignal() + + def __init__(self, imgDir, start_args): + # Create the userdir if it doesn't exist + if not os.path.exists(user_dir): + os.makedirs(user_dir) + + # Initialize Logs + initLogs() + super().__init__() + + # Register the signal handlers + signal.signal(signal.SIGTERM, service_shutdown) + signal.signal(signal.SIGINT, service_shutdown) + + # Get version and title + self.version = getVersion() + self.title = 'PET4L - PIVX Emergency Tool For Ledger - v.%s-%s' % (self.version['number'], self.version['tag']) + + # Open database + self.db = Database(self) + self.db.open() + + # Check for startup args (clear data) + if start_args.clearAppData: + settings = QSettings('PIVX', 'PET4L') + settings.clear() + + # Clear DB + self.db.clearTable('UTXOS') + + # Read cached app data + self.cache = readCacheSettings() + + # Initialize user interface + self.initUI(imgDir) + + + def initUI(self, imgDir): + # Set title and geometry + self.setWindowTitle(self.title) + self.resize(self.cache.get("window_width"), self.cache.get("window_height")) + # Set Icons + self.spmtIcon = QIcon(os.path.join(imgDir, 'spmtLogo_shield.png')) + self.pivx_icon = QIcon(os.path.join(imgDir, 'icon_pivx.png')) + self.script_icon = QIcon(os.path.join(imgDir, 'icon_script.png')) + self.setWindowIcon(self.spmtIcon) + # Add RPC server menu + mainMenu = self.menuBar() + confMenu = mainMenu.addMenu('Setup') + self.rpcConfMenu = QAction(self.pivx_icon, 'RPC Servers config...', self) + self.rpcConfMenu.triggered.connect(self.onEditRPCServer) + confMenu.addAction(self.rpcConfMenu) + # Create main window + self.mainWindow = MainWindow(self, imgDir) + self.setCentralWidget(self.mainWindow) + # Show + self.show() + self.activateWindow() + + + + def closeEvent(self, *args, **kwargs): + # Restore output stream + sys.stdout = sys.__stdout__ + # Terminate the running threads. + # Set the shutdown flag on each thread to trigger a clean shutdown of each thread. + self.mainWindow.myRpcWd.shutdown_flag.set() + logging.debug("Saving stuff & closing...") + try: + self.mainWindow.hwdevice.clearDevice() + except Exception as e: + logging.warning(str(e)) + + # Update window/splitter size + self.cache['window_width'] = self.width() + self.cache['window_height'] = self.height() + self.cache['splitter_x'] = self.mainWindow.splitter.sizes()[0] + self.cache['splitter_y'] = self.mainWindow.splitter.sizes()[1] + self.cache['console_hidden'] = (self.mainWindow.btn_consoleToggle.text() == 'Show') + + # persist cache + saveCacheSettings(self.cache) + + # clear / close DB + self.db.removeTable('UTXOS') + self.db.close() + + # Adios + print("Bye Bye.") + return QMainWindow.closeEvent(self, *args, **kwargs) + + + + def onEditRPCServer(self): + # Create Dialog + ui = ConfigureRPCservers_dlg(self) + if ui.exec(): + printDbg("Configuring RPC Servers...") diff --git a/src/mainWindow.py b/src/mainWindow.py index 2788524..ac2ee16 100644 --- a/src/mainWindow.py +++ b/src/mainWindow.py @@ -1,351 +1,501 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -from time import strftime, gmtime -from misc import printDbg, printException, printOK, getCallerName, getFunctionName, WriteStream, WriteStreamReceiver, now -from constants import starting_height, log_File - -from PyQt5.QtCore import pyqtSlot, Qt, QThread -from PyQt5.Qt import QTabWidget, QLabel, QIcon, QSplitter -from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGroupBox, QVBoxLayout, QFileDialog -from PyQt5.QtWidgets import QMessageBox, QTextEdit -from PyQt5.QtGui import QPixmap, QColor, QPalette, QTextCursor - -from queue import Queue - -from rpcClient import RpcClient -from hwdevice import HWdevice -from qt.guiHeader import GuiHeader -from tabRewards import TabRewards -from threads import ThreadFuns -from watchdogThreads import RpcWatchdog - - - -class MainWindow(QWidget): - - def __init__(self, parent, imgDir): - super(QWidget, self).__init__(parent) - self.parent = parent - self.imgDir = imgDir - self.runInThread = ThreadFuns.runInThread - ###-- Create clients and statuses - self.hwdevice = None - self.hwStatus = 0 - self.hwStatusMess = "Not Connected" - self.rpcClient = None - self.rpcConnected = False - self.rpcStatusMess = "Not Connected" - ###-- Load icons & images - self.loadIcons() - ###-- Create main layout - self.layout = QVBoxLayout() - self.header = GuiHeader(self) - self.initConsole() - self.layout.addWidget(self.header) - ###-- Create RPC Whatchdog - self.rpc_watchdogThread = QThread() - self.myRpcWd = RpcWatchdog(self) - self.myRpcWd.moveToThread(self.rpc_watchdogThread) - self.rpc_watchdogThread.started.connect(self.myRpcWd.run) - self.rpc_watchdogThread.start() - - ###-- Create Queues and redirect stdout and stderr (eventually) - self.queue = Queue() - self.queue2 = Queue() - sys.stdout = WriteStream(self.queue) - sys.stderr = WriteStream(self.queue2) - - ###-- Init last logs - logFile = open(log_File, 'w+') - timestamp = strftime('%Y-%m-%d %H:%M:%S', gmtime(now())) - log_line = '{}
'.format('STARTING PET4L at '+ timestamp) - logFile.write(log_line) - logFile.close() - - ###-- Create the thread to update console log for stdout - self.consoleLogThread = QThread() - self.myWSReceiver = WriteStreamReceiver(self.queue) - self.myWSReceiver.mysignal.connect(self.append_to_console) - self.myWSReceiver.moveToThread(self.consoleLogThread) - self.consoleLogThread.started.connect(self.myWSReceiver.run) - self.consoleLogThread.start() - printDbg("Console Log thread started") - ###-- Create the thread to update console log for stderr - self.consoleLogThread2 = QThread() - self.myWSReceiver2 = WriteStreamReceiver(self.queue2) - self.myWSReceiver2.mysignal.connect(self.append_to_console) - self.myWSReceiver2.moveToThread(self.consoleLogThread2) - self.consoleLogThread2.started.connect(self.myWSReceiver2.run) - self.consoleLogThread2.start() - printDbg("Console Log thread 2 started") - - ###-- Initialize tabs - self.tabs = QTabWidget() - self.t_rewards = TabRewards(self) - ###-- Add tabs - self.tabs.addTab(self.tabRewards, "Spend from Ledger") - ###-- Draw Tabs - self.splitter = QSplitter(Qt.Vertical) - ###-- Add tabs and console to Layout - self.splitter.addWidget(self.tabs) - self.splitter.addWidget(self.console) - self.splitter.setStretchFactor(0,0) - self.splitter.setStretchFactor(1,1) - self.splitter.setSizes([2,1]) - self.layout.addWidget(self.splitter) - ###-- Set Layout - self.setLayout(self.layout) - ###-- Let's go - self.mnode_to_change = None - printOK("Hello! Welcome to " + parent.title) - - - - - @pyqtSlot(str) - def append_to_console(self, text): - self.consoleArea.moveCursor(QTextCursor.End) - self.consoleArea.insertHtml(text) - # update last logs - logFile = open(log_File, 'a+') - logFile.write(text) - logFile.close() - - - - - def initConsole(self): - self.console = QGroupBox() - self.console.setTitle("Console Log") - layout = QVBoxLayout() - self.btn_consoleToggle = QPushButton('Hide') - self.btn_consoleToggle.setToolTip('Show/Hide console') - self.btn_consoleToggle.clicked.connect(lambda: self.onToggleConsole()) - consoleHeader = QHBoxLayout() - consoleHeader.addWidget(self.btn_consoleToggle) - self.consoleSaveButton = QPushButton('Save') - self.consoleSaveButton.clicked.connect(lambda: self.onSaveConsole()) - consoleHeader.addWidget(self.consoleSaveButton) - self.btn_consoleClean = QPushButton('Clean') - self.btn_consoleClean.setToolTip('Clean console log area') - self.btn_consoleClean.clicked.connect(lambda: self.onCleanConsole()) - consoleHeader.addWidget(self.btn_consoleClean) - consoleHeader.addStretch(1) - layout.addLayout(consoleHeader) - self.consoleArea = QTextEdit() - almostBlack = QColor(40, 40, 40) - palette = QPalette() - palette.setColor(QPalette.Base, almostBlack) - green = QColor(0, 255, 0) - palette.setColor(QPalette.Text, green) - self.consoleArea.setPalette(palette) - layout.addWidget(self.consoleArea) - self.console.setLayout(layout) - - - - - def loadIcons(self): - # Load Icons - self.ledPurpleH_icon = QPixmap(os.path.join(self.imgDir, 'icon_purpleLedH.png')).scaledToHeight(17, Qt.SmoothTransformation) - self.ledGrayH_icon = QPixmap(os.path.join(self.imgDir, 'icon_grayLedH.png')).scaledToHeight(17, Qt.SmoothTransformation) - self.ledHalfPurpleH_icon = QPixmap(os.path.join(self.imgDir, 'icon_halfPurpleLedH.png')).scaledToHeight(17, Qt.SmoothTransformation) - self.ledRedV_icon = QPixmap(os.path.join(self.imgDir, 'icon_redLedV.png')).scaledToHeight(17, Qt.SmoothTransformation) - self.ledGrayV_icon = QPixmap(os.path.join(self.imgDir, 'icon_grayLedV.png')).scaledToHeight(17, Qt.SmoothTransformation) - self.ledGreenV_icon = QPixmap(os.path.join(self.imgDir, 'icon_greenLedV.png')).scaledToHeight(17, Qt.SmoothTransformation) - self.ledgerImg = QPixmap(os.path.join(self.imgDir, 'ledger.png')) - - - - def myPopUp(self, messType, messTitle, messText, defaultButton=QMessageBox.No): - mess = QMessageBox(messType, messTitle, messText, defaultButton, parent=self) - mess.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - mess.setDefaultButton(defaultButton) - return mess.exec_() - - - - def myPopUp2(self, messType, messTitle, messText, singleButton=QMessageBox.Ok): - mess = QMessageBox(messType, messTitle, messText, singleButton, parent=self) - mess.setStandardButtons(singleButton | singleButton) - return mess.exec_() - - - - - @pyqtSlot() - def onCheckHw(self): - printDbg("Checking for HW device...") - self.runInThread(self.updateHWstatus, (), self.showHWstatus) - - - - - @pyqtSlot() - def onCheckRpc(self): - printDbg("Checking RPC server...") - self.runInThread(self.updateRPCstatus, (), self.showRPCstatus) - - - - - @pyqtSlot() - def onCleanConsole(self): - self.consoleArea.clear() - - - - - @pyqtSlot() - def onSaveConsole(self): - timestamp = strftime('%Y-%m-%d_%H-%M-%S', gmtime(now())) - options = QFileDialog.Options() - options |= QFileDialog.DontUseNativeDialog - fileName, _ = QFileDialog.getSaveFileName(self,"Save Logs to file","PET4L_Logs_%s.txt" % timestamp,"All Files (*);; Text Files (*.txt)", options=options) - try: - if fileName: - printOK("Saving logs to %s" % fileName) - log_file = open(fileName, 'w+') - log_text = self.consoleArea.toPlainText() - log_file.write(log_text) - log_file.close() - - except Exception as e: - err_msg = "error writing Log file" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - - @pyqtSlot() - def onToggleConsole(self): - if self.btn_consoleToggle.text() == 'Hide': - self.btn_consoleToggle.setText('Show') - self.consoleArea.hide() - self.previousH = self.splitter.sizes()[1] - self.console.setMaximumHeight(70) - else: - self.console.setMinimumHeight(self.previousH) - self.console.setMaximumHeight(starting_height) - self.btn_consoleToggle.setText('Hide') - self.consoleArea.show() - - - - - - - def showHWstatus(self): - self.updateHWleds() - self.myPopUp2(QMessageBox.Information, 'PET4L - hw check', "STATUS: %s" % self.hwStatusMess, QMessageBox.Ok) - - - - - def showRPCstatus(self): - self.updateRPCled() - self.myPopUp2(QMessageBox.Information, 'PET4L - rpc check', "STATUS: %s" % self.rpcStatusMess, QMessageBox.Ok) - - - - - def updateHWleds(self): - if self.hwStatus == 1: - self.header.hwLed.setPixmap(self.ledHalfPurpleH_icon) - elif self.hwStatus == 2: - self.header.hwLed.setPixmap(self.ledPurpleH_icon) - else: - self.header.hwLed.setPixmap(self.ledGrayH_icon) - self.header.hwLed.setToolTip(self.hwStatusMess) - - - - - def updateHWstatus(self, ctrl): - if self.hwdevice is None: - self.hwdevice = HWdevice() - - device = self.hwdevice - statusCode = device.getStatusCode() - statusMess = device.getStatusMess(statusCode) - printDbg("code: %s - mess: %s" % (statusCode, statusMess)) - if statusCode != 2: - try: - if getattr(self.hwdevice, 'dongle', None) is not None: - self.hwdevice.dongle.close() - self.hwdevice.initDevice() - device = self.hwdevice - statusCode = device.getStatusCode() - statusMess = device.getStatusMess(statusCode) - - except Exception as e: - err_msg = "error in checkHw" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - self.hwStatus = statusCode - self.hwStatusMess = statusMess - - - - - def updateLastBlockLabel(self): - text = '--' - if self.rpcLastBlock == 1: - text = "Loading block index..." - elif self.rpcLastBlock > 0 and self.rpcConnected: - text = str(self.rpcLastBlock) - - self.header.lastBlockLabel.setText(text) - - - - - def updateRPCled(self): - if self.rpcConnected: - self.header.rpcLed.setPixmap(self.ledPurpleH_icon) - else: - if self.rpcLastBlock == 1: - self.header.rpcLed.setPixmap(self.ledHalfPurpleH_icon) - else: - self.header.rpcLed.setPixmap(self.ledGrayH_icon) - - self.header.rpcLed.setToolTip(self.rpcStatusMess) - self.updateLastBlockLabel() - - - - - def updateRPCstatus(self, ctrl): - if self.rpcClient is None: - try: - self.rpcClient = RpcClient() - except Exception as e: - print(e) - status, lastBlock = self.rpcClient.getStatus() - statusMess = self.rpcClient.getStatusMess(status) - if not status and lastBlock==0: - try: - self.rpcClient = RpcClient() - status, lastBlock = self.rpcClient.getStatus() - statusMess = self.rpcClient.getStatusMess(status) - except Exception as e: - err_msg = "error in checkRpc" - printException(getCallerName(), getFunctionName(), err_msg, e) - - elif lastBlock == 1: - statusMess = "PIVX wallet is connected but still synchronizing / verifying blocks" - - self.rpcConnected = status - self.rpcLastBlock = lastBlock - self.rpcStatusMess = statusMess - - - - - - - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import logging +import os +from queue import Queue +import sys +from time import strftime, gmtime +import threading + +from PyQt5.QtCore import pyqtSignal, Qt, QThread +from PyQt5.QtGui import QPixmap, QColor, QPalette, QTextCursor, QFont, QIcon +from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGroupBox, QVBoxLayout, \ + QFileDialog, QTextEdit, QTabWidget, QLabel, QSplitter + +from apiClient import ApiClient +from constants import starting_height, DefaultCache +from hwdevice import HWdevice +from misc import printDbg, printException, printOK, getCallerName, getFunctionName, \ + WriteStream, WriteStreamReceiver, now, \ + persistCacheSetting, myPopUp_sb + +from tabRewards import TabRewards +from qt.guiHeader import GuiHeader +from rpcClient import RpcClient +from threads import ThreadFuns +from watchdogThreads import RpcWatchdog + + +class MainWindow(QWidget): + + # signal: clear RPC status label and icons (emitted by updateRPCstatus) + sig_clearRPCstatus = pyqtSignal() + + # signal: RPC status (for server id) is changed (emitted by updateRPCstatus) + sig_RPCstatusUpdated = pyqtSignal(int, bool) + + # signal: RPC list has been reloaded (emitted by updateRPClist) + sig_RPClistReloaded = pyqtSignal() + + # signal: UTXO list loading percent (emitted by load_utxos_thread in tabRewards) + sig_UTXOsLoading = pyqtSignal(int) + + # signal: UTXO list has been reloaded (emitted by load_utxos_thread in tabRewards) + sig_UTXOsLoaded = pyqtSignal() + + def __init__(self, parent, imgDir): + super(QWidget, self).__init__(parent) + self.parent = parent + self.imgDir = imgDir + self.runInThread = ThreadFuns.runInThread + self.lock = threading.Lock() + + ###-- Create clients and statuses + self.hwStatus = 0 + self.hwModel = 0 + self.hwStatusMess = "Not Connected" + self.rpcClient = None + self.rpcConnected = False + self.updatingRPCbox = False + self.rpcStatusMess = "Not Connected" + self.isBlockchainSynced = False + # Changes when an RPC client is connected (affecting API client) + self.isTestnetRPC = self.parent.cache['isTestnetRPC'] + + ###-- Load icons & images + self.loadIcons() + ###-- Create main layout + self.layout = QVBoxLayout() + self.header = GuiHeader(self) + self.initConsole() + self.layout.addWidget(self.header) + + ##-- Load RPC Servers list (init selection and self.isTestnet) + self.updateRPClist() + + ##-- Init HW selection + self.header.hwDevices.setCurrentIndex(self.parent.cache['selectedHW_index']) + + ##-- init HW Client + self.hwdevice = HWdevice(self) + + ##-- init Api Client + self.apiClient = ApiClient(self.isTestnetRPC) + + ###-- Create Queues and redirect stdout + self.queue = Queue() + sys.stdout = WriteStream(self.queue) + + ###-- Init last logs + logging.debug("STARTING PET4L") + + ###-- Create the thread to update console log for stdout + self.consoleLogThread = QThread() + self.myWSReceiver = WriteStreamReceiver(self.queue) + self.myWSReceiver.mysignal.connect(self.append_to_console) + self.myWSReceiver.moveToThread(self.consoleLogThread) + self.consoleLogThread.started.connect(self.myWSReceiver.run) + self.consoleLogThread.start() + printDbg("Console Log thread started") + + ###-- Initialize tabs + self.tabs = QTabWidget() + self.t_rewards = TabRewards(self) + ###-- Add tabs + self.tabs.addTab(self.tabRewards, "Spend from Ledger") + ###-- Draw Tabs + self.splitter = QSplitter(Qt.Vertical) + ###-- Add tabs and console to Layout + self.splitter.addWidget(self.tabs) + self.splitter.addWidget(self.console) + self.splitter.setStretchFactor(0,0) + self.splitter.setStretchFactor(1,1) + self.splitter.setSizes([2,1]) + self.layout.addWidget(self.splitter) + + ###-- Set Layout + self.setLayout(self.layout) + + ###-- Init Settings + self.initSettings() + + ###-- Connect buttons/signals + self.connButtons() + + ###-- Create RPC Whatchdog + self.rpc_watchdogThread = QThread() + self.myRpcWd = RpcWatchdog(self) + self.myRpcWd.moveToThread(self.rpc_watchdogThread) + self.rpc_watchdogThread.started.connect(self.myRpcWd.run) + + ###-- Let's go + self.mnode_to_change = None + printOK("Hello! Welcome to " + parent.title) + + + + def append_to_console(self, text): + self.consoleArea.moveCursor(QTextCursor.End) + self.consoleArea.insertHtml(text) + + + + def clearHWstatus(self, message=''): + self.hwStatus = 0 + self.hwStatusMess = "Not Connected" + self.header.hwLed.setPixmap(self.ledGrayH_icon) + if message != '': + self.hwStatus = 1 + myPopUp_sb(self, "crit", "hw device Disconnected", message) + + + + def clearRPCstatus(self): + with self.lock: + self.rpcConnected = False + self.header.lastPingBox.setHidden(False) + self.header.rpcLed.setPixmap(self.ledGrayH_icon) + self.header.lastBlockLabel.setText("Connecting...") + self.header.lastPingIcon.setPixmap(self.connRed_icon) + self.header.responseTimeLabel.setText("--") + self.header.responseTimeLabel.setStyleSheet("color: red") + self.header.lastPingIcon.setStyleSheet("color: red") + + + + def connButtons(self): + self.header.button_checkRpc.clicked.connect(lambda: self.onCheckRpc()) + self.header.button_checkHw.clicked.connect(lambda: self.onCheckHw()) + self.header.rpcClientsBox.currentIndexChanged.connect(self.onChangeSelectedRPC) + self.header.hwDevices.currentIndexChanged.connect(self.onChangeSelectedHW) + ##-- Connect signals + self.sig_clearRPCstatus.connect(self.clearRPCstatus) + self.sig_RPCstatusUpdated.connect(self.showRPCstatus) + self.parent.sig_changed_rpcServers.connect(self.updateRPClist) + + + + def getRPCserver(self): + itemData = self.header.rpcClientsBox.itemData(self.header.rpcClientsBox.currentIndex()) + rpc_index = self.header.rpcClientsBox.currentIndex() + rpc_protocol = itemData["protocol"] + rpc_host = itemData["host"] + rpc_user = itemData["user"] + rpc_password = itemData["password"] + + return rpc_index, rpc_protocol, rpc_host, rpc_user, rpc_password + + + + def getServerListIndex(self, server): + return self.header.rpcClientsBox.findData(server) + + + + + def initConsole(self): + self.console = QGroupBox() + self.console.setTitle("Console Log") + layout = QVBoxLayout() + self.btn_consoleToggle = QPushButton('Hide') + self.btn_consoleToggle.setToolTip('Show/Hide console') + self.btn_consoleToggle.clicked.connect(lambda: self.onToggleConsole()) + consoleHeader = QHBoxLayout() + consoleHeader.addWidget(self.btn_consoleToggle) + self.consoleSaveButton = QPushButton('Save') + self.consoleSaveButton.clicked.connect(lambda: self.onSaveConsole()) + consoleHeader.addWidget(self.consoleSaveButton) + self.btn_consoleClean = QPushButton('Clean') + self.btn_consoleClean.setToolTip('Clean console log area') + self.btn_consoleClean.clicked.connect(lambda: self.onCleanConsole()) + consoleHeader.addWidget(self.btn_consoleClean) + consoleHeader.addStretch(1) + self.versionLabel = QLabel("--") + self.versionLabel.setOpenExternalLinks(True) + consoleHeader.addWidget(self.versionLabel) + self.btn_checkVersion = QPushButton("Check SPMT version") + self.btn_checkVersion.setToolTip("Check latest stable release of SPMT") + self.btn_checkVersion.clicked.connect(lambda: self.onCheckVersion()) + consoleHeader.addWidget(self.btn_checkVersion) + layout.addLayout(consoleHeader) + self.consoleArea = QTextEdit() + almostBlack = QColor(40, 40, 40) + palette = QPalette() + palette.setColor(QPalette.Base, almostBlack) + green = QColor(0, 255, 0) + palette.setColor(QPalette.Text, green) + self.consoleArea.setPalette(palette) + layout.addWidget(self.consoleArea) + self.console.setLayout(layout) + + + + def initSettings(self): + self.splitter.setSizes([self.parent.cache.get("splitter_x"), self.parent.cache.get("splitter_y")]) + ###-- Hide console if it was previously hidden + if self.parent.cache.get("console_hidden"): + self.onToggleConsole() + + + + def loadIcons(self): + # Load Icons + self.ledPurpleH_icon = QPixmap(os.path.join(self.imgDir, 'icon_purpleLedH.png')).scaledToHeight(17, Qt.SmoothTransformation) + self.ledGrayH_icon = QPixmap(os.path.join(self.imgDir, 'icon_grayLedH.png')).scaledToHeight(17, Qt.SmoothTransformation) + self.ledHalfPurpleH_icon = QPixmap(os.path.join(self.imgDir, 'icon_halfPurpleLedH.png')).scaledToHeight(17, Qt.SmoothTransformation) + self.ledRedV_icon = QPixmap(os.path.join(self.imgDir, 'icon_redLedV.png')).scaledToHeight(17, Qt.SmoothTransformation) + self.ledGrayV_icon = QPixmap(os.path.join(self.imgDir, 'icon_grayLedV.png')).scaledToHeight(17, Qt.SmoothTransformation) + self.ledGreenV_icon = QPixmap(os.path.join(self.imgDir, 'icon_greenLedV.png')).scaledToHeight(17, Qt.SmoothTransformation) + self.lastBlock_icon = QPixmap(os.path.join(self.imgDir, 'icon_lastBlock.png')).scaledToHeight(15, Qt.SmoothTransformation) + self.connGreen_icon = QPixmap(os.path.join(self.imgDir, 'icon_greenConn.png')).scaledToHeight(15, Qt.SmoothTransformation) + self.connRed_icon = QPixmap(os.path.join(self.imgDir, 'icon_redConn.png')).scaledToHeight(15, Qt.SmoothTransformation) + self.connOrange_icon = QPixmap(os.path.join(self.imgDir, 'icon_orangeConn.png')).scaledToHeight(15, Qt.SmoothTransformation) + self.removeMN_icon = QIcon(os.path.join(self.imgDir, 'icon_delete.png')) + self.editMN_icon = QIcon(os.path.join(self.imgDir, 'icon_edit.png')) + self.ledgerImg = QPixmap(os.path.join(self.imgDir, 'ledger.png')) + + + def onCheckHw(self): + printDbg("Checking for HW device...") + self.updateHWstatus(None) + self.showHWstatus() + + + + + def onCheckRpc(self): + self.runInThread(self.updateRPCstatus, (True,),) + + + + def onChangeSelectedHW(self, i): + # Clear status + self.clearHWstatus() + + # Persist setting + self.parent.cache['selectedHW_index'] = persistCacheSetting('cache_HWindex',i) + + + + def onChangeSelectedRPC(self, i): + # Don't update when we are clearing the box + if not self.updatingRPCbox: + # persist setting + self.parent.cache['selectedRPC_index'] = persistCacheSetting('cache_RPCindex',i) + self.runInThread(self.updateRPCstatus, (True,), ) + + + + + def onCleanConsole(self): + self.consoleArea.clear() + + + + + + def onSaveConsole(self): + timestamp = strftime('%Y-%m-%d_%H-%M-%S', gmtime(now())) + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + fileName, _ = QFileDialog.getSaveFileName(self,"Save Logs to file","SPMT_Logs_%s.txt" % timestamp,"All Files (*);; Text Files (*.txt)", options=options) + try: + if fileName: + printOK("Saving logs to %s" % fileName) + log_file = open(fileName, 'w+') + log_text = self.consoleArea.toPlainText() + log_file.write(log_text) + log_file.close() + + except Exception as e: + err_msg = "error writing Log file" + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + + + + def onToggleConsole(self): + if self.btn_consoleToggle.text() == 'Hide': + self.btn_consoleToggle.setText('Show') + self.consoleArea.hide() + self.console.setMinimumHeight(70) + self.console.setMaximumHeight(70) + else: + self.console.setMinimumHeight(70) + self.console.setMaximumHeight(starting_height) + self.btn_consoleToggle.setText('Hide') + self.consoleArea.show() + + + + + def showHWstatus(self): + self.updateHWleds() + myPopUp_sb(self, "info", 'SPMT - hw check', "%s" % self.hwStatusMess) + + + + + def showRPCstatus(self, server_index, fDebug): + # Update displayed status only if selected server is not changed + if server_index == self.header.rpcClientsBox.currentIndex(): + self.updateRPCled(fDebug) + if fDebug: + myPopUp_sb(self, "info", 'SPMT - rpc check', "%s" % self.rpcStatusMess) + + + + def updateHWleds(self): + if self.hwStatus == 1: + self.header.hwLed.setPixmap(self.ledHalfPurpleH_icon) + elif self.hwStatus == 2: + self.header.hwLed.setPixmap(self.ledPurpleH_icon) + else: + self.header.hwLed.setPixmap(self.ledGrayH_icon) + self.header.hwLed.setToolTip(self.hwStatusMess) + + + + def updateHWstatus(self, ctrl): + # re-initialize device + try: + self.hwdevice.initDevice(self.header.hwDevices.currentIndex()) + self.hwModel, self.hwStatus, self.hwStatusMess = self.hwdevice.getStatus() + except Exception as e: + printDbg(str(e)) + pass + + printDbg("status:%s - mess: %s" % (self.hwStatus, self.hwStatusMess)) + + + + def updateLastBlockLabel(self): + text = '--' + if self.rpcLastBlock == 1: + text = "Loading block index..." + elif self.rpcConnected and self.rpcLastBlock > 0: + text = str(self.rpcLastBlock) + if not self.isBlockchainSynced: + text += " (Synchronizing)" + + self.header.lastBlockLabel.setText(text) + + + + def updateLastBlockPing(self): + if not self.rpcConnected: + self.header.lastPingBox.setHidden(True) + else: + self.header.lastPingBox.setHidden(False) + if self.rpcResponseTime > 2: + color = "red" + self.header.lastPingIcon.setPixmap(self.connRed_icon) + elif self.rpcResponseTime > 1: + color = "orange" + self.header.lastPingIcon.setPixmap(self.connOrange_icon) + else: + color = "green" + self.header.lastPingIcon.setPixmap(self.connGreen_icon) + if self.rpcResponseTime is not None: + self.header.responseTimeLabel.setText("%.3f" % self.rpcResponseTime) + self.header.responseTimeLabel.setStyleSheet("color: %s" % color) + self.header.lastPingIcon.setStyleSheet("color: %s" % color) + + + + def updateRPCled(self, fDebug=False): + if self.rpcConnected: + self.header.rpcLed.setPixmap(self.ledPurpleH_icon) + if fDebug: + printDbg("Connected to RPC server.") + else: + if self.rpcLastBlock == 1: + self.header.rpcLed.setPixmap(self.ledHalfPurpleH_icon) + if fDebug: + printDbg("Connected to RPC server - Still syncing...") + else: + self.header.rpcLed.setPixmap(self.ledGrayH_icon) + if fDebug: + printDbg("Connection to RPC server failed.") + + self.header.rpcLed.setToolTip(self.rpcStatusMess) + self.updateLastBlockLabel() + self.updateLastBlockPing() + + + + def updateRPClist(self): + # Clear old stuff + self.updatingRPCbox = True + self.header.rpcClientsBox.clear() + public_servers = self.parent.db.getRPCServers(custom=False) + custom_servers = self.parent.db.getRPCServers(custom=True) + self.rpcServersList = public_servers + custom_servers + # Add public servers (italics) + italicsFont = QFont("Times", italic=True) + for s in public_servers: + url = s["protocol"] + "://" + s["host"].split(':')[0] + self.header.rpcClientsBox.addItem(url, s) + self.header.rpcClientsBox.setItemData(self.getServerListIndex(s), italicsFont, Qt.FontRole) + # Add Local Wallet (bold) + boldFont = QFont("Times") + boldFont.setBold(True) + self.header.rpcClientsBox.addItem("Local Wallet", custom_servers[0]) + self.header.rpcClientsBox.setItemData(self.getServerListIndex(custom_servers[0]), boldFont, Qt.FontRole) + # Add custom servers + for s in custom_servers[1:]: + url = s["protocol"] + "://" + s["host"].split(':')[0] + self.header.rpcClientsBox.addItem(url, s) + # reset index + if self.parent.cache['selectedRPC_index'] >= self.header.rpcClientsBox.count(): + # (if manually removed from the config files) replace default index + self.parent.cache['selectedRPC_index'] = persistCacheSetting('cache_RPCindex', DefaultCache["selectedRPC_index"]) + + self.header.rpcClientsBox.setCurrentIndex(self.parent.cache['selectedRPC_index']) + self.updatingRPCbox = False + # reload servers in configure dialog + self.sig_RPClistReloaded.emit() + + + + def updateRPCstatus(self, ctrl, fDebug=False): + self.sig_clearRPCstatus.emit() + self.rpcClient = None + + rpc_index, rpc_protocol, rpc_host, rpc_user, rpc_password = self.getRPCserver() + if fDebug: + printDbg("Trying to connect to RPC %s://%s..." % (rpc_protocol, rpc_host)) + + try: + rpcClient = RpcClient(rpc_protocol, rpc_host, rpc_user, rpc_password) + except Exception as e: + printException(getCallerName(), getFunctionName(), "exception in updateRPCstatus", str(e)) + return + + try: + status, statusMess, lastBlock, r_time1, isTestnet = rpcClient.getStatus() + isBlockchainSynced, r_time2 = rpcClient.isBlockchainSynced() + except Exception as e: + return + + rpcResponseTime = None + if r_time1 is not None and r_time2 !=0 : + rpcResponseTime = round((r_time1+r_time2)/2, 3) + + # Update status and client only if selected server is not changed + if rpc_index != self.header.rpcClientsBox.currentIndex(): + return + + with self.lock: + self.rpcClient = rpcClient + self.rpcConnected = status + self.rpcLastBlock = lastBlock + self.rpcStatusMess = statusMess + self.isBlockchainSynced = isBlockchainSynced + self.rpcResponseTime = rpcResponseTime + # if testnet flag is changed, update api client and persist setting + if isTestnet != self.isTestnetRPC: + self.isTestnetRPC = isTestnet + self.parent.cache['isTestnetRPC'] = persistCacheSetting('isTestnetRPC', isTestnet) + self.apiClient = ApiClient(isTestnet) + self.sig_RPCstatusUpdated.emit(rpc_index, fDebug) diff --git a/src/misc.py b/src/misc.py index 0653cf1..0995d01 100644 --- a/src/misc.py +++ b/src/misc.py @@ -1,224 +1,359 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -from ipaddress import ip_address -sys.path.append(os.path.join(os.path.dirname(__file__), '.')) -import time -from PyQt5.QtCore import QObject, pyqtSignal - -from constants import log_File, user_dir - -def append_to_logfile(text): - try: - logFile = open(log_File, 'a+') - logFile.write(text) - logFile.close() - except Exception as e: - print(e) - - - -def clean_for_html(text): - if text is None: - return "" - return text.replace("<", "{").replace(">","}") - - - -def clear_screen(): - os.system('clear') - - - -def getCallerName(): - try: - return sys._getframe(2).f_code.co_name - except Exception: - return None - - - -def getFunctionName(): - try: - return sys._getframe(1).f_code.co_name - except Exception: - return None - - - -def getVersion(): - import simplejson as json - version_file = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'version.txt') - with open(version_file) as data_file: - data = json.load(data_file) - data_file.close() - return data - - - -def getTxidTxidn(txid, txidn): - if txid is None or txidn is None: - return None - else: - return txid + '-' + str(txidn) - - - -def ipport(ip, port): - if ip is None or port is None: - return None - else: - ipAddr = ip_address(ip) - if ipAddr.version == 4: - return ip + ':' + port - elif ipAddr.version == 6: - return "[" + ip + "]:" + port - else: - raise Exception("invalid IP version number") - - - -def now(): - return int(time.time()) - - - -def printDbg_msg(what): - what = clean_for_html(what) - timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(now())) - log_line = '{} : {}
'.format(timestamp, what) - return log_line - - - -def printDbg(what): - log_line = printDbg_msg(what) - append_to_logfile(log_line) - print(log_line) - - - - -def printException_msg( - caller_name, - function_name, - err_msg, - errargs=None): - VERSION = getVersion() - msg = 'EXCEPTION
' - msg += 'version : %s-%s
' % (VERSION['number'], VERSION['tag']) - msg += 'caller : %s
' % caller_name - msg += 'function : %s
' % function_name - msg += '' - if errargs: - msg += 'err: %s
' % str(errargs) - - msg += '===> %s

' % err_msg - return msg - - - -def printException(caller_name, - function_name, - err_msg, - errargs=None): - text = printException_msg(caller_name, function_name, err_msg, errargs) - append_to_logfile(text) - print(text) - - - - -def printOK(what): - msg = '===> ' + what + '
' - append_to_logfile(msg) - print(msg) - - - -def splitString(text, n): - arr = [text[i:i+n] for i in range(0, len(text), n)] - return '\n'.join(arr) - - - - -def readRPCfile(): - try: - import simplejson as json - config_file = os.path.join(user_dir, 'rpcServer.json') - if os.path.exists(config_file): - with open(config_file) as data_file: - rpc_config = json.load(data_file) - data_file.close() - else: - raise Exception("No rpcServer.json found. Creating new.") - except Exception as e: - # save default config and return it - config = {"rpc_ip": "127.0.0.1", "rpc_port": 45458, "rpc_user": "myUsername", "rpc_password": "myPassword"} - writeRPCfile(config) - return "127.0.0.1", 45458, "myUsername", "myPassword" - - rpc_ip = rpc_config.get('rpc_ip') - rpc_port = int(rpc_config.get('rpc_port')) - rpc_user = rpc_config.get('rpc_user') - rpc_password = rpc_config.get('rpc_password') - - return rpc_ip, rpc_port, rpc_user, rpc_password - - - -def sec_to_time(seconds): - days = seconds//86400 - seconds -= days*86400 - hrs = seconds//3600 - seconds -= hrs*3600 - mins = seconds//60 - seconds -= mins*60 - return "{} days, {} hrs, {} mins, {} secs".format(days, hrs, mins, seconds) - - - - - -def writeRPCfile(configuration): - try: - import simplejson as json - rpc_file = os.path.join(user_dir, 'rpcServer.json') - with open(rpc_file, 'w+') as data_file: - json.dump(configuration, data_file) - data_file.close() - - except Exception as e: - printException(getCallerName(), getFunctionName(), "error writing RPC file", e.args) - - - -# Stream object to redirect sys.stdout and sys.stderr to a queue -class WriteStream(object): - def __init__(self, queue): - self.queue = queue - - def write(self, text): - self.queue.put(text) - - def flush(self): - pass - - - -# QObject (to be run in QThread) that blocks until data is available -# and then emits a QtSignal to the main thread. -class WriteStreamReceiver(QObject): - mysignal = pyqtSignal(str) - - def __init__(self, queue, *args, **kwargs): - QObject.__init__(self, *args, **kwargs) - self.queue = queue - - def run(self): - while True: - text = self.queue.get() - self.mysignal.emit(text) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os, sys +from ipaddress import ip_address +import logging +import simplejson as json +import time +from urllib.parse import urlparse + +from PyQt5.QtCore import QObject, pyqtSignal, QSettings + +from constants import log_File, DefaultCache +from PyQt5.QtWidgets import QMessageBox + + +def add_defaultKeys_to_dict(dictObj, defaultObj): + for key in defaultObj: + if key not in dictObj: + dictObj[key] = defaultObj[key] + + + +QT_MESSAGE_TYPE = { + "info": QMessageBox.Information, + "warn": QMessageBox.Warning, + "crit": QMessageBox.Critical, + "quest": QMessageBox.Question + } + + +def checkRPCstring(urlstring, action_msg="Malformed credentials"): + try: + o = urlparse(urlstring) + if o.scheme is None or o.scheme == '': + raise Exception("Wrong protocol. Set either http or https.") + if o.netloc is None or o.netloc == '': + raise Exception("Malformed host network location part.") + if o.port is None or o.port == '': + raise Exception("Wrong IP port number") + if o.username is None: + raise Exception("Malformed username") + if o.password is None: + raise Exception("Malformed password") + return True + + except Exception as e: + error_msg = "Unable to parse URL" + printException(getCallerName(), getFunctionName(), error_msg, e) + return False + + + +def clean_for_html(text): + if text is None: + return "" + return text.replace("<", "{").replace(">","}") + + + +def clear_screen(): + os.system('clear') + + + +def getCallerName(inDecorator=False): + try: + if inDecorator: + return sys._getframe(3).f_code.co_name + return sys._getframe(2).f_code.co_name + except Exception: + return None + + + +def getFunctionName(inDecorator=False): + try: + if inDecorator: + return sys._getframe(2).f_code.co_name + return sys._getframe(1).f_code.co_name + except Exception: + return None + + + +def getVersion(): + version_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), 'version.txt') + with open(version_file) as data_file: + data = json.load(data_file) + + return data + + + +def getTxidTxidn(txid, txidn): + if txid is None or txidn is None: + return None + else: + return txid + '-' + str(txidn) + + + +def initLogs(): + filename = log_File + filemode = 'w' + format = '%(asctime)s - %(levelname)s - %(threadName)s | %(message)s' + level = logging.DEBUG + logging.basicConfig(filename=filename, + filemode=filemode, + format=format, + level=level + ) + + + +def ipport(ip, port): + if ip is None or port is None: + return None + elif ip.endswith('.onion'): + return ip + ':' + port + else: + ipAddr = ip_address(ip) + if ipAddr.version == 4: + return ip + ':' + port + elif ipAddr.version == 6: + return "[" + ip + "]:" + port + else: + raise Exception("invalid IP version number") + + + +def myPopUp(parentWindow, messType, messTitle, messText, defaultButton=QMessageBox.No): + if messType in QT_MESSAGE_TYPE: + type = QT_MESSAGE_TYPE[messType] + else: + type = QMessageBox.Question + mess = QMessageBox(type, messTitle, messText, defaultButton, parent=parentWindow) + mess.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + mess.setDefaultButton(defaultButton) + return mess.exec_() + + + +def myPopUp_sb(parentWindow, messType, messTitle, messText, singleButton=QMessageBox.Ok): + if messType in QT_MESSAGE_TYPE: + type = QT_MESSAGE_TYPE[messType] + else: + type = QMessageBox.Information + mess = QMessageBox(type, messTitle, messText, singleButton, parent=parentWindow) + mess.setStandardButtons(singleButton | singleButton) + return mess.exec_() + + + +def is_hex(s): + try: + int(s, 16) + return True + except ValueError: + return False + + +def now(): + return int(time.time()) + + + +def persistCacheSetting(cache_key, cache_value): + settings = QSettings('PIVX', 'SecurePivxMasternodeTool') + if not settings.contains(cache_key): + printDbg("Cache key %s not found" % str(cache_key)) + printOK("Adding new cache key to settings...") + + if type(cache_value) in [list, dict]: + settings.setValue(cache_key, json.dumps(cache_value)) + else: + settings.setValue(cache_key, cache_value) + + return cache_value + + + +def printDbg(what): + logging.info(what) + log_line = printDbg_msg(what) + print(log_line) + + + +def printDbg_msg(what): + what = clean_for_html(what) + timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(now())) + log_line = '{} : {}
'.format(timestamp, what) + return log_line + + + +def printError( + caller_name, + function_name, + what +): + logging.error("%s | %s | %s" % (caller_name, function_name, what)) + log_line = printException_msg(caller_name, function_name, what, None, True) + print(log_line) + + + +def printException( + caller_name, + function_name, + err_msg, + errargs=None +): + what = err_msg + if errargs is not None: + what += " ==> %s" % str(errargs) + logging.warning("%s | %s | %s" % (caller_name, function_name, what)) + text = printException_msg(caller_name, function_name, err_msg, errargs) + print(text) + + + +def printException_msg( + caller_name, + function_name, + err_msg, + errargs=None, + is_error=False +): + if is_error: + msg = 'ERROR
' + else: + msg = 'EXCEPTION
' + msg += 'caller : %s
' % caller_name + msg += 'function : %s
' % function_name + msg += '' + if errargs: + msg += 'err: %s
' % str(errargs) + + msg += '===> %s

' % err_msg + return msg + + + + +def printOK(what): + logging.debug(what) + msg = '===> ' + what + '
' + print(msg) + + + +def splitString(text, n): + arr = [text[i:i+n] for i in range(0, len(text), n)] + return '\n'.join(arr) + + + + +def readCacheSettings(): + settings = QSettings('PIVX', 'PET4L') + try: + cache = {} + cache["lastAddress"] = settings.value('cache_lastAddress', DefaultCache["lastAddress"], type=str) + cache["window_width"] = settings.value('cache_winWidth', DefaultCache["window_width"], type=int) + cache["window_height"] = settings.value('cache_winHeight', DefaultCache["window_height"], type=int) + cache["splitter_x"] = settings.value('cache_splitterX', DefaultCache["splitter_x"], type=int) + cache["splitter_y"] = settings.value('cache_splitterY', DefaultCache["splitter_y"], type=int) + cache["console_hidden"] = settings.value('cache_consoleHidden', DefaultCache["console_hidden"], type=bool) + cache["useSwiftX"] = settings.value('cache_useSwiftX', DefaultCache["useSwiftX"], type=bool) + cache["selectedHW_index"] = settings.value('cache_HWindex', DefaultCache["selectedHW_index"], type=int) + cache["selectedRPC_index"] = settings.value('cache_RPCindex', DefaultCache["selectedRPC_index"], type=int) + cache["isTestnetRPC"] = settings.value('cache_isTestnetRPC', DefaultCache["isTestnetRPC"], type=bool) + add_defaultKeys_to_dict(cache, DefaultCache) + return cache + except: + return DefaultCache + + +def saveCacheSettings(cache): + settings = QSettings('PIVX', 'PET4L') + settings.setValue('cache_lastAddress', cache.get('lastAddress')) + settings.setValue('cache_useSwiftX', cache.get('useSwiftX')) + settings.setValue('cache_winWidth', cache.get('window_width')) + settings.setValue('cache_winHeight', cache.get('window_height')) + settings.setValue('cache_splitterX', cache.get('splitter_x')) + settings.setValue('cache_splitterY', cache.get('splitter_y')) + settings.setValue('cache_consoleHidden', cache.get('console_hidden')) + settings.setValue('cache_HWindex', cache.get('selectedHW_index')) + settings.setValue('cache_RPCindex', cache.get('selectedRPC_index')) + settings.setValue('cache_isTestnetRPC', cache.get('isTestnetRPC')) + + + +def sec_to_time(seconds): + days = seconds//86400 + seconds -= days*86400 + hrs = seconds//3600 + seconds -= hrs*3600 + mins = seconds//60 + seconds -= mins*60 + return "{} days, {} hrs, {} mins, {} secs".format(days, hrs, mins, seconds) + + + + +def splitString(text, n): + arr = [text[i:i+n] for i in range(0, len(text), n)] + return '\n'.join(arr) + + + + +def timeThis(function, *args): + try: + start = time.clock() + val = function(*args) + end = time.clock() + return val, (end-start) + except Exception: + return None, None + + + +class DisconnectedException(Exception): + def __init__(self, message, hwDevice): + # Call the base class constructor + super().__init__(message) + # clear device + hwDevice.closeDevice() + + +# Stream object to redirect sys.stdout and sys.stderr to a queue +class WriteStream(object): + def __init__(self, queue): + self.queue = queue + + def write(self, text): + self.queue.put(text) + + def flush(self): + pass + + + +# QObject (to be run in QThread) that blocks until data is available +# and then emits a QtSignal to the main thread. +class WriteStreamReceiver(QObject): + mysignal = pyqtSignal(str) + + def __init__(self, queue, *args, **kwargs): + QObject.__init__(self, *args, **kwargs) + self.queue = queue + + def run(self): + while True: + text = self.queue.get() + self.mysignal.emit(text) diff --git a/src/pivx_b58.py b/src/pivx_b58.py index f0ece50..5febbbe 100644 --- a/src/pivx_b58.py +++ b/src/pivx_b58.py @@ -1,68 +1,66 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*-import sys -import os -import sys -sys.path.append(os.path.join(os.path.dirname(__file__), '.')) - -__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -__b58base = len(__b58chars) -b58chars = __b58chars - -long = int -_bchr = lambda x: bytes([x]) -_bord = lambda x: x - - -def b58encode(v): - """ encode v, which is a string of bytes, to base58. - """ - long_value = 0 - for (i, c) in enumerate(v[::-1]): - long_value += (256**i) * _bord(c) - - result = '' - while long_value >= __b58base: - div, mod = divmod(long_value, __b58base) - result = __b58chars[mod] + result - long_value = div - result = __b58chars[long_value] + result - # Bitcoin does a little leading-zero-compression: - # leading 0-bytes in the input become leading-1s - nPad = 0 - for c in v: - # if c == '\0': nPad += 1 - if c == 0: - nPad += 1 - else: - break - - return (__b58chars[0] * nPad) + result - - - -def b58decode(v, length=None): - """ decode v into a string of len bytes - """ - long_value = 0 - for (i, c) in enumerate(v[::-1]): - long_value += __b58chars.find(c) * (__b58base**i) - - result = bytes() - while long_value >= 256: - div, mod = divmod(long_value, 256) - result = _bchr(mod) + result - long_value = div - result = _bchr(long_value) + result - - nPad = 0 - for c in v: - if c == __b58chars[0]: - nPad += 1 - else: - break - - result = _bchr(0) * nPad + result - if length is not None and len(result) != length: - return None - - return result +#!/usr/bin/env python3 +# -*- coding: utf-8 -*-import sys + +__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' +__b58base = len(__b58chars) +b58chars = __b58chars + +long = int +_bchr = lambda x: bytes([x]) +_bord = lambda x: x + + +def b58encode(v): + """ + encode v, which is a string of bytes, to base58. + """ + long_value = 0 + for (i, c) in enumerate(v[::-1]): + long_value += (256**i) * _bord(c) + + result = '' + while long_value >= __b58base: + div, mod = divmod(long_value, __b58base) + result = __b58chars[mod] + result + long_value = div + result = __b58chars[long_value] + result + # Bitcoin does a little leading-zero-compression: + # leading 0-bytes in the input become leading-1s + nPad = 0 + for c in v: + # if c == '\0': nPad += 1 + if c == 0: + nPad += 1 + else: + break + + return (__b58chars[0] * nPad) + result + + + +def b58decode(v, length=None): + """ decode v into a string of len bytes + """ + long_value = 0 + for (i, c) in enumerate(v[::-1]): + long_value += __b58chars.find(c) * (__b58base**i) + + result = bytes() + while long_value >= 256: + div, mod = divmod(long_value, 256) + result = _bchr(mod) + result + long_value = div + result = _bchr(long_value) + result + + nPad = 0 + for c in v: + if c == __b58chars[0]: + nPad += 1 + else: + break + + result = _bchr(0) * nPad + result + if length is not None and len(result) != length: + return None + + return result diff --git a/src/pivx_hashlib.py b/src/pivx_hashlib.py index c689447..4e4d6d3 100644 --- a/src/pivx_hashlib.py +++ b/src/pivx_hashlib.py @@ -1,83 +1,72 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os -sys.path.append(os.path.join(os.path.dirname(__file__), '.')) -import hashlib -import bitcoin -from constants import WIF_PREFIX, MAGIC_BYTE, TESTNET_WIF_PREFIX, TESTNET_MAGIC_BYTE -from pivx_b58 import b58encode, b58decode - -def double_sha256(data): - return hashlib.sha256(hashlib.sha256(data).digest()).digest() - - -def single_sha256(data): - return hashlib.sha256(data).digest() - - -def generate_privkey(isTestnet=False): - """ - Based on Andreas Antonopolous work from 'Mastering Bitcoin'. - """ - base58_secret = TESTNET_WIF_PREFIX if isTestnet else WIF_PREFIX - valid = False - privkey = 0 - while not valid: - privkey = bitcoin.random_key() - decoded_private_key = bitcoin.decode_privkey(privkey, 'hex') - valid = 0 < decoded_private_key < bitcoin.N - data = bytes([base58_secret]) + bytes.fromhex(privkey) - checksum = bitcoin.bin_dbl_sha256(data)[0:4] - return b58encode(data + checksum) - - - -def pubkey_to_address(pubkey, isTestnet=False): - base58_pubkey = TESTNET_MAGIC_BYTE if isTestnet else MAGIC_BYTE - pubkey_bin = bytes.fromhex(pubkey) - pub_hash = bitcoin.bin_hash160(pubkey_bin) - data = bytes([base58_pubkey]) + pub_hash - checksum = bitcoin.bin_dbl_sha256(data)[0:4] - return b58encode(data + checksum) - - - -def num_to_varint(a): - """ - Based on project: https://github.com/chaeplin/dashmnb - """ - x = int(a) - if x < 253: - return x.to_bytes(1, byteorder='big') - elif x < 65536: - return int(253).to_bytes(1, byteorder='big') + x.to_bytes(2, byteorder='little') - elif x < 4294967296: - return int(254).to_bytes(1, byteorder='big') + x.to_bytes(4, byteorder='little') - else: - return int(255).to_bytes(1, byteorder='big') + x.to_bytes(8, byteorder='little') - - - -def wif_to_privkey(string): - wif_compressed = 52 == len(string) - pvkeyencoded = b58decode(string).hex() - wifversion = pvkeyencoded[:2] - checksum = pvkeyencoded[-8:] - vs = bytes.fromhex(pvkeyencoded[:-8]) - check = double_sha256(vs)[0:4] - - if (wifversion == WIF_PREFIX.to_bytes(1, byteorder='big').hex() and checksum == check.hex()) \ - or (wifversion == TESTNET_WIF_PREFIX.to_bytes(1, byteorder='big').hex() and checksum == check.hex()): - - if wif_compressed: - privkey = pvkeyencoded[2:-10] - - else: - privkey = pvkeyencoded[2:-8] - - return privkey - - else: - return None - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import bitcoin +import hashlib + +from constants import WIF_PREFIX, MAGIC_BYTE, TESTNET_WIF_PREFIX, TESTNET_MAGIC_BYTE +from pivx_b58 import b58encode, b58decode + +def double_sha256(data): + return hashlib.sha256(hashlib.sha256(data).digest()).digest() + + + +def single_sha256(data): + return hashlib.sha256(data).digest() + + + +def generate_privkey(isTestnet=False): + """ + Based on Andreas Antonopolous work from 'Mastering Bitcoin'. + """ + base58_secret = TESTNET_WIF_PREFIX if isTestnet else WIF_PREFIX + valid = False + privkey = 0 + while not valid: + privkey = bitcoin.random_key() + decoded_private_key = bitcoin.decode_privkey(privkey, 'hex') + valid = 0 < decoded_private_key < bitcoin.N + data = bytes([base58_secret]) + bytes.fromhex(privkey) + checksum = bitcoin.bin_dbl_sha256(data)[0:4] + return b58encode(data + checksum) + + + +def pubkey_to_address(pubkey, isTestnet=False): + pubkey_bin = bytes.fromhex(pubkey) + pkey_hash = bitcoin.bin_hash160(pubkey_bin) + return pubkeyhash_to_address(pkey_hash, isTestnet) + + + +def pubkeyhash_to_address(pkey_hash, isTestnet=False): + base58_pubkey = TESTNET_MAGIC_BYTE if isTestnet else MAGIC_BYTE + data = bytes([base58_pubkey]) + pkey_hash + checksum = bitcoin.bin_dbl_sha256(data)[0:4] + return b58encode(data + checksum) + + + + +def wif_to_privkey(string): + wif_compressed = 52 == len(string) + pvkeyencoded = b58decode(string).hex() + wifversion = pvkeyencoded[:2] + checksum = pvkeyencoded[-8:] + vs = bytes.fromhex(pvkeyencoded[:-8]) + check = double_sha256(vs)[0:4] + + if (wifversion == WIF_PREFIX.to_bytes(1, byteorder='big').hex() and checksum == check.hex()) \ + or (wifversion == TESTNET_WIF_PREFIX.to_bytes(1, byteorder='big').hex() and checksum == check.hex()): + + if wif_compressed: + privkey = pvkeyencoded[2:-10] + + else: + privkey = pvkeyencoded[2:-8] + + return privkey + + else: + return None diff --git a/src/pivx_parser.py b/src/pivx_parser.py new file mode 100644 index 0000000..e6e23f5 --- /dev/null +++ b/src/pivx_parser.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from utils import extract_pkh_from_locking_script +from pivx_hashlib import pubkeyhash_to_address + +class HexParser(): + def __init__(self, hex_str): + self.cursor = 0 + self.hex_str = hex_str + + def readInt(self, nbytes, byteorder="big", signed=False): + if self.cursor + nbytes * 2 > len(self.hex_str): + raise Exception("HexParser range error") + b = bytes.fromhex(self.hex_str[self.cursor:self.cursor + nbytes * 2]) + res = int.from_bytes(b, byteorder=byteorder, signed=signed) + self.cursor += nbytes * 2 + return res + + def readString(self, nbytes, byteorder="big"): + if self.cursor + nbytes * 2 > len(self.hex_str): + raise Exception("HexParser range error") + res = self.hex_str[self.cursor:self.cursor + nbytes * 2] + self.cursor += nbytes * 2 + if byteorder == "little": + splits = [res[i:i + 2] for i in range(0, len(res), 2)] + return ''.join(splits[::-1]) + return res + + +def IsCoinBase(vin): + return vin["txid"] == "0" * 64 and vin["vout"] == 4294967295 and vin["scriptSig"]["hex"][:2] != "c2" + + +def ParseTxInput(p): + vin = {} + vin["txid"] = p.readString(32, "little") + vin["vout"] = p.readInt(4, "little") + script_len = p.readInt(1, "little") + vin["scriptSig"] = {} + vin["scriptSig"]["hex"] = p.readString(script_len, "big") + vin["sequence"] = p.readInt(4, "little") + if IsCoinBase(vin): + del vin["txid"] + del vin["vout"] + vin["coinbase"] = vin["scriptSig"]["hex"] + del vin["scriptSig"] + + return vin + + +def ParseTxOutput(p, isTestnet=False): + vout = {} + vout["value"] = p.readInt(8, "little") + script_len = p.readInt(1, "little") + vout["scriptPubKey"] = {} + vout["scriptPubKey"]["hex"] = p.readString(script_len, "big") + vout["scriptPubKey"]["addresses"] = [] + try: + add_bytes = extract_pkh_from_locking_script(bytes.fromhex(vout["scriptPubKey"]["hex"])) + address = pubkeyhash_to_address(add_bytes, isTestnet) + vout["scriptPubKey"]["addresses"].append(address) + except Exception as e: + print(e) + return vout + + +def ParseTx(hex_string, isTestnet=False): + p = HexParser(hex_string) + tx = {} + + tx["version"] = p.readInt(4, "little") + + num_of_inputs = p.readInt(1, "little") + tx["vin"] = [] + for i in range(num_of_inputs): + tx["vin"].append(ParseTxInput(p)) + + num_of_outputs = p.readInt(1, "little") + tx["vout"] = [] + for i in range(num_of_outputs): + tx["vout"].append(ParseTxOutput(p, isTestnet)) + + tx["locktime"] = p.readInt(4, "little") + return tx diff --git a/src/qt/dlg_configureRPCserver.py b/src/qt/dlg_configureRPCserver.py deleted file mode 100644 index da6f8b9..0000000 --- a/src/qt/dlg_configureRPCserver.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -from ipaddress import ip_address -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -from PyQt5.QtWidgets import QDialog, QLabel, QSpinBox -from PyQt5.Qt import QPushButton, QGroupBox, QLineEdit, QHBoxLayout, QFormLayout -from PyQt5.QtCore import pyqtSlot -from threads import ThreadFuns - -from misc import writeRPCfile, readRPCfile, printDbg - - -class ConfigureRPCserver_dlg(QDialog): - def __init__(self, main_wnd): - QDialog.__init__(self, parent=main_wnd) - self.main_wnd = main_wnd - self.setWindowTitle('RPC Server Configuration') - self.loadRPCfile() - self.initUI() - - - def initUI(self): - self.ui = Ui_ConfigureRPCserverDlg() - self.ui.setupUi(self) - - - def loadRPCfile(self): - self.rpc_ip, self.rpc_port, self.rpc_user, self.rpc_password = readRPCfile() - - - -class Ui_ConfigureRPCserverDlg(object): - def setupUi(self, ConfigureRPCserverDlg): - ConfigureRPCserverDlg.setModal(True) - ## -- Layout - self.layout = QGroupBox(ConfigureRPCserverDlg) - self.layout.setTitle("Local Pivx-Cli wallet Configuration") - self.layout.setContentsMargins(80, 30, 10, 10) - form = QFormLayout(ConfigureRPCserverDlg) - form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) - ## -- ROW 1 - line1 = QHBoxLayout() - self.edt_rpcIp = QLineEdit() - self.edt_rpcIp.setToolTip("rpc server (local wallet) IP address\n-- example [IPv4] 88.172.23.1\n-- example [IPv6] 2001:db8:85a3::8a2e:370:7334") - self.edt_rpcIp.setText(ConfigureRPCserverDlg.rpc_ip) - line1.addWidget(self.edt_rpcIp) - line1.addWidget(QLabel("IP Port")) - self.edt_rpcPort = QSpinBox() - self.edt_rpcPort.setRange(1, 65535) - self.edt_rpcPort.setValue(ConfigureRPCserverDlg.rpc_port) - self.edt_rpcPort.setFixedWidth(180) - line1.addWidget(self.edt_rpcPort) - form.addRow(QLabel("IP Address"), line1) - ## -- ROW 2 - self.edt_rpcUser = QLineEdit() - self.edt_rpcUser.setText(ConfigureRPCserverDlg.rpc_user) - form.addRow(QLabel("RPC Username"), self.edt_rpcUser) - ## -- ROW 3 - self.edt_rpcPassword = QLineEdit() - self.edt_rpcPassword.setText(ConfigureRPCserverDlg.rpc_password) - form.addRow(QLabel("RPC Password"), self.edt_rpcPassword) - ## -- ROW 4 - hBox = QHBoxLayout() - self.buttonCancel = QPushButton("Cancel") - self.buttonCancel.clicked.connect(lambda: self.onButtonCancel(ConfigureRPCserverDlg)) - hBox.addWidget(self.buttonCancel) - self.buttonSave = QPushButton("Save") - self.buttonSave.clicked.connect(lambda: self.onButtonSave(ConfigureRPCserverDlg)) - hBox.addWidget(self.buttonSave) - form.addRow(hBox) - ## Set Layout - self.layout.setLayout(form) - ConfigureRPCserverDlg.setFixedSize(self.layout.sizeHint()) - - - @pyqtSlot() - def onButtonSave(self, main_dlg): - try: - main_dlg.rpc_ip = ip_address(self.edt_rpcIp.text().strip()).compressed - main_dlg.rpc_port = int(self.edt_rpcPort.value()) - main_dlg.rpc_user = self.edt_rpcUser.text() - main_dlg.rpc_password = self.edt_rpcPassword.text() - conf = {} - conf["rpc_ip"] = main_dlg.rpc_ip - conf["rpc_port"] = main_dlg.rpc_port - conf["rpc_user"] = main_dlg.rpc_user - conf["rpc_password"] = main_dlg.rpc_password - - # Update File - writeRPCfile(conf) - - # Update current RPC Server - main_dlg.main_wnd.mainWindow.rpcClient = None - main_dlg.main_wnd.mainWindow.rpcConnected = False - printDbg("Trying to connect to RPC server [%s]:%s" % (conf["rpc_ip"], str(conf["rpc_port"]))) - self.runInThread = ThreadFuns.runInThread(main_dlg.main_wnd.mainWindow.updateRPCstatus, (), main_dlg.main_wnd.mainWindow.updateRPCled) - main_dlg.close() - - except Exception as e: - print(e) - - - - @pyqtSlot() - def onButtonCancel(self, main_wnd): - main_wnd.close() \ No newline at end of file diff --git a/src/qt/dlg_configureRPCservers.py b/src/qt/dlg_configureRPCservers.py new file mode 100644 index 0000000..2fcbfa9 --- /dev/null +++ b/src/qt/dlg_configureRPCservers.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QLabel, \ + QListWidget, QFrame, QFormLayout, QComboBox, QLineEdit, QListWidgetItem, \ + QWidget, QPushButton, QMessageBox + +from misc import myPopUp, checkRPCstring +from threads import ThreadFuns + + +class ConfigureRPCservers_dlg(QDialog): + def __init__(self, main_wnd): + QDialog.__init__(self, parent=main_wnd) + self.main_wnd = main_wnd + self.setWindowTitle('RPC Servers Configuration') + self.changing_index = None + self.initUI() + self.loadServers() + self.main_wnd.mainWindow.sig_RPClistReloaded.connect(self.loadServers) + + def clearEditFrame(self): + self.ui.user_edt.clear() + self.ui.passwd_edt.clear() + self.ui.protocol_select.setCurrentIndex(0) + self.ui.host_edt.clear() + + def initUI(self): + self.ui = Ui_ConfigureRPCserversDlg() + self.ui.setupUi(self) + + def insert_server_list(self, server): + id = server['id'] + index = self.main_wnd.mainWindow.getServerListIndex(server) + server_line = QWidget() + server_row = QHBoxLayout() + server_text = "%s://%s" % (server['protocol'], server['host']) + if server['id'] == 0 and server['isCustom']: + # Local Wallet + server_text = server_text + "  Local Wallet" + elif not server['isCustom']: + server_text = "%s" % server_text + server_row.addWidget(QLabel(server_text)) + server_row.addStretch(1) + ## -- Edit button + editBtn = QPushButton() + editBtn.setIcon(self.main_wnd.mainWindow.editMN_icon) + editBtn.setToolTip("Edit server configuration") + if not server['isCustom']: + editBtn.setDisabled(True) + editBtn.setToolTip('Default servers are not editable') + editBtn.clicked.connect(lambda: self.onAddServer(index)) + server_row.addWidget(editBtn) + ## -- Remove button + removeBtn = QPushButton() + removeBtn.setIcon(self.main_wnd.mainWindow.removeMN_icon) + removeBtn.setToolTip("Remove server configuration") + if id == 0: + removeBtn.setDisabled(True) + removeBtn.setToolTip('Cannot remove local wallet') + if not server['isCustom']: + removeBtn.setDisabled(True) + removeBtn.setToolTip('Cannot remove default servers') + removeBtn.clicked.connect(lambda: self.onRemoveServer(index)) + server_row.addWidget(removeBtn) + ## -- + server_line.setLayout(server_row) + self.serverItems[id] = QListWidgetItem() + self.serverItems[id].setSizeHint(server_line.sizeHint()) + self.ui.serversBox.addItem(self.serverItems[id]) + self.ui.serversBox.setItemWidget(self.serverItems[id], server_line) + + def loadServers(self): + # Clear serversBox + self.ui.serversBox.clear() + # Fill serversBox + self.serverItems = {} + for server in self.main_wnd.mainWindow.rpcServersList: + self.insert_server_list(server) + + def loadEditFrame(self, index): + server = self.main_wnd.mainWindow.rpcServersList[index] + self.ui.user_edt.setText(server['user']) + self.ui.passwd_edt.setText(server['password']) + if server['protocol'] == 'https': + self.ui.protocol_select.setCurrentIndex(1) + else: + self.ui.protocol_select.setCurrentIndex(0) + self.ui.host_edt.setText(server['host']) + + def onAddServer(self, index=None): + # Save current index (None for new entry) + self.changing_index = index + # Hide 'Add' and 'Close' buttons and disable serversBox + self.ui.addServer_btn.hide() + self.ui.close_btn.hide() + self.ui.serversBox.setEnabled(False) + # Show edit-frame + self.ui.editFrame.setHidden(False) + # If we are adding a new server, clear edit-frame + if index is None: + self.clearEditFrame() + # else pre-load data + else: + self.loadEditFrame(index) + + def onCancel(self): + # Show 'Add' and 'Close' buttons and enable serversBox + self.ui.addServer_btn.show() + self.ui.close_btn.show() + self.ui.serversBox.setEnabled(True) + # Hide edit-frame + self.ui.editFrame.setHidden(True) + # Clear edit-frame + self.clearEditFrame() + + def onClose(self): + # close dialog + self.close() + + def onRemoveServer(self, index): + mess = "Are you sure you want to remove server with index %d (%s) from list?" % ( + index, self.main_wnd.mainWindow.rpcServersList[index].get('host')) + ans = myPopUp(self, QMessageBox.Question, 'SPMT - remove server', mess) + if ans == QMessageBox.Yes: + # Remove entry from database + id = self.main_wnd.mainWindow.rpcServersList[index].get('id') + self.main_wnd.db.removeRPCServer(id) + + def onSave(self): + # Get new config data + protocol = "http" if self.ui.protocol_select.currentIndex() == 0 else "https" + host = self.ui.host_edt.text() + user = self.ui.user_edt.text() + passwd = self.ui.passwd_edt.text() + # Check malformed URL + url_string = "%s://%s:%s@%s" % (protocol, user, passwd, host) + if checkRPCstring(url_string): + if self.changing_index is None: + # Save new entry in DB. + self.main_wnd.db.addRPCServer(protocol, host, user, passwd) + else: + # Edit existing entry to DB. + id = self.main_wnd.mainWindow.rpcServersList[self.changing_index].get('id') + self.main_wnd.db.editRPCServer(protocol, host, user, passwd, id) + # If this was previously selected in mainWindow, update status + clients = self.main_wnd.mainWindow.header.rpcClientsBox + data = clients.itemData(clients.currentIndex()) + if data.get('id') == id and data.get('isCustom'): + ThreadFuns.runInThread(self.main_wnd.mainWindow.updateRPCstatus, (True,), ) + + # call onCancel + self.onCancel() + + +class Ui_ConfigureRPCserversDlg(object): + def setupUi(self, ConfigureRPCserversDlg): + ConfigureRPCserversDlg.setModal(True) + ## -- Layout + self.layout = QVBoxLayout(ConfigureRPCserversDlg) + self.layout.setSpacing(10) + ## -- Servers List + self.serversBox = QListWidget() + self.layout.addWidget(self.serversBox) + ## -- 'Add Server' button + self.addServer_btn = QPushButton("Add RPC Server") + self.layout.addWidget(self.addServer_btn) + ## -- 'Close' button + hBox = QHBoxLayout() + hBox.addStretch(1) + self.close_btn = QPushButton("Close") + hBox.addWidget(self.close_btn) + self.layout.addLayout(hBox) + ## -- Edit section + self.editFrame = QFrame() + frameLayout = QFormLayout() + frameLayout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) + frameLayout.setContentsMargins(5, 10, 5, 5) + frameLayout.setSpacing(7) + self.user_edt = QLineEdit() + frameLayout.addRow(QLabel("Username"), self.user_edt) + self.passwd_edt = QLineEdit() + frameLayout.addRow(QLabel("Password"), self.passwd_edt) + hBox = QHBoxLayout() + self.protocol_select = QComboBox() + self.protocol_select.addItems(['http', 'https']) + hBox.addWidget(self.protocol_select) + hBox.addWidget(QLabel("://")) + self.host_edt = QLineEdit() + self.host_edt.setPlaceholderText('myserver.net:8080') + hBox.addWidget(self.host_edt) + frameLayout.addRow(QLabel("URL"), hBox) + hBox2 = QHBoxLayout() + self.cancel_btn = QPushButton("Cancel") + self.save_btn = QPushButton("Save") + hBox2.addWidget(self.cancel_btn) + hBox2.addWidget(self.save_btn) + frameLayout.addRow(hBox2) + self.editFrame.setLayout(frameLayout) + self.layout.addWidget(self.editFrame) + self.editFrame.setHidden(True) + ConfigureRPCserversDlg.setMinimumWidth(500) + ConfigureRPCserversDlg.setMinimumHeight(500) + # Connect main buttons + self.addServer_btn.clicked.connect(lambda: ConfigureRPCserversDlg.onAddServer()) + self.close_btn.clicked.connect(lambda: ConfigureRPCserversDlg.onClose()) + self.cancel_btn.clicked.connect(lambda: ConfigureRPCserversDlg.onCancel()) + self.save_btn.clicked.connect(lambda: ConfigureRPCserversDlg.onSave()) + diff --git a/src/qt/guiHeader.py b/src/qt/guiHeader.py index 16a1137..bb947bb 100644 --- a/src/qt/guiHeader.py +++ b/src/qt/guiHeader.py @@ -1,64 +1,77 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -from PyQt5.QtCore import Qt -from PyQt5.Qt import QLabel, QGridLayout, QHBoxLayout, QComboBox, QWidget -from PyQt5.QtWidgets import QPushButton -from PyQt5.QtGui import QPixmap, QFont - -class GuiHeader(QWidget): - def __init__(self, caller, *args, **kwargs): - QWidget.__init__(self) - myFont = QFont("Times", italic=True) - layout = QHBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - # --- 1) Check Box - self.centralBox = QGridLayout() - self.centralBox.setContentsMargins(0, 0, 0, 5) - # --- 1a) Select & Check RPC - label1 = QLabel("PIVX server") - self.centralBox.addWidget(label1, 0, 0) - self.rpcClientsBox = QComboBox() - self.rpcClientsBox.setToolTip("Select RPC server.\nLocal must be configured.") - rpcClients = ["Local Wallet"] - self.rpcClientsBox.addItems(rpcClients) - self.centralBox.addWidget(self.rpcClientsBox, 0, 1) - self.button_checkRpc = QPushButton("Connect") - self.button_checkRpc.setToolTip("try to connect to RPC server") - self.button_checkRpc.clicked.connect(caller.onCheckRpc) - self.centralBox.addWidget(self.button_checkRpc, 0, 2) - self.rpcLed = QLabel() - self.rpcLed.setToolTip("status: %s" % caller.rpcStatusMess) - self.rpcLed.setPixmap(caller.ledGrayH_icon) - self.centralBox.addWidget(self.rpcLed, 0, 3) - label2 = QLabel("Last Ping Block:") - self.centralBox.addWidget(label2, 0, 4) - self.lastBlockLabel = QLabel() - self.lastBlockLabel.setFont(myFont) - self.centralBox.addWidget(self.lastBlockLabel, 0, 5) - # -- 1b) Select & Check hardware - label3 = QLabel("HW device") - self.centralBox.addWidget(label3, 1, 0) - self.hwDevices = QComboBox() - self.hwDevices.setToolTip("Select hardware device") - hwDevices = ["Ledger Nano S"] - self.hwDevices.addItems(hwDevices) - self.centralBox.addWidget(self.hwDevices, 1, 1) - self.button_checkHw = QPushButton("Connect") - self.button_checkHw.setToolTip("try to connect to Hardware Wallet") - self.button_checkHw.clicked.connect(caller.onCheckHw) - self.centralBox.addWidget(self.button_checkHw, 1, 2) - self.hwLed = QLabel() - self.hwLed.setToolTip("status: %s" % caller.hwStatusMess) - self.hwLed.setPixmap(caller.ledGrayH_icon) - self.centralBox.addWidget(self.hwLed, 1, 3) - layout.addLayout(self.centralBox) - layout.addStretch(1) - # --- 3) logo - Logo = QLabel() - Logo_file = os.path.join(caller.imgDir, 'pet4lLogo_horiz.png') - Logo.setPixmap(QPixmap(Logo_file).scaledToHeight(87, Qt.SmoothTransformation)) - layout.addWidget(Logo) - self.setLayout(layout) \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QPushButton, QLabel, QGridLayout, QHBoxLayout, QComboBox, QWidget + +from constants import HW_devices +from PyQt5.Qt import QSizePolicy + +class GuiHeader(QWidget): + def __init__(self, caller, *args, **kwargs): + QWidget.__init__(self) + layout = QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + # --- 1) Check Box + self.centralBox = QGridLayout() + self.centralBox.setContentsMargins(0, 0, 0, 5) + # --- 1a) Select & Check RPC + label1 = QLabel("PIVX server") + self.centralBox.addWidget(label1, 0, 0) + self.rpcClientsBox = QComboBox() + self.rpcClientsBox.setToolTip("Select RPC server.") + self.centralBox.addWidget(self.rpcClientsBox, 0, 1) + self.button_checkRpc = QPushButton("Connect/Update") + self.button_checkRpc.setToolTip("try to connect to RPC server") + self.centralBox.addWidget(self.button_checkRpc, 0, 2) + self.rpcLed = QLabel() + self.rpcLed.setToolTip("%s" % caller.rpcStatusMess) + self.rpcLed.setPixmap(caller.ledGrayH_icon) + self.centralBox.addWidget(self.rpcLed, 0, 3) + self.lastPingBox = QWidget() + sp_retain = QSizePolicy() + sp_retain.setRetainSizeWhenHidden(True) + self.lastPingBox.setSizePolicy(sp_retain) + self.lastPingBox.setContentsMargins(0, 0, 0, 0) + lastPingBoxLayout = QHBoxLayout() + self.lastPingIcon = QLabel() + self.lastPingIcon.setToolTip("Last ping server response time.\n(The lower, the better)") + self.lastPingIcon.setPixmap(caller.connRed_icon) + lastPingBoxLayout.addWidget(self.lastPingIcon) + self.responseTimeLabel = QLabel() + self.responseTimeLabel.setToolTip("Last ping server response time.\n(The lower, the better)") + lastPingBoxLayout.addWidget(self.responseTimeLabel) + lastPingBoxLayout.addSpacing(10) + self.lastBlockIcon = QLabel() + self.lastBlockIcon.setToolTip("Last ping block number") + self.lastBlockIcon.setPixmap(caller.lastBlock_icon) + lastPingBoxLayout.addWidget(self.lastBlockIcon) + self.lastBlockLabel = QLabel() + self.lastBlockLabel.setToolTip("Last ping block number") + lastPingBoxLayout.addWidget(self.lastBlockLabel) + self.lastPingBox.setLayout(lastPingBoxLayout) + self.centralBox.addWidget(self.lastPingBox, 0, 4) + # -- 1b) Select & Check hardware + label3 = QLabel("Hardware Device") + self.centralBox.addWidget(label3, 1, 0) + self.hwDevices = QComboBox() + self.hwDevices.setToolTip("Select hardware device") + self.hwDevices.addItems([x[0] for x in HW_devices]) + self.centralBox.addWidget(self.hwDevices, 1, 1) + self.button_checkHw = QPushButton("Connect") + self.button_checkHw.setToolTip("try to connect to Hardware Wallet") + self.centralBox.addWidget(self.button_checkHw, 1, 2) + self.hwLed = QLabel() + self.hwLed.setToolTip("status: %s" % caller.hwStatusMess) + self.hwLed.setPixmap(caller.ledGrayH_icon) + self.centralBox.addWidget(self.hwLed, 1, 3) + layout.addLayout(self.centralBox) + layout.addStretch(1) + # --- 3) logo + Logo = QLabel() + Logo_file = os.path.join(caller.imgDir, 'pet4lLogo_horiz.png') + Logo.setPixmap(QPixmap(Logo_file).scaledToHeight(87, Qt.SmoothTransformation)) + layout.addWidget(Logo) + self.setLayout(layout) diff --git a/src/qt/gui_tabRewards.py b/src/qt/gui_tabRewards.py index 7c3ad1f..0f4f88c 100644 --- a/src/qt/gui_tabRewards.py +++ b/src/qt/gui_tabRewards.py @@ -1,142 +1,165 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -from PyQt5.QtCore import Qt -from PyQt5.Qt import QLabel, QFormLayout, QDoubleSpinBox, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QSpinBox -from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGroupBox, QVBoxLayout -from PyQt5.QtWidgets import QLineEdit, QComboBox - -class TabRewards_gui(QWidget): - def __init__(self, *args, **kwargs): - QWidget.__init__(self) - self.initRewardsForm() - mainVertical = QVBoxLayout() - mainVertical.addWidget(self.rewardsForm) - buttonbox = QHBoxLayout() - buttonbox.addStretch(1) - buttonbox.addWidget(self.btn_Cancel) - mainVertical.addLayout(buttonbox) - self.setLayout(mainVertical) - - - - - def initRewardsForm(self): - self.collateralHidden = True - self.rewardsForm = QGroupBox() - self.rewardsForm.setTitle("Transfer UTXOs") - layout = QFormLayout() - layout.setContentsMargins(10, 10, 10, 10) - layout.setSpacing(13) - layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) - ##--- ROW 1 - line1 = QHBoxLayout() - line1.addWidget(QLabel("Account HW")) - self.edt_hwAccount = QSpinBox() - self.edt_hwAccount.setFixedWidth(50) - self.edt_hwAccount.setToolTip("account number of the hardware wallet.\nIf unsure put 0") - self.edt_hwAccount.setValue(0) - line1.addWidget(self.edt_hwAccount) - line1.addWidget(QLabel("spath from")) - self.edt_spathFrom = QSpinBox() - self.edt_spathFrom.setFixedWidth(50) - self.edt_spathFrom.setToolTip("starting address n.") - self.edt_spathFrom.setValue(0) - line1.addWidget(self.edt_spathFrom) - line1.addWidget(QLabel("spath to")) - self.edt_spathTo = QSpinBox() - self.edt_spathTo.setFixedWidth(50) - self.edt_spathTo.setToolTip("ending address n.") - self.edt_spathTo.setValue(10) - line1.addWidget(self.edt_spathTo) - line1.addWidget(QLabel("internal/external")) - self.edt_internalExternal = QSpinBox() - self.edt_internalExternal.setFixedWidth(50) - self.edt_internalExternal.setToolTip("ending address n.") - self.edt_internalExternal.setValue(0) - self.edt_internalExternal.setMaximum(1) - line1.addWidget(self.edt_internalExternal) - line1.addStretch(1) - self.btn_reload = QPushButton("Scan Ledger device") - self.btn_reload.setToolTip("Reload data from ledger device") - line1.addWidget(self.btn_reload) - layout.addRow(line1) - - hBox = QHBoxLayout() - self.addySelect = QComboBox() - self.addySelect.setToolTip("Select Address") - hBox.addWidget(self.addySelect) - layout.addRow(hBox) - ## --- ROW 2: UTXOs - self.rewardsList = QVBoxLayout() - self.rewardsList.statusLabel = QLabel('Checking explorer...') - self.rewardsList.statusLabel.setVisible(True) - self.rewardsList.addWidget(self.rewardsList.statusLabel) - self.rewardsList.box = QTableWidget() - self.rewardsList.box.setMinimumHeight(200) - #self.rewardsList.box.setMaximumHeight(140) - self.rewardsList.box.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.rewardsList.box.setSelectionMode(QAbstractItemView.MultiSelection) - self.rewardsList.box.setSelectionBehavior(QAbstractItemView.SelectRows) - self.rewardsList.box.setShowGrid(True) - self.rewardsList.box.setColumnCount(4) - self.rewardsList.box.setRowCount(0) - self.rewardsList.box.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) - self.rewardsList.box.verticalHeader().hide() - item = QTableWidgetItem() - item.setText("PIVs") - item.setTextAlignment(Qt.AlignCenter) - self.rewardsList.box.setHorizontalHeaderItem(0, item) - item = QTableWidgetItem() - item.setText("Confirmations") - item.setTextAlignment(Qt.AlignCenter) - self.rewardsList.box.setHorizontalHeaderItem(1, item) - item = QTableWidgetItem() - item.setText("TX Hash") - item.setTextAlignment(Qt.AlignCenter) - self.rewardsList.box.setHorizontalHeaderItem(2, item) - item = QTableWidgetItem() - item.setText("TX Output N") - item.setTextAlignment(Qt.AlignCenter) - self.rewardsList.box.setHorizontalHeaderItem(3, item) - item = QTableWidgetItem() - self.rewardsList.addWidget(self.rewardsList.box) - layout.addRow(self.rewardsList) - ##--- ROW 3 - hBox2 = QHBoxLayout() - self.btn_selectAllRewards = QPushButton("Select All") - self.btn_selectAllRewards.setToolTip("Select all available UTXOs") - hBox2.addWidget(self.btn_selectAllRewards) - self.btn_deselectAllRewards = QPushButton("Deselect All") - self.btn_deselectAllRewards.setToolTip("Deselect current selection") - hBox2.addWidget(self.btn_deselectAllRewards) - hBox2.addWidget(QLabel("Selected UTXOs")) - self.selectedRewardsLine = QLabel() - self.selectedRewardsLine.setMinimumWidth(200) - self.selectedRewardsLine.setStyleSheet("color: purple") - self.selectedRewardsLine.setToolTip("PIVX to move away") - hBox2.addWidget(self.selectedRewardsLine) - hBox2.addStretch(1) - layout.addRow(hBox2) - ##--- ROW 4 - hBox3 = QHBoxLayout() - self.destinationLine = QLineEdit() - self.destinationLine.setToolTip("PIVX address to send PIV to") - hBox3.addWidget(self.destinationLine) - hBox3.addWidget(QLabel("Fee")) - self.feeLine = QDoubleSpinBox() - self.feeLine.setDecimals(8) - self.feeLine.setPrefix("PIV ") - self.feeLine.setToolTip("Insert a small fee amount") - self.feeLine.setFixedWidth(150) - self.feeLine.setSingleStep(0.001) - hBox3.addWidget(self.feeLine) - self.btn_sendRewards = QPushButton("Send") - hBox3.addWidget(self.btn_sendRewards) - layout.addRow(QLabel("Destination Address"), hBox3) - #--- Set Layout - self.rewardsForm.setLayout(layout) - #--- ROW 5 - self.btn_Cancel = QPushButton("Clear") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +import os.path +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) +from PyQt5.QtCore import Qt +from PyQt5.Qt import QLabel, QFormLayout, QDoubleSpinBox, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QSpinBox +from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, QGroupBox, QVBoxLayout +from PyQt5.QtWidgets import QLineEdit, QComboBox, QCheckBox, QProgressBar + +class TabRewards_gui(QWidget): + def __init__(self, imgDir, *args, **kwargs): + QWidget.__init__(self) + self.imgDir = imgDir + self.initRewardsForm() + mainVertical = QVBoxLayout() + mainVertical.addWidget(self.rewardsForm) + buttonbox = QHBoxLayout() + buttonbox.addStretch(1) + buttonbox.addWidget(self.btn_Cancel) + mainVertical.addLayout(buttonbox) + self.setLayout(mainVertical) + + + + + def initRewardsForm(self): + self.rewardsForm = QGroupBox() + self.rewardsForm.setTitle("Transfer UTXOs") + layout = QFormLayout() + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(13) + layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) + ##--- ROW 1 + line1 = QHBoxLayout() + line1.addWidget(QLabel("Account HW")) + self.edt_hwAccount = QSpinBox() + self.edt_hwAccount.setFixedWidth(50) + self.edt_hwAccount.setToolTip("account number of the hardware wallet.\nIf unsure put 0") + self.edt_hwAccount.setValue(0) + line1.addWidget(self.edt_hwAccount) + line1.addWidget(QLabel("spath from")) + self.edt_spathFrom = QSpinBox() + self.edt_spathFrom.setFixedWidth(50) + self.edt_spathFrom.setToolTip("starting address n.") + self.edt_spathFrom.setValue(0) + line1.addWidget(self.edt_spathFrom) + line1.addWidget(QLabel("spath to")) + self.edt_spathTo = QSpinBox() + self.edt_spathTo.setFixedWidth(50) + self.edt_spathTo.setToolTip("ending address n.") + self.edt_spathTo.setValue(10) + line1.addWidget(self.edt_spathTo) + line1.addWidget(QLabel("internal/external")) + self.edt_internalExternal = QSpinBox() + self.edt_internalExternal.setFixedWidth(50) + self.edt_internalExternal.setToolTip("ending address n.") + self.edt_internalExternal.setValue(0) + self.edt_internalExternal.setMaximum(1) + line1.addWidget(self.edt_internalExternal) + line1.addStretch(1) + self.btn_reload = QPushButton("Scan Ledger device") + self.btn_reload.setToolTip("Reload data from ledger device") + line1.addWidget(self.btn_reload) + layout.addRow(line1) + hBox = QHBoxLayout() + self.addySelect = QComboBox() + self.addySelect.setToolTip("Select Address") + hBox.addWidget(self.addySelect) + layout.addRow(hBox) + ## --- ROW 2: UTXOs + self.rewardsList = QVBoxLayout() + self.rewardsList.statusLabel = QLabel('Reload Rewards') + self.rewardsList.statusLabel.setVisible(True) + self.rewardsList.addWidget(self.rewardsList.statusLabel) + self.rewardsList.box = QTableWidget() + self.rewardsList.box.setMinimumHeight(200) + #self.rewardsList.box.setMaximumHeight(140) + self.rewardsList.box.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.rewardsList.box.setSelectionMode(QAbstractItemView.MultiSelection) + self.rewardsList.box.setSelectionBehavior(QAbstractItemView.SelectRows) + self.rewardsList.box.setShowGrid(True) + self.rewardsList.box.setColumnCount(4) + self.rewardsList.box.setRowCount(0) + self.rewardsList.box.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) + self.rewardsList.box.verticalHeader().hide() + item = QTableWidgetItem() + item.setText("PIVs") + item.setTextAlignment(Qt.AlignCenter) + self.rewardsList.box.setHorizontalHeaderItem(0, item) + item = QTableWidgetItem() + item.setText("Confirmations") + item.setTextAlignment(Qt.AlignCenter) + self.rewardsList.box.setHorizontalHeaderItem(1, item) + item = QTableWidgetItem() + item.setText("TX Hash") + item.setTextAlignment(Qt.AlignCenter) + self.rewardsList.box.setHorizontalHeaderItem(2, item) + item = QTableWidgetItem() + item.setText("TX Output N") + item.setTextAlignment(Qt.AlignCenter) + self.rewardsList.box.setHorizontalHeaderItem(3, item) + item = QTableWidgetItem() + self.rewardsList.addWidget(self.rewardsList.box) + layout.addRow(self.rewardsList) + ##--- ROW 3 + hBox2 = QHBoxLayout() + self.btn_selectAllRewards = QPushButton("Select All") + self.btn_selectAllRewards.setToolTip("Select all available UTXOs") + hBox2.addWidget(self.btn_selectAllRewards) + self.btn_deselectAllRewards = QPushButton("Deselect All") + self.btn_deselectAllRewards.setToolTip("Deselect current selection") + hBox2.addWidget(self.btn_deselectAllRewards) + hBox2.addWidget(QLabel("Selected UTXOs")) + self.selectedRewardsLine = QLabel() + self.selectedRewardsLine.setMinimumWidth(200) + self.selectedRewardsLine.setStyleSheet("color: purple") + self.selectedRewardsLine.setToolTip("PIVX to move away") + hBox2.addWidget(self.selectedRewardsLine) + hBox2.addStretch(1) + self.swiftxCheck = QCheckBox() + self.swiftxCheck.setToolTip("check for SwiftX instant transaction (flat fee rate of 0.01 PIV)") + hBox2.addWidget(QLabel("Use SwiftX")) + hBox2.addWidget(self.swiftxCheck) + layout.addRow(hBox2) + ##--- ROW 4 + hBox3 = QHBoxLayout() + self.destinationLine = QLineEdit() + self.destinationLine.setToolTip("PIVX address to send PIV to") + hBox3.addWidget(self.destinationLine) + hBox3.addWidget(QLabel("Fee")) + self.feeLine = QDoubleSpinBox() + self.feeLine.setDecimals(8) + self.feeLine.setPrefix("PIV ") + self.feeLine.setToolTip("Insert a small fee amount") + self.feeLine.setFixedWidth(150) + self.feeLine.setSingleStep(0.001) + hBox3.addWidget(self.feeLine) + self.btn_sendRewards = QPushButton("Send") + hBox3.addWidget(self.btn_sendRewards) + layout.addRow(QLabel("Destination Address"), hBox3) + hBox4 = QHBoxLayout() + hBox4.addStretch(1) + self.loadingLine = QLabel("Preparing TX. Completed: ") + self.loadingLinePercent = QProgressBar() + self.loadingLinePercent.setMaximumWidth(200) + self.loadingLinePercent.setMaximumHeight(10) + self.loadingLinePercent.setRange(0, 100) + hBox4.addWidget(self.loadingLine) + hBox4.addWidget(self.loadingLinePercent) + self.loadingLine.hide() + self.loadingLinePercent.hide() + layout.addRow(hBox4) + #--- Set Layout + self.rewardsForm.setLayout(layout) + #--- ROW 5 + self.btn_Cancel = QPushButton("Clear") + + + def resetStatusLabel(self, message=None): + if message is None: + self.rewardsList.statusLabel.setText('Checking explorer...') + else: + self.rewardsList.statusLabel.setText(message) + self.rewardsList.statusLabel.setVisible(True) diff --git a/src/rpcClient.py b/src/rpcClient.py index f37bd09..89c40ce 100644 --- a/src/rpcClient.py +++ b/src/rpcClient.py @@ -1,204 +1,300 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException -from misc import getCallerName, getFunctionName, printException, printDbg, readRPCfile -from constants import DEFAULT_PROTOCOL_VERSION, MINIMUM_FEE - -class RpcClient: - - def __init__(self): - self.rpc_ip, self.rpc_port, self.rpc_user, self.rpc_passwd = readRPCfile() - rpc_url = "http://%s:%s@%s:%d" % (self.rpc_user, self.rpc_passwd, self.rpc_ip, self.rpc_port) - try: - self.conn = AuthServiceProxy(rpc_url, timeout=8) - except JSONRPCException as e: - err_msg = 'remote or local PIVX-cli running?' - printException(getCallerName(), getFunctionName(), err_msg, e) - except Exception as e: - err_msg = 'remote or local PIVX-cli running?' - printException(getCallerName(), getFunctionName(), err_msg, e) - - - - - def decodeRawTx(self, rawTx): - try: - return self.conn.decoderawtransaction(rawTx) - except Exception as e: - err_msg = 'error in decodeRawTx' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def getAddressUtxos(self, addresses): - try: - return self.conn.getaddressutxos({'addresses': addresses}) - except Exception as e: - err_msg = "error in getAddressUtxos" - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - printException(getCallerName(), getFunctionName(), err_msg, e.args) - raise e - - - - - def getBlockCount(self): - try: - n = self.conn.getblockcount() - return n - except Exception as e: - err_msg = 'remote or local PIVX-cli running?' - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def getBlockHash(self, blockNum): - try: - h = self.conn.getblockhash(blockNum) - return h - except Exception as e: - err_msg = 'remote or local PIVX-cli running?' - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - def getFeePerKb(self): - try: - # get transaction data from last 10 blocks - feePerKb = float(self.conn.getfeeinfo(10)['feeperkb']) - return (feePerKb if feePerKb > MINIMUM_FEE else MINIMUM_FEE) - except Exception as e: - err_msg = 'error in getFeePerKb' - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - def getMNStatus(self, address): - try: - mnStatusList = self.conn.listmasternodes(address) - if not mnStatusList: - return None - mnStatus = mnStatusList[0] - mnStatus['mnCount'] = self.conn.getmasternodecount()['enabled'] - return mnStatus - - except Exception as e: - err_msg = "error in getMNStatus" - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def getProtocolVersion(self): - try: - prot_version = self.conn.getinfo().get('protocolversion') - return int(prot_version) - - except Exception as e: - err_msg = 'error in getProtocolVersion' - printException(getCallerName(), getFunctionName(), err_msg, e.args) - return DEFAULT_PROTOCOL_VERSION - - - - - def getRawTransaction(self, txid): - try: - return self.conn.getrawtransaction(txid) - except Exception as e: - err_msg = "error in getRawTransaction for txid=%s" % txid - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - return None - - - - - def getStatus(self): - status = False - n = 0 - try: - n = self.conn.getblockcount() - if n > 0: - status = True - - except Exception as e: - # If loading block index set lastBlock=1 - if str(e.args[0]) == "Loading block index..." or str(e.args[0]) == "Verifying blocks...": - printDbg(str(e.args[0])) - n = 1 - #else: - #err_msg = "Error while contacting RPC server" - #printException(getCallerName(), getFunctionName(), err_msg, e.args) - return status, n - - - - - def getStatusMess(self, status=None): - if status == None: - status = self.getStatus() - if status: - return "RPC status: CONNECTED!!!" - else: - return "RPC status: NOT CONNECTED. remote or local PIVX-cli running?" - - - - - - def decodemasternodebroadcast(self, work): - try: - return self.conn.decodemasternodebroadcast(work.strip()) - except Exception as e: - err_msg = "error in decodemasternodebroadcast" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - def relaymasternodebroadcast(self, work): - try: - return self.conn.relaymasternodebroadcast(work.strip()) - except Exception as e: - err_msg = "error in relaymasternodebroadcast" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - def sendRawTransaction(self, tx_hex): - try: - tx_id = self.conn.sendrawtransaction(tx_hex) - return tx_id - except Exception as e: - err_msg = 'error in rpcClient.sendRawTransaction' - if str(e.args[0]) != "Request-sent": - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def verifyMessage(self, pivxaddress, signature, message): - try: - return self.conn.verifymessage(pivxaddress, signature, message) - - except Exception as e: - err_msg = "error in verifyMessage" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from bitcoinrpc.authproxy import AuthServiceProxy +try: + import http.client as httplib +except ImportError: + import httplib +import ssl +import threading + +from constants import DEFAULT_PROTOCOL_VERSION, MINIMUM_FEE +from misc import getCallerName, getFunctionName, printException, printDbg, now, timeThis + +def process_RPC_exceptions(func): + + def process_RPC_exceptions_int(*args, **kwargs): + try: + args[0].httpConnection.connect() + return func(*args, **kwargs) + + except Exception as e: + message = "Exception in RPC client" + printException(getCallerName(True), getFunctionName(True), message, str(e)) + finally: + try: + args[0].httpConnection.close() + except Exception as e: + printDbg(e) + pass + + return process_RPC_exceptions_int + + + +class RpcClient: + + def __init__(self, rpc_protocol, rpc_host, rpc_user, rpc_password): + # Lock for threads + self.lock = threading.RLock() + + self.rpc_url = "%s://%s:%s@%s" % (rpc_protocol, rpc_user, rpc_password, rpc_host) + + host, port = rpc_host.split(":") + if rpc_protocol == "https": + self.httpConnection = httplib.HTTPSConnection(host, port, timeout=20, context=ssl._create_unverified_context()) + else: + self.httpConnection = httplib.HTTPConnection(host, port, timeout=20) + + self.conn = AuthServiceProxy(self.rpc_url, timeout=1000, connection=self.httpConnection) + + + + + @process_RPC_exceptions + def getBlockCount(self): + n = 0 + with self.lock: + n = self.conn.getblockcount() + + return n + + + + @process_RPC_exceptions + def getBlockHash(self, blockNum): + h = None + with self.lock: + h = self.conn.getblockhash(blockNum) + + return h + + + + @process_RPC_exceptions + def getBudgetVotes(self, proposal): + votes = {} + with self.lock: + votes = self.conn.getbudgetvotes(proposal) + + return votes + + + + @process_RPC_exceptions + def getFeePerKb(self): + res = MINIMUM_FEE + with self.lock: + # get transaction data from last 200 blocks + feePerKb = float(self.conn.getfeeinfo(200)['feeperkb']) + res = (feePerKb if feePerKb > MINIMUM_FEE else MINIMUM_FEE) + + return res + + + + @process_RPC_exceptions + def getMNStatus(self, address): + mnStatus = None + with self.lock: + mnStatusList = self.conn.listmasternodes(address) + if not mnStatusList: + return None + mnStatus = mnStatusList[0] + mnStatus['mnCount'] = self.conn.getmasternodecount()['enabled'] + + return mnStatus + + + + @process_RPC_exceptions + def getMasternodeCount(self): + ans = None + with self.lock: + ans = self.conn.getmasternodecount() + + return ans + + + + @process_RPC_exceptions + def getMasternodes(self): + printDbg("RPC: Getting masternode list...") + mnList = {} + score = [] + masternodes = [] + with self.lock: + masternodes = self.conn.listmasternodes() + + for mn in masternodes: + if mn.get('status') == 'ENABLED': + # compute masternode score + if mn.get('lastpaid') == 0: + mn['score'] = mn.get('activetime') + else: + lastpaid_ago = now() - mn.get('lastpaid') + mn['score'] = min(lastpaid_ago, mn.get('activetime')) + + else: + mn['score'] = 0 + + score.append(mn) + + # sort masternodes by decreasing score + score.sort(key=lambda x: x['score'], reverse=True) + + # save masternode position in the payment queue + for mn in masternodes: + mn['queue_pos'] = score.index(mn) + + mnList['masternodes'] = masternodes + + return mnList + + + + @process_RPC_exceptions + def getNextSuperBlock(self): + n = 0 + with self.lock: + n = self.conn.getnextsuperblock() + + return n + + + + + @process_RPC_exceptions + def getProposalsProjection(self): + printDbg("RPC: Getting proposals projection...") + data = [] + proposals = [] + with self.lock: + # get budget projection JSON data + data = self.conn.getbudgetprojection() + + for p in data: + # create proposal-projection dictionary + new_proposal = {} + new_proposal['Name'] = p.get('Name') + new_proposal['Allotted'] = float(p.get("Alloted")) + new_proposal['Votes'] = p.get('Yeas') - p.get('Nays') + new_proposal['Total_Allotted'] = float(p.get('TotalBudgetAlloted')) + # append dictionary to list + proposals.append(new_proposal) + + # return proposals list + return proposals + + + + @process_RPC_exceptions + def getProtocolVersion(self): + res = DEFAULT_PROTOCOL_VERSION + with self.lock: + prot_version = self.conn.getinfo().get('protocolversion') + res = int(prot_version) + + return res + + + + @process_RPC_exceptions + def getRawTransaction(self, txid): + res = None + with self.lock: + res = self.conn.getrawtransaction(txid) + + return res + + + + @process_RPC_exceptions + def getStatus(self): + status = False + statusMess = "Unable to connect to a PIVX RPC server.\n" + statusMess += "Either the local PIVX wallet is not open, or the remote RPC server is not responding." + n = 0 + response_time = None + with self.lock: + isTestnet = self.conn.getinfo()['testnet'] + n, response_time = timeThis(self.conn.getblockcount) + if n is None: + n = 0 + + if n > 0: + status = True + statusMess = "Connected to PIVX Blockchain" + + return status, statusMess, n, response_time, isTestnet + + + + @process_RPC_exceptions + def isBlockchainSynced(self): + res = False + response_time = None + with self.lock: + status, response_time = timeThis(self.conn.mnsync, 'status') + if status is not None: + res = status.get("IsBlockchainSynced") + + return res, response_time + + + + @process_RPC_exceptions + def mnBudgetRawVote(self, mn_tx_hash, mn_tx_index, proposal_hash, vote, time, vote_sig): + res = None + with self.lock: + res = self.conn.mnbudgetrawvote(mn_tx_hash, mn_tx_index, proposal_hash, vote, time, vote_sig) + + return res + + + + @process_RPC_exceptions + def decodemasternodebroadcast(self, work): + printDbg("RPC: Decoding masternode broadcast...") + res = "" + with self.lock: + res = self.conn.decodemasternodebroadcast(work.strip()) + + return res + + + + @process_RPC_exceptions + def relaymasternodebroadcast(self, work): + printDbg("RPC: Relaying masternode broadcast...") + res = "" + with self.lock: + res = self.conn.relaymasternodebroadcast(work.strip()) + + return res + + + + @process_RPC_exceptions + def sendRawTransaction(self, tx_hex, use_swiftx): + dbg_mess = "RPC: Sending raw transaction" + if use_swiftx: + dbg_mess += " with SwiftX" + dbg_mess += "..." + printDbg(dbg_mess) + tx_id = None + with self.lock: + tx_id = self.conn.sendrawtransaction(tx_hex, True, bool(use_swiftx)) + + return tx_id + + + + @process_RPC_exceptions + def verifyMessage(self, pivxaddress, signature, message): + printDbg("RPC: Verifying message...") + res = False + with self.lock: + res = self.conn.verifymessage(pivxaddress, signature, message) + + return res diff --git a/src/tabRewards.py b/src/tabRewards.py index 89a801d..428d5d2 100644 --- a/src/tabRewards.py +++ b/src/tabRewards.py @@ -1,297 +1,427 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -import os.path -sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -from misc import printDbg, printException, getCallerName, getFunctionName -from threads import ThreadFuns -from utils import checkPivxAddr -from apiClient import ApiClient -from constants import MINIMUM_FEE - -from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QFont -from PyQt5.Qt import QTableWidgetItem, QHeaderView, QItemSelectionModel -from PyQt5.QtWidgets import QMessageBox - -from qt.gui_tabRewards import TabRewards_gui - - -class TabRewards(): - def __init__(self, caller): - self.caller = caller - self.apiClient = ApiClient() - ##--- Initialize Selection - self.rewards = None - self.selectedRewards = None - self.rawtransactions = {} - ##--- Initialize GUI - self.ui = TabRewards_gui() - self.caller.tabRewards = self.ui - self.ui.feeLine.setValue(MINIMUM_FEE) - # Connect GUI buttons - self.ui.addySelect.currentIndexChanged.connect(lambda: self.onChangeSelected()) - self.ui.rewardsList.box.itemClicked.connect(lambda: self.updateSelection()) - self.ui.btn_reload.clicked.connect(lambda: self.loadSelection()) - self.ui.btn_selectAllRewards.clicked.connect(lambda: self.onSelectAllRewards()) - self.ui.btn_deselectAllRewards.clicked.connect(lambda: self.onDeselectAllRewards()) - self.ui.btn_sendRewards.clicked.connect(lambda: self.onSendRewards()) - self.ui.btn_Cancel.clicked.connect(lambda: self.onCancel()) - - - - - def display_utxos(self): - try: - if self.rewards is not None: - def item(value): - item = QTableWidgetItem(value) - item.setTextAlignment(Qt.AlignCenter) - item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) - return item - - self.ui.rewardsList.box.setRowCount(len(self.rewards)) - for row, utxo in enumerate(self.rewards): - txId = utxo.get('tx_hash', None) - - pivxAmount = round(int(utxo.get('value', 0))/1e8, 8) - self.ui.rewardsList.box.setItem(row, 0, item(str(pivxAmount))) - self.ui.rewardsList.box.setItem(row, 1, item(str(utxo.get('confirmations', None)))) - self.ui.rewardsList.box.setItem(row, 2, item(txId)) - self.ui.rewardsList.box.setItem(row, 3, item(str(utxo.get('tx_ouput_n', None)))) - self.ui.rewardsList.box.showRow(row) - - if len(self.rewards) > 0: - self.ui.rewardsList.box.resizeColumnsToContents() - self.ui.rewardsList.statusLabel.setVisible(False) - self.ui.rewardsList.box.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) - - else: - if not self.caller.rpcConnected: - self.ui.rewardsList.statusLabel.setText('PIVX wallet not connected') - else: - if self.apiConnected: - self.ui.rewardsList.statusLabel.setText('Found no UTXOs for %s' % self.curr_addr) - else: - self.ui.rewardsList.statusLabel.setText('Unable to connect to API provider') - self.ui.rewardsList.statusLabel.setVisible(True) - except Exception as e: - print(e) - - - - - def getSelection(self): - try: - returnData = [] - items = self.ui.rewardsList.box.selectedItems() - # Save row indexes to a set to avoid repetition - rows = set() - for i in range(0, len(items)): - row = items[i].row() - rows.add(row) - rowList = list(rows) - - return [self.rewards[row] for row in rowList] - - return returnData - except Exception as e: - print(e) - - - - @pyqtSlot() - def loadSelection(self): - # Check dongle - printDbg("Checking HW device") - if self.caller.hwStatus != 2: - self.caller.myPopUp2(QMessageBox.Critical, 'PET4L - hw device check', "Connect to HW device first") - printDbg("Unable to connect - hw status: %d" % self.caller.hwStatus) - return None - - self.ui.addySelect.clear() - ThreadFuns.runInThread(self.loadSelection_thread, ()) - - - - def loadSelection_thread(self, ctrl): - hwAcc = self.ui.edt_hwAccount.value() - spathFrom = self.ui.edt_spathFrom.value() - spathTo = self.ui.edt_spathTo.value() - intExt = self.ui.edt_internalExternal.value() - - for i in range(spathFrom, spathTo+1): - path = "44'/77'/%d'/%d/%d" % (hwAcc, intExt, i) - address = self.caller.hwdevice.scanForAddress(path) - try: - balance = self.apiClient.getBalance(address) - except Exception as e: - print(e) - balance = 0 - - itemLine = "%s -- %s" % (path, address) - if(balance): - itemLine += " [%s PIV]" % str(balance) - self.ui.addySelect.addItem(itemLine, [path, address, balance]) - - - def load_utxos_thread(self, ctrl): - self.apiConnected = False - try: - if not self.caller.rpcConnected: - self.rewards = [] - printDbg('PIVX daemon not connected') - - else: - try: - if self.apiClient.getStatus() != 200: - return - self.apiConnected = True - self.blockCount = self.caller.rpcClient.getBlockCount() - self.rewards = self.apiClient.getAddressUtxos(self.curr_addr)['unspent_outputs'] - for utxo in self.rewards: - self.rawtransactions[utxo['tx_hash']] = self.caller.rpcClient.getRawTransaction(utxo['tx_hash']) - - except Exception as e: - self.errorMsg = 'Error occurred while calling getaddressutxos method: ' + str(e) - printDbg(self.errorMsg) - - except Exception as e: - print(e) - pass - - - - - @pyqtSlot() - def onCancel(self): - self.ui.selectedRewardsLine.setText("0.0") - self.ui.addySelect.setCurrentIndex(0) - self.ui.destinationLine.setText('') - self.ui.feeLine.setValue(MINIMUM_FEE) - self.onChangeSelected() - - - - - @pyqtSlot() - def onChangeSelected(self): - if self.ui.addySelect.currentIndex() >= 0: - self.curr_path = self.ui.addySelect.itemData(self.ui.addySelect.currentIndex())[0] - self.curr_addr = self.ui.addySelect.itemData(self.ui.addySelect.currentIndex())[1] - self.curr_balance = self.ui.addySelect.itemData(self.ui.addySelect.currentIndex())[2] - - if self.curr_balance is not None: - self.runInThread = ThreadFuns.runInThread(self.load_utxos_thread, (), self.display_utxos) - - - - @pyqtSlot() - def onSelectAllRewards(self): - self.ui.rewardsList.box.selectAll() - self.updateSelection() - - - @pyqtSlot() - def onDeselectAllRewards(self): - self.ui.rewardsList.box.clearSelection() - self.updateSelection() - - - - - @pyqtSlot() - def onSendRewards(self): - self.dest_addr = self.ui.destinationLine.text().strip() - - # Check dongle - printDbg("Checking HW device") - if self.caller.hwStatus != 2: - self.caller.myPopUp2(QMessageBox.Critical, 'PET4L - hw device check', "Connect to HW device first") - printDbg("Unable to connect - hw status: %d" % self.caller.hwStatus) - return None - - # Check destination Address - if not checkPivxAddr(self.dest_addr): - self.caller.myPopUp2(QMessageBox.Critical, 'PET4L - PIVX address check', "Invalid Destination Address") - return None - - - # LET'S GO - printDbg("Sending from PIVX address %s to PIVX address %s " % (self.curr_addr, self.dest_addr)) - if self.selectedRewards: - self.currFee = self.ui.feeLine.value() * 1e8 - # connect signal - self.caller.hwdevice.sigTxdone.connect(self.FinishSend) - try: - self.txFinished = False - self.caller.hwdevice.prepare_transfer_tx(self.caller, self.curr_path, self.selectedRewards, self.dest_addr, self.currFee, self.rawtransactions) - except Exception as e: - err_msg = "Error while preparing transaction" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - else: - self.caller.myPopUp2(QMessageBox.Information, 'transaction NOT Sent', "No UTXO to send") - - - - - # Activated by signal sigTxdone from hwdevice - @pyqtSlot(bytearray, str) - def FinishSend(self, serialized_tx, amount_to_send): - if not self.txFinished: - try: - self.txFinished = True - tx_hex = serialized_tx.hex() - printDbg("Raw signed transaction: " + tx_hex) - printDbg("Amount to send :" + amount_to_send) - - if len(tx_hex) > 90000: - mess = "Transaction's length exceeds 90000 bytes. Select less UTXOs and try again." - self.caller.myPopUp2(QMessageBox.Warning, 'transaction Warning', mess) - - else: - message = 'Broadcast signed transaction?

Destination address:
%s

' % (self.dest_addr) - message += 'Amount to send: %s PIV
' % amount_to_send - message += 'Fee: %s PIV
Size: %d bytes' % (str(round(self.currFee / 1e8, 8) ), len(tx_hex)/2) - reply = self.caller.myPopUp(QMessageBox.Information, 'Send transaction', message) - if reply == QMessageBox.Yes: - txid = self.caller.rpcClient.sendRawTransaction(tx_hex) - mess = QMessageBox(QMessageBox.Information, 'transaction Sent', 'transaction Sent') - mess.setDetailedText(txid) - mess.exec_() - - else: - self.caller.myPopUp2(QMessageBox.Information, 'transaction NOT Sent', "transaction NOT sent") - - self.onCancel() - - except Exception as e: - err_msg = "Exception in sendRewards" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - - - def updateSelection(self, clicked_item=None): - total = 0 - self.selectedRewards = self.getSelection() - numOfInputs = len(self.selectedRewards) - if numOfInputs: - - for i in range(0, numOfInputs): - total += int(self.selectedRewards[i].get('value')) - - # update suggested fee and selected rewards - estimatedTxSize = (44+numOfInputs*148)*1.0 / 1000 # kB - feePerKb = self.caller.rpcClient.getFeePerKb() - suggestedFee = round(feePerKb * estimatedTxSize, 8) - printDbg("estimatedTxSize is %s kB" % str(estimatedTxSize)) - printDbg("suggested fee is %s PIV (%s PIV/kB)" % (str(suggestedFee), str(feePerKb))) - - self.ui.selectedRewardsLine.setText(str(round(total/1e8, 8))) - self.ui.feeLine.setValue(suggestedFee) - - else: - self.ui.selectedRewardsLine.setText("") - self.ui.feeLine.setValue(MINIMUM_FEE) - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import threading +import simplejson as json + +from PyQt5.Qt import QApplication, pyqtSignal +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QMessageBox, QTableWidgetItem, QHeaderView + +from constants import MINIMUM_FEE +from misc import printDbg, printError, printException, getCallerName, getFunctionName, \ + persistCacheSetting, myPopUp, myPopUp_sb, DisconnectedException +from pivx_parser import ParseTx +from qt.gui_tabRewards import TabRewards_gui +from threads import ThreadFuns +from utils import checkPivxAddr + + +class TabRewards(): + def __init__(self, caller): + self.caller = caller + ##--- Lock for loading UTXO thread + self.runInThread = ThreadFuns.runInThread + self.Lock = threading.Lock() + + ##--- Initialize Selection + self.utxoLoaded = False + self.selectedRewards = None + self.feePerKb = MINIMUM_FEE + self.suggestedFee = MINIMUM_FEE + + ##--- Initialize GUI + self.ui = TabRewards_gui(self.caller.imgDir) + self.caller.tabRewards = self.ui + + # load last used destination from cache + self.ui.destinationLine.setText(self.caller.parent.cache.get("lastAddress")) + # load useSwiftX check from cache + if self.caller.parent.cache.get("useSwiftX"): + self.ui.swiftxCheck.setChecked(True) + + self.updateFee() + + # Connect GUI buttons + self.ui.addySelect.currentIndexChanged.connect(lambda: self.onChangeSelected()) + self.ui.rewardsList.box.itemClicked.connect(lambda: self.updateSelection()) + self.ui.btn_reload.clicked.connect(lambda: self.loadSelection()) + self.ui.btn_selectAllRewards.clicked.connect(lambda: self.onSelectAllRewards()) + self.ui.btn_deselectAllRewards.clicked.connect(lambda: self.onDeselectAllRewards()) + self.ui.swiftxCheck.clicked.connect(lambda: self.updateFee()) + self.ui.btn_sendRewards.clicked.connect(lambda: self.onSendRewards()) + self.ui.btn_Cancel.clicked.connect(lambda: self.onCancel()) + + # Connect Signals + self.caller.sig_UTXOsLoading.connect(self.update_loading_utxos) + self.caller.sig_UTXOsLoaded.connect(self.display_utxos) + + + + def display_utxos(self): + # update fee + if self.caller.rpcConnected: + self.feePerKb = self.caller.rpcClient.getFeePerKb() + if self.feePerKb is None: + self.feePerKb = MINIMUM_FEE + else: + self.feePerKb = MINIMUM_FEE + + rewards = self.caller.parent.db.getRewardsList(self.curr_addr) + + if rewards is not None: + def item(value): + item = QTableWidgetItem(value) + item.setTextAlignment(Qt.AlignCenter) + item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + return item + + # Clear up old list + self.ui.rewardsList.box.setRowCount(0) + # Make room for new list + self.ui.rewardsList.box.setRowCount(len(rewards)) + # Insert items + for row, utxo in enumerate(rewards): + txId = utxo.get('txid', None) + pivxAmount = round(int(utxo.get('satoshis', 0)) / 1e8, 8) + self.ui.rewardsList.box.setItem(row, 0, item(str(pivxAmount))) + self.ui.rewardsList.box.setItem(row, 1, item(str(utxo.get('confirmations', None)))) + self.ui.rewardsList.box.setItem(row, 2, item(txId)) + self.ui.rewardsList.box.setItem(row, 3, item(str(utxo.get('vout', None)))) + self.ui.rewardsList.box.showRow(row) + + self.ui.rewardsList.box.resizeColumnsToContents() + + if len(rewards) > 0: + self.ui.rewardsList.statusLabel.setVisible(False) + self.ui.rewardsList.box.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) + + else: + if not self.caller.rpcConnected: + self.ui.resetStatusLabel('PIVX wallet not connected') + else: + self.ui.resetStatusLabel('Found no Rewards for %s' % self.curr_addr) + + + + def getSelection(self): + # Get selected rows indexes + items = self.ui.rewardsList.box.selectedItems() + rows = set() + for i in range(0, len(items)): + row = items[i].row() + rows.add(row) + indexes = list(rows) + # Get UTXO info from DB for each + selection = [] + for idx in indexes: + txid = self.ui.rewardsList.box.item(idx, 2).text() + txidn = int(self.ui.rewardsList.box.item(idx, 3).text()) + selection.append(self.caller.parent.db.getReward(txid, txidn)) + + return selection + + + + def loadSelection(self): + # Check dongle + printDbg("Checking HW device") + if self.caller.hwStatus != 2: + myPopUp_sb(self.caller, "crit", 'PET4L - hw device check', "Connect to HW device first") + printDbg("Unable to connect - hw status: %d" % self.caller.hwStatus) + return None + + self.ui.addySelect.clear() + ThreadFuns.runInThread(self.loadSelection_thread, ()) + + + + def loadSelection_thread(self, ctrl): + hwAcc = self.ui.edt_hwAccount.value() + spathFrom = self.ui.edt_spathFrom.value() + spathTo = self.ui.edt_spathTo.value() + intExt = self.ui.edt_internalExternal.value() + isTestnet = self.caller.isTestnetRPC + + for i in range(spathFrom, spathTo+1): + path = "%d'/%d/%d" % (hwAcc, intExt, i) + address = self.caller.hwdevice.scanForAddress(hwAcc, i, intExt, isTestnet) + try: + balance = self.caller.apiClient.getBalance(address) + except Exception as e: + print(e) + balance = 0 + + itemLine = "%s -- %s" % (path, address) + if(balance): + itemLine += " [%s PIV]" % str(balance) + + self.ui.addySelect.addItem(itemLine, [path, address, balance]) + + + + def load_utxos_thread(self, ctrl): + with self.Lock: + # clear utxos DB + printDbg("Updating UTXOs...") + self.caller.parent.db.clearTable('UTXOS') + self.utxoLoaded = False + + if not self.caller.rpcConnected: + printError(getCallerName(), getFunctionName(), 'PIVX daemon not connected - Unable to update UTXO list') + return + + utxos = self.caller.apiClient.getAddressUtxos(self.curr_addr) + total_num_of_utxos = len(utxos) + + # Get raw transactions + curr_utxo = 0 + percent = 0 + for u in utxos: + rawtx = None + percent = int(100 * curr_utxo / total_num_of_utxos) + rawtx = self.caller.rpcClient.getRawTransaction(u['txid']) + + # break if raw TX is unavailable + if rawtx is None: + return + + # Save utxo to db + u['receiver'] = self.curr_addr + u['raw_tx'] = rawtx + self.caller.parent.db.addReward(u) + + # emit percent + self.caller.sig_UTXOsLoading.emit(percent) + curr_utxo += 1 + + self.caller.sig_UTXOsLoading.emit(100) + printDbg("--# REWARDS table updated") + self.utxoLoaded = True + self.caller.sig_UTXOsLoaded.emit() + + + + def onCancel(self): + self.ui.rewardsList.box.clearSelection() + self.selectedRewards = None + self.ui.selectedRewardsLine.setText("0.0") + self.suggestedFee = MINIMUM_FEE + self.updateFee() + self.AbortSend() + + + + def onChangeSelected(self): + if self.ui.addySelect.currentIndex() >= 0: + self.ui.resetStatusLabel() + self.curr_path = self.ui.addySelect.itemData(self.ui.addySelect.currentIndex())[0] + self.curr_addr = self.ui.addySelect.itemData(self.ui.addySelect.currentIndex())[1] + self.curr_balance = self.ui.addySelect.itemData(self.ui.addySelect.currentIndex())[2] + + if self.curr_balance is not None: + self.runInThread = ThreadFuns.runInThread(self.load_utxos_thread, (), self.display_utxos) + + + + def onSelectAllRewards(self): + self.ui.rewardsList.box.selectAll() + self.updateSelection() + + + + def onDeselectAllRewards(self): + self.ui.rewardsList.box.clearSelection() + self.updateSelection() + + + + def onSendRewards(self): + self.dest_addr = self.ui.destinationLine.text().strip() + + # Check HW device + if self.caller.hwStatus != 2: + myPopUp_sb(self.caller, "crit", 'SPMT - hw device check', "Connect to HW device first") + printDbg("Unable to connect to hardware device. The device status is: %d" % self.caller.hwStatus) + return None + + # Check destination Address + if not checkPivxAddr(self.dest_addr, self.caller.isTestnetRPC): + myPopUp_sb(self.caller, "crit", 'SPMT - PIVX address check', "The destination address is missing, or invalid.") + return None + + # LET'S GO + if self.selectedRewards: + printDbg("Sending from PIVX address %s to PIVX address %s " % (self.curr_addr, self.dest_addr)) + printDbg("Preparing transaction. Please wait...") + try: + self.ui.loadingLine.show() + self.ui.loadingLinePercent.show() + QApplication.processEvents() + self.currFee = self.ui.feeLine.value() * 1e8 + + # save last destination address and swiftxCheck to cache and persist to settings + self.caller.parent.cache["lastAddress"] = persistCacheSetting('cache_lastAddress', self.dest_addr) + self.caller.parent.cache["useSwiftX"] = persistCacheSetting('cache_useSwiftX', self.useSwiftX()) + + self.currFee = self.ui.feeLine.value() * 1e8 + # re-connect signals + try: + self.caller.hwdevice.api.sigTxdone.disconnect() + except: + pass + try: + self.caller.hwdevice.api.sigTxabort.disconnect() + except: + pass + try: + self.caller.hwdevice.api.tx_progress.disconnect() + except: + pass + self.caller.hwdevice.api.sigTxdone.connect(self.FinishSend) + self.caller.hwdevice.api.sigTxabort.connect(self.AbortSend) + self.caller.hwdevice.api.tx_progress.connect(self.updateProgressPercent) + + try: + self.txFinished = False + self.caller.hwdevice.prepare_transfer_tx(self.caller, self.curr_path, self.selectedRewards, + self.dest_addr, self.currFee, self.useSwiftX(), + self.caller.isTestnetRPC) + except DisconnectedException as e: + self.caller.hwStatus = 0 + self.caller.updateHWleds() + + except Exception as e: + err_msg = "Error while preparing transaction.
" + err_msg += "Probably Blockchain wasn't synced when trying to fetch raw TXs.
" + err_msg += "Wait for full synchronization then hit 'Clear/Reload'" + printException(getCallerName(), getFunctionName(), err_msg, e.args) + except Exception as e: + print(e) + else: + myPopUp_sb(self.caller, "warn", 'Transaction NOT sent', "No UTXO to send") + + + + def removeSpentRewards(self): + for utxo in self.selectedRewards: + self.caller.parent.db.deleteReward(utxo['txid'], utxo['vout']) + + + + + # Activated by signal sigTxdone from hwdevice + def FinishSend(self, serialized_tx, amount_to_send): + self.AbortSend() + if not self.txFinished: + try: + self.txFinished = True + tx_hex = serialized_tx.hex() + printDbg("Raw signed transaction: " + tx_hex) + printDbg("Amount to send :" + amount_to_send) + + if len(tx_hex) > 90000: + mess = "Transaction's length exceeds 90000 bytes. Select less UTXOs and try again." + self.caller.myPopUp2(QMessageBox.Warning, 'transaction Warning', mess) + + else: + decodedTx = None + try: + decodedTx = ParseTx(tx_hex, self.caller.isTestnetRPC) + destination = decodedTx.get("vout")[0].get("scriptPubKey").get("addresses")[0] + amount = decodedTx.get("vout")[0].get("value") + message = '

Broadcast signed transaction?

Destination address:
%s

' % destination + message += '

Amount: %s PIV
' % str(amount) + message += 'Fees: %s PIV
Size: %d Bytes

' % ( + str(round(self.currFee / 1e8, 8)), len(tx_hex) / 2) + except Exception as e: + printException(getCallerName(), getFunctionName(), "decoding exception", str(e)) + message = '

Unable to decode TX- Broadcast anyway?

' + + mess1 = QMessageBox(QMessageBox.Information, 'Send transaction', message) + if decodedTx is not None: + mess1.setDetailedText(json.dumps(decodedTx, indent=4, sort_keys=False)) + mess1.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + + reply = mess1.exec_() + if reply == QMessageBox.Yes: + txid = self.caller.rpcClient.sendRawTransaction(tx_hex, self.useSwiftX()) + if txid is None: + raise Exception("Unable to send TX - connection to RPC server lost.") + mess2_text = "

Transaction successfully sent.

" + mess2 = QMessageBox(QMessageBox.Information, 'transaction Sent', mess2_text) + mess2.setDetailedText(txid) + mess2.exec_() + # remove spent rewards from DB + self.removeSpentRewards() + # reload utxos + self.display_utxos() + self.onCancel() + + else: + myPopUp_sb(self.caller, "warn", 'Transaction NOT sent', "Transaction NOT sent") + self.onCancel() + + except Exception as e: + err_msg = "Exception in FinishSend" + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + + + # Activated by signal sigTxabort from hwdevice + def AbortSend(self): + self.ui.loadingLine.hide() + self.ui.loadingLinePercent.setValue(0) + self.ui.loadingLinePercent.hide() + + + + def updateFee(self): + if self.useSwiftX(): + self.ui.feeLine.setValue(0.01) + self.ui.feeLine.setEnabled(False) + else: + self.ui.feeLine.setValue(self.suggestedFee) + self.ui.feeLine.setEnabled(True) + + + + # Activated by signal tx_progress from hwdevice + def updateProgressPercent(self, percent): + self.ui.loadingLinePercent.setValue(percent) + QApplication.processEvents() + + + + def updateSelection(self, clicked_item=None): + total = 0 + self.selectedRewards = self.getSelection() + numOfInputs = len(self.selectedRewards) + if numOfInputs: + for i in range(0, numOfInputs): + total += int(self.selectedRewards[i].get('satoshis')) + + # update suggested fee and selected rewards + estimatedTxSize = (44+numOfInputs*148)*1.0 / 1000 # kB + feePerKb = self.caller.rpcClient.getFeePerKb() + self.suggestedFee = round(feePerKb * estimatedTxSize, 8) + printDbg("estimatedTxSize is %s kB" % str(estimatedTxSize)) + printDbg("suggested fee is %s PIV (%s PIV/kB)" % (str(self.suggestedFee), str(feePerKb))) + + self.ui.selectedRewardsLine.setText(str(round(total/1e8, 8))) + + else: + self.ui.selectedRewardsLine.setText("") + + self.updateFee() + + + + def update_loading_utxos(self, percent): + self.ui.resetStatusLabel('Checking explorer... %d%%' % percent) + + + + def useSwiftX(self): + return self.ui.swiftxCheck.isChecked() + diff --git a/src/threads.py b/src/threads.py index 1b9ed85..608dd30 100644 --- a/src/threads.py +++ b/src/threads.py @@ -1,55 +1,55 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" - Based on project: - https://github.com/Bertrand256/dash-masternode-tool -""" -import threading -import traceback -from functools import partial -from workerThread import WorkerThread - -class ThreadFuns: - @staticmethod - def runInThread(worker_fun, worker_fun_args, on_thread_finish=None, on_thread_exception=None, - skip_raise_exception=False): - """ - Run a function inside a thread. - :param worker_fun: reference to function to be executed inside a thread - :param worker_fun_args: arguments passed to a thread function - :param on_thread_finish: function to be called after thread finishes its execution - :param skip_raise_exception: Exception raised inside the 'worker_fun' will be passed to the calling thread if: - - on_thread_exception is a valid function (it's exception handler) - - skip_raise_exception is False - :return: reference to a thread object - """ - - def on_thread_finished_int(thread_arg, on_thread_finish_arg, skip_raise_exception_arg, on_thread_exception_arg): - if thread_arg.worker_exception: - if on_thread_exception_arg: - on_thread_exception_arg(thread_arg.worker_exception) - else: - if not skip_raise_exception_arg: - raise thread_arg.worker_exception - else: - if on_thread_finish_arg: - on_thread_finish_arg() - - if threading.current_thread() != threading.main_thread(): - # starting thread from another thread causes an issue of not passing arguments' - # values to on_thread_finished_int function, so on_thread_finish is not called - st = traceback.format_stack() - print('Running thread from inside another thread. Stack: \n' + ''.join(st)) - - thread = WorkerThread(worker_fun=worker_fun, worker_fun_args=worker_fun_args) - - # in Python 3.5 local variables sometimes are removed before calling on_thread_finished_int - # so we have to bind that variables with the function ref - bound_on_thread_finished = partial(on_thread_finished_int, thread, on_thread_finish, skip_raise_exception, - on_thread_exception) - - thread.finished.connect(bound_on_thread_finished) - thread.daemon = True - thread.start() - return thread - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" + Based on project: + https://github.com/Bertrand256/dash-masternode-tool +""" +import threading +import traceback +from functools import partial +from workerThread import WorkerThread + + +class ThreadFuns: + @staticmethod + def runInThread(worker_fun, worker_fun_args, on_thread_finish=None, on_thread_exception=None, + skip_raise_exception=False): + """ + Run a function inside a thread. + :param worker_fun: reference to function to be executed inside a thread + :param worker_fun_args: arguments passed to a thread function + :param on_thread_finish: function to be called after thread finishes its execution + :param skip_raise_exception: Exception raised inside the 'worker_fun' will be passed to the calling thread if: + - on_thread_exception is a valid function (it's exception handler) + - skip_raise_exception is False + :return: reference to a thread object + """ + + def on_thread_finished_int(thread_arg, on_thread_finish_arg, skip_raise_exception_arg, on_thread_exception_arg): + if thread_arg.worker_exception: + if on_thread_exception_arg: + on_thread_exception_arg(thread_arg.worker_exception) + else: + if not skip_raise_exception_arg: + raise thread_arg.worker_exception + else: + if on_thread_finish_arg: + on_thread_finish_arg() + + if threading.current_thread() != threading.main_thread(): + # starting thread from another thread causes an issue of not passing arguments' + # values to on_thread_finished_int function, so on_thread_finish is not called + st = traceback.format_stack() + print('Running thread from inside another thread. Stack: \n' + ''.join(st)) + + thread = WorkerThread(worker_fun=worker_fun, worker_fun_args=worker_fun_args) + + # in Python 3.5 local variables sometimes are removed before calling on_thread_finished_int + # so we have to bind that variables with the function ref + bound_on_thread_finished = partial(on_thread_finished_int, thread, on_thread_finish, skip_raise_exception, + on_thread_exception) + + thread.finished.connect(bound_on_thread_finished) + thread.daemon = True + thread.start() + return thread diff --git a/src/utils.py b/src/utils.py index 044cb23..586f9c5 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,200 +1,234 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import base64 -from misc import getCallerName, getFunctionName, printException -from bitcoin import b58check_to_hex, ecdsa_raw_sign, ecdsa_raw_verify, privkey_to_pubkey, encode_sig, decode_sig, dbl_sha256 -from pivx_hashlib import wif_to_privkey -from pivx_b58 import b58decode -from bitcoin import bin_dbl_sha256 -from ipaddress import ip_address -# Bitcoin opcodes used in the application -OP_DUP = b'\x76' -OP_HASH160 = b'\xA9' -OP_QEUALVERIFY = b'\x88' -OP_CHECKSIG = b'\xAC' -OP_EQUAL = b'\x87' -# Prefixes - Check P2SH -P2PKH_PREFIXES = ['D'] -P2SH_PREFIXES = ['7'] - - -def b64encode(text): - return base64.b64encode(bytearray.fromhex(text)).decode('utf-8') - - - -def checkPivxAddr(address): - try: - # check leading char 'D' - if address[0] != 'D': - return False - - # decode and verify checksum - addr_bin = bytes.fromhex(b58decode(address).hex()) - addr_bin_check = bin_dbl_sha256(addr_bin[0:-4])[0:4] - if addr_bin[-4:] != addr_bin_check: - return False - - return True - except Exception: - return False - - - -def compose_tx_locking_script(dest_address): - """ - Create a Locking script (ScriptPubKey) that will be assigned to a transaction output. - :param dest_address: destination address in Base58Check format - :return: sequence of opcodes and its arguments, defining logic of the locking script - """ - pubkey_hash = bytearray.fromhex(b58check_to_hex(dest_address)) # convert address to a public key hash - if len(pubkey_hash) != 20: - raise Exception('Invalid length of the public key hash: ' + str(len(pubkey_hash))) - - if dest_address[0] in P2PKH_PREFIXES: - # sequence of opcodes/arguments for p2pkh (pay-to-public-key-hash) - scr = OP_DUP + \ - OP_HASH160 + \ - int.to_bytes(len(pubkey_hash), 1, byteorder='little') + \ - pubkey_hash + \ - OP_QEUALVERIFY + \ - OP_CHECKSIG - elif dest_address[0] in P2SH_PREFIXES: - # sequence of opcodes/arguments for p2sh (pay-to-script-hash) - scr = OP_HASH160 + \ - int.to_bytes(len(pubkey_hash), 1, byteorder='little') + \ - pubkey_hash + \ - OP_EQUAL - else: - raise Exception('Invalid dest address prefix: ' + dest_address[0]) - return scr - - - -def ecdsa_sign(msg, priv): - """ - Based on project: https://github.com/chaeplin/dashmnb. - """ - v, r, s = ecdsa_raw_sign(electrum_sig_hash(msg), priv) - sig = encode_sig(v, r, s) - pubkey = privkey_to_pubkey(wif_to_privkey(priv)) - - ok = ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pubkey) - if not ok: - raise Exception('Bad signature!') - return sig - - - -def electrum_sig_hash(message): - """ - Based on project: https://github.com/chaeplin/dashmnb. - """ - padded = b'\x18DarkNet Signed Message:\n' + num_to_varint(len(message)) + from_string_to_bytes(message) - return dbl_sha256(padded) - - - -def extract_pkh_from_locking_script(script): - if len(script) == 25: - if script[0:1] == OP_DUP and script[1:2] == OP_HASH160: - if read_varint(script, 2)[0] == 20: - return script[3:23] - else: - raise Exception('Non-standard public key hash length (should be 20)') - raise Exception('Non-standard locking script type (should be P2PKH)') - - - -def from_string_to_bytes(a): - return a if isinstance(a, bytes) else bytes(a, 'utf-8') - - - -def ipmap(ip, port): - try: - ipAddr = ip_address(ip) - ipv6map = '' - - if ipAddr.version==4: - ipv6map = '00000000000000000000ffff' - ip_digits = map(int, ipAddr.exploded.split('.')) - for i in ip_digits: - ipv6map += i.to_bytes(1, byteorder='big')[::-1].hex() - - elif ipAddr.version==6: - ip_hextets = map(str, ipAddr.exploded.split(':')) - for a in ip_hextets: - ipv6map += a - - else: - raise Exception("invalid version number (%d)" % version) - - - ipv6map += int(port).to_bytes(2, byteorder='big').hex() - if len(ipv6map) != 36: - raise Exception("Problems! len is %d" % len(ipv6map)) - return ipv6map - - except Exception as e: - err_msg = "error in ipmap" - printException(getCallerName(), getFunctionName(), err_msg, e.args) - - - -def num_to_varint(a): - """ - Based on project: https://github.com/chaeplin/dashmnb - """ - x = int(a) - if x < 253: - return x.to_bytes(1, byteorder='big') - elif x < 65536: - return int(253).to_bytes(1, byteorder='big') + x.to_bytes(2, byteorder='little') - elif x < 4294967296: - return int(254).to_bytes(1, byteorder='big') + x.to_bytes(4, byteorder='little') - else: - return int(255).to_bytes(1, byteorder='big') + x.to_bytes(8, byteorder='little') - - - -def read_varint(buffer, offset): - if (buffer[offset] < 0xfd): - value_size = 1 - value = buffer[offset] - elif (buffer[offset] == 0xfd): - value_size = 3 - value = int.from_bytes(buffer[offset + 1: offset + 3], byteorder='little') - elif (buffer[offset] == 0xfe): - value_size = 5 - value = int.from_bytes(buffer[offset + 1: offset + 5], byteorder='little') - elif (buffer[offset] == 0xff): - value_size = 9 - value = int.from_bytes(buffer[offset + 1: offset + 9], byteorder='little') - else: - raise Exception("Invalid varint size") - return value, value_size - - - -def serialize_input_str(tx, prevout_n, sequence, script_sig): - """ - Based on project: https://github.com/chaeplin/dashmnb. - """ - s = ['CTxIn('] - s.append('COutPoint(%s, %s)' % (tx, prevout_n)) - s.append(', ') - if tx == '00' * 32 and prevout_n == 0xffffffff: - s.append('coinbase %s' % script_sig) - - else: - script_sig2 = script_sig - if len(script_sig2) > 24: - script_sig2 = script_sig2[0:24] - s.append('scriptSig=%s' % script_sig2) - - if sequence != 0xffffffff: - s.append(', nSequence=%d' % sequence) - - s.append(')') - return ''.join(s) \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import base64 +from bitcoin import bin_hash160, b58check_to_hex, ecdsa_raw_sign, ecdsa_raw_verify, privkey_to_pubkey, \ + encode_sig, decode_sig, dbl_sha256, bin_dbl_sha256 +from ipaddress import ip_address + +from misc import getCallerName, getFunctionName, printException +from pivx_b58 import b58decode +from pivx_hashlib import wif_to_privkey + + +# Bitcoin opcodes used in the application +OP_DUP = b'\x76' +OP_HASH160 = b'\xA9' +OP_QEUALVERIFY = b'\x88' +OP_CHECKSIG = b'\xAC' +OP_EQUAL = b'\x87' +OP_RETURN = b'\x6a' +# Prefixes - Check P2SH +P2PKH_PREFIXES = ['D'] +P2SH_PREFIXES = ['7'] + + +def b64encode(text): + return base64.b64encode(bytearray.fromhex(text)).decode('utf-8') + + + +def checkPivxAddr(address, isTestnet=False): + try: + # check leading char 'D' or (for testnet) 'x' or 'y' + if isTestnet and address[0] not in ['x', 'y']: + return False + if not isTestnet and address[0] != 'D': + return False + + # decode and verify checksum + addr_bin = bytes.fromhex(b58decode(address).hex()) + addr_bin_check = bin_dbl_sha256(addr_bin[0:-4])[0:4] + if addr_bin[-4:] != addr_bin_check: + return False + + return True + except Exception: + return False + + + +def compose_tx_locking_script(dest_address): + """ + Create a Locking script (ScriptPubKey) that will be assigned to a transaction output. + :param dest_address: destination address in Base58Check format + :return: sequence of opcodes and its arguments, defining logic of the locking script + """ + pubkey_hash = bytearray.fromhex(b58check_to_hex(dest_address)) # convert address to a public key hash + if len(pubkey_hash) != 20: + raise Exception('Invalid length of the public key hash: ' + str(len(pubkey_hash))) + + if dest_address[0] in P2PKH_PREFIXES: + # sequence of opcodes/arguments for p2pkh (pay-to-public-key-hash) + scr = OP_DUP + \ + OP_HASH160 + \ + int.to_bytes(len(pubkey_hash), 1, byteorder='little') + \ + pubkey_hash + \ + OP_QEUALVERIFY + \ + OP_CHECKSIG + elif dest_address[0] in P2SH_PREFIXES: + # sequence of opcodes/arguments for p2sh (pay-to-script-hash) + scr = OP_HASH160 + \ + int.to_bytes(len(pubkey_hash), 1, byteorder='little') + \ + pubkey_hash + \ + OP_EQUAL + else: + raise Exception('Invalid dest address prefix: ' + dest_address[0]) + return scr + + + +def compose_tx_locking_script_OR(message): + """ + Create a Locking script (ScriptPubKey) that will be assigned to a transaction output. + :param message: data for the OP_RETURN + :return: sequence of opcodes and its arguments, defining logic of the locking script + """ + data = message.encode() + scr = OP_RETURN + int.to_bytes(len(data), 1, byteorder='little') + data + + return scr + + + +def ecdsa_sign(msg, priv): + """ + Based on project: https://github.com/chaeplin/dashmnb. + """ + v, r, s = ecdsa_raw_sign(electrum_sig_hash(msg), priv) + sig = encode_sig(v, r, s) + pubkey = privkey_to_pubkey(wif_to_privkey(priv)) + + ok = ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pubkey) + if not ok: + raise Exception('Bad signature!') + return sig + + + +def electrum_sig_hash(message): + """ + Based on project: https://github.com/chaeplin/dashmnb. + """ + padded = b'\x18DarkNet Signed Message:\n' + num_to_varint(len(message)) + from_string_to_bytes(message) + return dbl_sha256(padded) + + + +def extract_pkh_from_locking_script(script): + if len(script) == 25: + if script[0:1] == OP_DUP and script[1:2] == OP_HASH160: + if read_varint(script, 2)[0] == 20: + return script[3:23] + else: + raise Exception('Non-standard public key hash length (should be 20)') + + elif len(script) == 35: + scriptlen = read_varint(script, 0)[0] + if scriptlen in [32, 33]: + return bin_hash160(script[1:1 + scriptlen]) + else: + raise Exception('Non-standard public key length (should be 32 or 33)') + raise Exception('Non-standard locking script type (should be P2PKH or P2PK). len is %d' % len(script)) + + + +def from_string_to_bytes(a): + return a if isinstance(a, bytes) else bytes(a, 'utf-8') + + + +def ipmap(ip, port): + try: + ipv6map = '' + + if len(ip) > 6 and ip.endswith('.onion'): + pchOnionCat = bytearray([0xFD,0x87,0xD8,0x7E,0xEB,0x43]) + vchAddr = base64.b32decode(ip[0:-6], True) + if len(vchAddr) != 16-len(pchOnionCat): + raise Exception('Invalid onion %s' % str(ip)) + return pchOnionCat.hex() + vchAddr.hex() + int(port).to_bytes(2, byteorder='big').hex() + + ipAddr = ip_address(ip) + + if ipAddr.version == 4: + ipv6map = '00000000000000000000ffff' + ip_digits = map(int, ipAddr.exploded.split('.')) + for i in ip_digits: + ipv6map += i.to_bytes(1, byteorder='big')[::-1].hex() + + elif ipAddr.version == 6: + ip_hextets = map(str, ipAddr.exploded.split(':')) + for a in ip_hextets: + ipv6map += a + + else: + raise Exception("invalid version number (%d)" % ipAddr.version) + + + ipv6map += int(port).to_bytes(2, byteorder='big').hex() + if len(ipv6map) != 36: + raise Exception("Problems! len is %d" % len(ipv6map)) + return ipv6map + + except Exception as e: + err_msg = "error in ipmap" + printException(getCallerName(), getFunctionName(), err_msg, e.args) + + + +def num_to_varint(a): + """ + Based on project: https://github.com/chaeplin/dashmnb + """ + x = int(a) + if x < 253: + return x.to_bytes(1, byteorder='big') + elif x < 65536: + return int(253).to_bytes(1, byteorder='big') + x.to_bytes(2, byteorder='little') + elif x < 4294967296: + return int(254).to_bytes(1, byteorder='big') + x.to_bytes(4, byteorder='little') + else: + return int(255).to_bytes(1, byteorder='big') + x.to_bytes(8, byteorder='little') + + + +def read_varint(buffer, offset): + if (buffer[offset] < 0xfd): + value_size = 1 + value = buffer[offset] + elif (buffer[offset] == 0xfd): + value_size = 3 + value = int.from_bytes(buffer[offset + 1: offset + 3], byteorder='little') + elif (buffer[offset] == 0xfe): + value_size = 5 + value = int.from_bytes(buffer[offset + 1: offset + 5], byteorder='little') + elif (buffer[offset] == 0xff): + value_size = 9 + value = int.from_bytes(buffer[offset + 1: offset + 9], byteorder='little') + else: + raise Exception("Invalid varint size") + return value, value_size + + + +def serialize_input_str(tx, prevout_n, sequence, script_sig): + """ + Based on project: https://github.com/chaeplin/dashmnb. + """ + s = ['CTxIn('] + s.append('COutPoint(%s, %s)' % (tx, prevout_n)) + s.append(', ') + if tx == '00' * 32 and prevout_n == 0xffffffff: + s.append('coinbase %s' % script_sig) + + else: + script_sig2 = script_sig + if len(script_sig2) > 24: + script_sig2 = script_sig2[0:24] + s.append('scriptSig=%s' % script_sig2) + + if sequence != 0xffffffff: + s.append(', nSequence=%d' % sequence) + + s.append(')') + return ''.join(s) diff --git a/src/version.txt b/src/version.txt index a678c3b..f2055d6 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1,5 +1,5 @@ -{ - "number": "0.0.1", - "tag": "f", - "comments": ["alpha release"] - } \ No newline at end of file +{ + "number": "0.0.2", + "tag": "a", + "comments": ["beta release"] + } diff --git a/src/watchdogThreads.py b/src/watchdogThreads.py index 05f76e0..7d64564 100644 --- a/src/watchdogThreads.py +++ b/src/watchdogThreads.py @@ -1,33 +1,37 @@ -from time import sleep -from PyQt5.Qt import QApplication, QObject -from misc import printOK -from threading import Event - -class CtrlObject(object): - pass - -class RpcWatchdog(QObject): - def __init__(self, control_tab, timer_off=1, timer_on=3, *args, **kwargs): - QObject.__init__(self, *args, **kwargs) - self.shutdown_flag = Event() - self.control_tab = control_tab - self.timer_off = timer_off #delay when not connected - self.timer_on = timer_on #delay when connected - self.ctrl_obj = CtrlObject() - self.ctrl_obj.finish = False - - - def run(self): - while not self.shutdown_flag.is_set(): - self.control_tab.updateRPCstatus(self.ctrl_obj) - QApplication.processEvents() - self.control_tab.updateRPCled() - - if not self.control_tab.rpcConnected: - sleep(self.timer_off) - else: - sleep(self.timer_on) - - printOK("Exiting Rpc Watchdog Thread") - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from time import sleep +from threading import Event + +from PyQt5.Qt import QObject + + +class CtrlObject(object): + pass + +class RpcWatchdog(QObject): + def __init__(self, control_tab, timer_off=10, timer_on=120, *args, **kwargs): + QObject.__init__(self, *args, **kwargs) + self.firstLoop = True + self.shutdown_flag = Event() + self.control_tab = control_tab + self.timer_off = timer_off #delay when not connected + self.timer_on = timer_on #delay when connected + self.ctrl_obj = CtrlObject() + self.ctrl_obj.finish = False + + + + def run(self): + while not self.shutdown_flag.is_set(): + # update status without printing on debug + self.control_tab.updateRPCstatus(self.ctrl_obj, False) + + if not self.control_tab.rpcConnected: + sleep(self.timer_off) + + else: + sleep(self.timer_on) + + printOK("Exiting Rpc Watchdog Thread") + diff --git a/src/workerThread.py b/src/workerThread.py index d7a305a..bd4e9c8 100644 --- a/src/workerThread.py +++ b/src/workerThread.py @@ -1,36 +1,38 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" - Based on project: - https://github.com/Bertrand256/dash-masternode-tool -""" -from PyQt5.QtCore import QThread - -class CtrlObject(object): - pass - -class WorkerThread(QThread): - """ - Helper class for running function inside a thread. - """ - - def __init__(self, worker_fun, worker_fun_args): - QThread.__init__(self) - self.worker_fun = worker_fun - self.worker_fun_args = worker_fun_args - # prepare control object passed to external thread function - self.ctrl_obj = CtrlObject() - self.ctrl_obj.finish = False - self.worker_result = None - self.worker_exception = None - - def stop(self): - """ - Sets information in control object that thread should finish its work as soon as possible. - Finish attribute should be checked by a thread periodically. - """ - self.ctrl_obj.finish = True - - def run(self): - self.worker_result = self.worker_fun(self.ctrl_obj, *self.worker_fun_args) - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" + Based on project: + https://github.com/Bertrand256/dash-masternode-tool +""" +from PyQt5.QtCore import QThread + +class CtrlObject(object): + pass + +class WorkerThread(QThread): + """ + Helper class for running function inside a thread. + """ + + def __init__(self, worker_fun, worker_fun_args): + QThread.__init__(self) + self.worker_fun = worker_fun + self.worker_fun_args = worker_fun_args + # prepare control object passed to external thread function + self.ctrl_obj = CtrlObject() + self.ctrl_obj.finish = False + self.worker_result = None + self.worker_exception = None + + def stop(self): + """ + Sets information in control object that thread should finish its work as soon as possible. + Finish attribute should be checked by a thread periodically. + """ + self.ctrl_obj.finish = True + + def run(self): + try: + self.worker_result = self.worker_fun(self.ctrl_obj, *self.worker_fun_args) + except Exception as e: + print(e)