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: %sPayment to:
%s
Net amount:
%s PIV
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
Amount: %s PIV
' % str(amount)
+ message += 'Fees: %s PIV
Size: %d Bytes
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)