diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 939734a..c697947 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -17,7 +17,7 @@ jobs: - name: Initialize Python uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.10.6' - name: Install Dependencies run: | @@ -26,9 +26,9 @@ jobs: pip install setuptools==53.0.0 pip install -r requirements.txt pip install importlib-metadata==4.13.0 - pip install flake8==3.8.4 - pip install mypy==0.781 - pip install vulture==2.3 + pip install flake8==4.0.0 + pip install mypy==0.920 + pip install vulture==2.4 - name: Lint run: | @@ -66,7 +66,7 @@ jobs: - name: Setup Python 3.10 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.10.6' - name: Setup pip cache uses: actions/cache@v4 with: diff --git a/requirements.txt b/requirements.txt index 340430b..6c1baca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,5 @@ PyQt5>=5.15.10 requests==2.32.2 simplejson==3.19.2 ecdsa==0.19.0 +types-requests>=2.28.11,<3.0 +types-simplejson>=3.18.0,<4.0 diff --git a/src/apiClient.py b/src/apiClient.py index 2205db8..a54e903 100644 --- a/src/apiClient.py +++ b/src/apiClient.py @@ -31,9 +31,10 @@ def process_api_exceptions_int(*args, **kwargs): class ApiClient: - def __init__(self, isTestnet=False): - self.isTestnet = isTestnet - self.api = BlockBookClient(isTestnet) + def __init__(self, main_wnd): + self.main_wnd = main_wnd + self.isTestnet = main_wnd.isTestnetRPC + self.api = BlockBookClient(main_wnd, self.isTestnet) @process_api_exceptions def getAddressUtxos(self, address): diff --git a/src/blockbookClient.py b/src/blockbookClient.py index 4348542..d6d55b7 100644 --- a/src/blockbookClient.py +++ b/src/blockbookClient.py @@ -15,11 +15,8 @@ def process_blockbook_exceptions_int(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: - if client.isTestnet: - new_url = "https://testnet.fuzzbawls.pw" - else: - new_url = "https://zkbitcoin.com/" - message = "BlockBook Client exception on %s\nTrying backup server %s" % (client.url, new_url) + new_url = "https://testnet.fuzzbawls.pw" if client.isTestnet else "https://zkbitcoin.com/" + message = f"BlockBook Client exception on {client.url}\nTrying backup server {new_url}" printException(getCallerName(True), getFunctionName(True), message, str(e)) try: client.url = new_url @@ -33,21 +30,18 @@ def process_blockbook_exceptions_int(*args, **kwargs): class BlockBookClient: - def __init__(self, isTestnet=False): + def __init__(self, main_wnd, isTestnet=False): + self.main_wnd = main_wnd self.isTestnet = isTestnet - if isTestnet: - self.url = "https://testnet.rockdev.org/" - else: - self.url = "https://explorer.rockdev.org/" + self.url = "https://testnet.rockdev.org/" if isTestnet else "https://explorer.rockdev.org/" def checkResponse(self, method, param=""): - url = self.url + "/api/%s" % method - if param != "": - url += "/%s" % param + url = f"{self.url}/api/{method}" + if param: + url += f"/{param}" resp = requests.get(url, data={}, verify=True) if resp.status_code == 200: - data = resp.json() - return data + return resp.json() raise Exception("Invalid response") @process_blockbook_exceptions diff --git a/src/constants.py b/src/constants.py index 222b648..bc38d39 100644 --- a/src/constants.py +++ b/src/constants.py @@ -7,7 +7,7 @@ import os from queue import Queue -wqueue = Queue() # type: Queue[str] +wqueue: Queue[str] = Queue() APPDATA_DIRNAME = ".PET4L-DATA" @@ -21,7 +21,7 @@ TESTNET_MAGIC_BYTE = 139 TESTNET_STAKE_MAGIC_BYTE = 73 DEFAULT_PROTOCOL_VERSION = 70915 -MINIMUM_FEE = 0.0001 # minimum PIV/kB +MINIMUM_FEE = 0.0001 # minimum PIV/kB SECONDS_IN_2_MONTHS = 60 * 24 * 60 * 60 MAX_INPUTS_NO_WARNING = 75 starting_width = 1033 @@ -50,8 +50,8 @@ trusted_RPC_Servers = [ ["https", "lithuania.fuzzbawls.pw:8080", "spmtUser", "WUss6sr8956S5Paex254"], ["https", "latvia.fuzzbawls.pw:8080", "spmtUser", "8X88u7TuefPm7mQaJY52"], - ["https", "charlotte.fuzzbawls.pw:8080", "spmtUser", "ZyD936tm9dvqmMP8A777"]] - + ["https", "charlotte.fuzzbawls.pw:8080", "spmtUser", "ZyD936tm9dvqmMP8A777"] +] HW_devices = [ # (model name, api index) diff --git a/src/cryptoIDClient.py b/src/cryptoIDClient.py index 45fb180..fbfef04 100644 --- a/src/cryptoIDClient.py +++ b/src/cryptoIDClient.py @@ -11,7 +11,6 @@ api_keys = ["b62b40b5091e", "f1d66708a077", "ed85c85c0126", "ccc60d06f737"] - def process_cryptoID_exceptions(func): def process_cryptoID_exceptions_int(*args, **kwargs): try: @@ -20,27 +19,23 @@ def process_cryptoID_exceptions_int(*args, **kwargs): 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_u = { + "txid": u["tx_hash"], + "vout": u["tx_ouput_n"], + "satoshis": u["value"], + "confirmations": u["confirmations"], + "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") @@ -53,24 +48,24 @@ def checkResponse(self, parameters): parameters['key'] = key resp = requests.get(self.url, params=parameters) if resp.status_code == 200: - data = resp.json() - return data + return resp.json() return None @process_cryptoID_exceptions def getAddressUtxos(self, address): - self.parameters = {} - self.parameters['q'] = 'unspent' - self.parameters['active'] = address + self.parameters = { + 'q': 'unspent', + 'active': address + } res = self.checkResponse(self.parameters) if res is None: return None - else: - return UTXOS_cryptoID_to_trezor(res['unspent_outputs']) + 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 + self.parameters = { + 'q': 'getbalance', + 'a': address + } return self.checkResponse(self.parameters) diff --git a/src/hwdevice.py b/src/hwdevice.py index 0b270e3..b93af8e 100644 --- a/src/hwdevice.py +++ b/src/hwdevice.py @@ -14,31 +14,28 @@ from time import sleep from trezorClient import TrezorApi - 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__) + logging.warning(f"{func.__name__}: hwDevice.api is None") 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) def __init__(self, main_wnd, *args, **kwargs): printDbg("HW: Initializing Class...") - QObject.__init__(self, *args, **kwargs) + super().__init__(*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) + printDbg(f"HW: initializing hw device with index {hw_index}") if hw_index >= len(HW_devices): raise Exception("Invalid HW index") @@ -53,7 +50,7 @@ def initDevice(self, hw_index): self.api.initDevice() self.sig1done = self.api.sig1done self.api.sig_disconnected.connect(self.main_wnd.clearHWstatus) - printOK("HW: hw device with index %d initialized" % hw_index) + printOK(f"HW: hw device with index {hw_index} initialized") @check_api_init def clearDevice(self): @@ -68,14 +65,15 @@ def clearDevice(self): @check_api_init def getStatus(self): printDbg("HW: checking device status...") - printOK("Status: %d" % self.api.status) + printOK(f"Status: {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, isTestnet=False): + def prepare_transfer_tx(self, caller, bip32_path, utxos_to_spend, dest_address, tx_fee, isTestnet=False): rewardsArray = [] - mnode = {} - mnode['path'] = bip32_path - mnode['utxos'] = utxos_to_spend + mnode = { + 'path': bip32_path, + 'utxos': utxos_to_spend + } rewardsArray.append(mnode) self.prepare_transfer_tx_bulk(caller, rewardsArray, dest_address, tx_fee, isTestnet) @@ -86,17 +84,17 @@ def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, i @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)) + printOK(f"HW: Scanning for Address n. {spath} on account n. {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) + printOK(f"HW: Scanning for Bip32 path of address: {address}") found = False spath = -1 for i in range(starting_spath, starting_spath + spath_count): - printDbg("HW: checking path... %d'/0/%d" % (account, i)) + printDbg(f"HW: checking path... {account}'/0/{i}") curr_addr = self.api.scanForAddress(account, i, isTestnet) if curr_addr == address: @@ -106,11 +104,11 @@ def scanForBip32(self, account, address, starting_spath=0, spath_count=10, isTes sleep(0.01) - return (found, spath) + 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)) + printOK(f"HW: Scanning for PubKey of address n. {spath} on account n. {account}") return self.api.scanForPubKey(account, spath, isTestnet) @check_api_init diff --git a/src/ledgerClient.py b/src/ledgerClient.py index e0c46bb..ac71283 100644 --- a/src/ledgerClient.py +++ b/src/ledgerClient.py @@ -29,10 +29,10 @@ def process_ledger_exceptions_int(*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)): + if e.sw in (0x6f01, 0x6d00, 0x6700, 0x6faa): e.message = 'Make sure the PIVX app is open on your Ledger device.' e.message += '<br>If there is a program (such as Ledger Bitcoin Wallet) interfering with the USB communication, close it first.' - elif (e.sw == 0x6982): + 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) @@ -63,7 +63,7 @@ class LedgerApi(QObject): 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 + # signal: tx_progress percent - emitted by prepare_transfer_tx_bulk tx_progress = pyqtSignal(int) # signal: sig_progress percent - emitted by signTxSign sig_progress = pyqtSignal(int) @@ -71,8 +71,8 @@ class LedgerApi(QObject): sig_disconnected = pyqtSignal(str) def __init__(self, main_wnd, *args, **kwargs): + super().__init__(*args, **kwargs) self.main_wnd = main_wnd - QObject.__init__(self, *args, **kwargs) self.model = [x[0] for x in HW_devices].index("LEDGER Nano") self.messages = [ 'Device not initialized.', @@ -96,9 +96,9 @@ def initDevice(self): printDbg("Ledger Initialized") self.status = 1 ver = self.chip.getFirmwareVersion() - printOK("Ledger HW device connected [v. %s]" % str(ver.get('version'))) + printOK(f"Ledger HW device connected [v. {ver.get('version')}]") # Check device is unlocked - bip32_path = MPATH + "%d'/0/%d" % (0, 0) + bip32_path = f"{MPATH}{0}'/0/{0}" _ = self.chip.getWalletPublicKey(bip32_path) self.status = 2 self.sig_progress.connect(self.updateSigProgress) @@ -125,8 +125,7 @@ def append_inputs_to_TX(self, utxo, bip32_path): 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['txid'], utxo['vout'])) + raise Exception(f'Incorrect value of outputIndex for UTXO {utxo["txid"]}-{utxo["vout"]}') trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) self.trusted_inputs.append(trusted_input) @@ -136,10 +135,10 @@ def append_inputs_to_TX(self, utxo, bip32_path): 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 = f"Error: The hashes for the public key for the BIP32 path ({bip32_path}), and the UTXO locking script do not match." 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() + text += f"pubkey_hash: {pubkey_hash.hex()}\n" + text += f"pubkey_hash_from_script: {pubkey_hash_from_script.hex()}\n" printDbg(text) self.arg_inputs.append({ @@ -156,18 +155,15 @@ def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, i 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) + # 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]) + 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'] + mnode['path'] = (MPATH_TESTNET if isTestnet else MPATH) + mnode['path'] # Create a TX input with each utxo for utxo in mnode['utxos']: @@ -198,10 +194,10 @@ def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, i self.mBox2 = QMessageBox(caller) self.messageText = "<p>Confirm transaction on your device, with the following details:</p>" - # messageText += "From bip32_path: <b>%s</b><br><br>" % str(bip32_path) - self.messageText += "<p>Payment to:<br><b>%s</b></p>" % dest_address - self.messageText += "<p>Net amount:<br><b>%s</b> PIV</p>" % str(round(self.amount / 1e8, 8)) - self.messageText += "<p>Fees:<br><b>%s</b> PIV<p>" % str(round(int(tx_fee) / 1e8, 8)) + # messageText += f"From bip32_path: <b>{bip32_path}</b><br><br>" + self.messageText += f"<p>Payment to:<br><b>{dest_address}</b></p>" + self.messageText += f"<p>Net amount:<br><b>{round(self.amount / 1e8, 8)}</b> PIV</p>" + self.messageText += f"<p>Fees:<br><b>{round(int(tx_fee) / 1e8, 8)}</b> PIV<p>" messageText = self.messageText + "Signature Progress: 0 %" self.mBox2.setText(messageText) self.mBox2.setIconPixmap(caller.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) @@ -215,23 +211,15 @@ def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, i @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) + curr_path = (MPATH_TESTNET if isTestnet else MPATH) + f"{hwAcc}'/{intExt}/{spath}" + curr_addr = self.chip.getWalletPublicKey(curr_path).get('address')[12:-2] if not isTestnet else pubkey_to_address(compress_public_key(self.chip.getWalletPublicKey(curr_path).get('publicKey')).hex(), 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 + hwpath = f"{account}'/0/{spath}" + curr_path = (MPATH_TESTNET if isTestnet else MPATH) + hwpath with self.lock: nodeData = self.chip.getWalletPublicKey(curr_path) @@ -240,10 +228,7 @@ def scanForPubKey(self, account, spath, isTestnet=False): @process_ledger_exceptions def signMess(self, caller, hwpath, message, isTestnet=False): - if isTestnet: - path = MPATH_TESTNET + hwpath - else: - path = MPATH + hwpath + path = (MPATH_TESTNET if isTestnet else 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') @@ -251,9 +236,11 @@ def signMess(self, caller, hwpath, message, isTestnet=False): # Connection pop-up mBox = QMessageBox(caller) - warningText = "Another application (such as Ledger Wallet app) has probably taken over " - warningText += "the communication with the Ledger device.<br><br>To continue, close that application and " - warningText += "click the <b>Retry</b> button.\nTo cancel, click the <b>Abort</b> button" + warningText = ( + "Another application (such as Ledger Wallet app) has probably taken over " + "the communication with the Ledger device.<br><br>To continue, close that application and " + "click the <b>Retry</b> button.\nTo cancel, click the <b>Abort</b> button" + ) mBox.setText(warningText) mBox.setWindowTitle("WARNING") mBox.setStandardButtons(QMessageBox.Retry | QMessageBox.Abort) @@ -274,8 +261,7 @@ def signMess(self, caller, hwpath, message, isTestnet=False): printOK('Signing Message') self.mBox = QMessageBox(caller.ui) - messageText = "Check display of your hardware device\n\n- message hash:\n\n%s\n\n-path:\t%s\n" % ( - message_sha, path) + messageText = f"Check display of your hardware device\n\n- message hash:\n\n{message_sha}\n\n-path:\t{path}\n" self.mBox.setText(messageText) self.mBox.setIconPixmap(caller.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) self.mBox.setWindowTitle("CHECK YOUR LEDGER") @@ -314,13 +300,13 @@ def signMessageFinish(self): printOK("Message signed") sig1 = work.hex() else: - printDbg('client.signMessageSign() returned invalid response (code 3): ' + self.signature.hex()) + printDbg(f'client.signMessageSign() returned invalid response (code 3): {self.signature.hex()}') sig1 = "None" else: - printDbg('client.signMessageSign() returned invalid response (code 2): ' + self.signature.hex()) + printDbg(f'client.signMessageSign() returned invalid response (code 2): {self.signature.hex()}') sig1 = "None" else: - printDbg('client.signMessageSign() returned invalid response (code 1): ' + self.signature.hex()) + printDbg(f'client.signMessageSign() returned invalid response (code 1): {self.signature.hex()}') sig1 = "None" else: printOK("Signature refused by the user") @@ -377,13 +363,13 @@ def signTxFinish(self): self.mBox2.accept() if self.tx_raw is not None: - # Signal to be catched by FinishSend on TabRewards / dlg_sewwpAll + # Signal to be caught 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: <b style='color:red'>" + str(percent) + " %</b>" + messageText = f"{self.messageText}Signature Progress: <b style='color:red'>{percent} %</b>" self.mBox2.setText(messageText) QApplication.processEvents() diff --git a/src/mainApp.py b/src/mainApp.py index e0b9419..de9c74b 100644 --- a/src/mainApp.py +++ b/src/mainApp.py @@ -17,8 +17,8 @@ from misc import printDbg, initLogs, saveCacheSettings, readCacheSettings, getVersion from mainWindow import MainWindow from constants import user_dir, SECONDS_IN_2_MONTHS -from qt.dlg_configureRPCservers import ConfigureRPCservers_dlg -from qt.dlg_signmessage import SignMessage_dlg +from qt.dlg_configureRPCservers import ConfigureRPCserversDlg +from qt.dlg_signmessage import SignMessageDlg class ServiceExit(Exception): @@ -30,7 +30,7 @@ class ServiceExit(Exception): def service_shutdown(signum, frame): - print('Caught signal %d' % signum) + print(f'Caught signal {signum}') raise ServiceExit @@ -54,7 +54,7 @@ def __init__(self, imgDir, app, start_args): # Get version and title self.version = getVersion() - self.title = 'PET4L - PIVX Emergency Tool For Ledger - v.%s-%s' % (self.version['number'], self.version['tag']) + self.title = f'PET4L - PIVX Emergency Tool For Ledger - v.{self.version["number"]}-{self.version["tag"]}' # Open database self.db = Database(self) @@ -107,7 +107,7 @@ def initUI(self, imgDir): self.show() self.activateWindow() - def closeEvent(self, *args, **kwargs): + def closeEvent(self, event): # 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() @@ -133,16 +133,16 @@ def closeEvent(self, *args, **kwargs): # Adios print("Bye Bye.") - return QMainWindow.closeEvent(self, *args, **kwargs) + return super().closeEvent(event) def onEditRPCServer(self): # Create Dialog - ui = ConfigureRPCservers_dlg(self) + ui = ConfigureRPCserversDlg(self) if ui.exec(): printDbg("Configuring RPC Servers...") def onSignVerifyMessage(self): # Create Dialog - ui = SignMessage_dlg(self.mainWindow) + ui = SignMessageDlg(self.mainWindow) if ui.exec(): printDbg("Sign/Verify message...") diff --git a/src/mainWindow.py b/src/mainWindow.py index 727b793..87ae2fe 100644 --- a/src/mainWindow.py +++ b/src/mainWindow.py @@ -41,9 +41,8 @@ class MainWindow(QWidget): # signal: UTXO list loading percent (emitted by load_utxos_thread in tabRewards) sig_UTXOsLoading = pyqtSignal(int) - def __init__(self, parent, imgDir): - super(QWidget, self).__init__(parent) + super().__init__(parent) self.parent = parent self.imgDir = imgDir self.runInThread = ThreadFuns.runInThread @@ -79,7 +78,7 @@ def __init__(self, parent, imgDir): self.hwdevice = HWdevice(self) # -- init Api Client - self.apiClient = ApiClient(self.isTestnetRPC) + self.apiClient = ApiClient(self) # Pass 'self' as main_wnd reference # -- Create Queue to redirect stdout self.queue = wqueue @@ -257,7 +256,7 @@ def checkVersion(self, ctrl): (remote_version[0] == local_version[0] and remote_version[1] > local_version[1]) or \ (remote_version[0] == local_version[0] and remote_version[1] == local_version[1] and remote_version[2] > local_version[2]): - self.versionMess = '<b style="color:red">New Version Available:</b> %s ' % (self.gitVersion) + self.versionMess = f'<b style="color:red">New Version Available:</b> {self.gitVersion} ' self.versionMess += '(<a href="https://github.com/PIVX-Project/PET4L/releases/">download</a>)' else: self.versionMess = "You have the latest version of PET4L" @@ -265,7 +264,7 @@ def checkVersion(self, ctrl): def updateVersion(self): if self.versionMess is not None: self.versionLabel.setText(self.versionMess) - printOK("Remote version: %s" % str(self.gitVersion)) + printOK(f"Remote version: {self.gitVersion}") def onChangeSelectedHW(self, i): # Clear status @@ -288,14 +287,13 @@ 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) + fileName, _ = QFileDialog.getSaveFileName(self, f"Save Logs to file PET4L_Logs_{timestamp}.txt", "All Files (*);; Text Files (*.txt)", options=options) try: if fileName: - printOK("Saving logs to %s" % fileName) - log_file = open(fileName, 'w+', encoding="utf-8") - log_text = self.consoleArea.toPlainText() - log_file.write(log_text) - log_file.close() + printOK(f"Saving logs to {fileName}") + with open(fileName, 'w+', encoding="utf-8") as log_file: + log_text = self.consoleArea.toPlainText() + log_file.write(log_text) except Exception as e: err_msg = "error writing Log file" @@ -315,14 +313,14 @@ def onToggleConsole(self): def showHWstatus(self): self.updateHWleds() - myPopUp_sb(self, "info", 'PET4L - hw check', "%s" % self.hwStatusMess) + myPopUp_sb(self, "info", 'PET4L - hw check', f"{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", 'PET4L - rpc check', "%s" % self.rpcStatusMess) + myPopUp_sb(self, "info", 'PET4L - rpc check', f"{self.rpcStatusMess}") def updateHWleds(self): if self.hwStatus == 1: @@ -342,7 +340,7 @@ def updateHWstatus(self, ctrl): printDbg(str(e)) pass - printDbg("status:%s - mess: %s" % (self.hwStatus, self.hwStatusMess)) + printDbg(f"status:{self.hwStatus} - mess: {self.hwStatusMess}") def updateLastBlockLabel(self): text = '--' @@ -370,9 +368,9 @@ def updateLastBlockPing(self): 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) + self.header.responseTimeLabel.setText(f"{self.rpcResponseTime:.3f}") + self.header.responseTimeLabel.setStyleSheet(f"color: {color}") + self.header.lastPingIcon.setStyleSheet(f"color: {color}") def updateRPCled(self, fDebug=False): if self.rpcConnected: @@ -403,7 +401,7 @@ def updateRPClist(self): # Add public servers (italics) italicsFont = QFont("Times", italic=True) for s in public_servers: - url = s["protocol"] + "://" + s["host"].split(':')[0] + url = f"{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) @@ -413,7 +411,7 @@ def updateRPClist(self): 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] + url = f"{s['protocol']}://{s['host'].split(':')[0]}" self.header.rpcClientsBox.addItem(url, s) # reset index if self.parent.cache['selectedRPC_index'] >= self.header.rpcClientsBox.count(): @@ -428,7 +426,7 @@ def updateRPClist(self): def updateRPCstatus(self, ctrl, fDebug=False): 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)) + printDbg(f"Trying to connect to RPC {rpc_protocol}://{rpc_host}...") try: rpcClient = RpcClient(rpc_protocol, rpc_host, rpc_user, rpc_password) diff --git a/src/misc.py b/src/misc.py index 6e377ec..9fc962a 100644 --- a/src/misc.py +++ b/src/misc.py @@ -8,24 +8,26 @@ import os import sys import time +import io from contextlib import redirect_stdout from ipaddress import ip_address from urllib.parse import urlparse +from typing import Any, Callable, Dict, Optional, Tuple import simplejson as json from PyQt5.QtCore import QObject, pyqtSignal, QSettings from PyQt5.QtWidgets import QMessageBox -from constants import log_File, DefaultCache, wqueue, MAX_INPUTS_NO_WARNING +from constants import log_File, DefaultCache, MAX_INPUTS_NO_WARNING -def add_defaultKeys_to_dict(dictObj, defaultObj): +def add_defaultKeys_to_dict(dictObj: Dict[str, Any], defaultObj: Dict[str, Any]) -> None: for key in defaultObj: if key not in dictObj: dictObj[key] = defaultObj[key] -QT_MESSAGE_TYPE = { +QT_MESSAGE_TYPE: Dict[str, QMessageBox.Icon] = { "info": QMessageBox.Information, "warn": QMessageBox.Warning, "crit": QMessageBox.Critical, @@ -33,71 +35,69 @@ def add_defaultKeys_to_dict(dictObj, defaultObj): } -def checkRPCstring(urlstring): +def checkRPCstring(urlstring: str) -> bool: 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") + if not o.scheme: + raise ValueError("Wrong protocol. Set either http or https.") + if not o.netloc: + raise ValueError("Malformed host network location part.") + if not o.port: + raise ValueError("Wrong IP port number") + if not o.username: + raise ValueError("Malformed username") + if not o.password: + raise ValueError("Malformed password") return True - except Exception as e: error_msg = "Unable to parse URL" printException(getCallerName(), getFunctionName(), error_msg, e) return False -def checkTxInputs(parentWindow, num_of_inputs): +def checkTxInputs(parentWindow: Any, num_of_inputs: int) -> Optional[int]: if num_of_inputs == 0: myPopUp_sb(parentWindow, "warn", 'Transaction NOT sent', "No UTXO to send") return None if num_of_inputs > MAX_INPUTS_NO_WARNING: - warning = "Warning: Trying to spend %d inputs.\nA few minutes could be required " \ - "for the transaction to be prepared and signed.\n\nThe hardware device must remain unlocked " \ - "during the whole time (it's advised to disable the auto-lock feature)\n\n" \ - "Do you wish to proceed?" % num_of_inputs - title = "PET4L - spending more than %d inputs" % MAX_INPUTS_NO_WARNING + warning = (f"Warning: Trying to spend {num_of_inputs} inputs.\n" + "A few minutes could be required for the transaction to be prepared and signed.\n\n" + "The hardware device must remain unlocked during the whole time " + "(it's advised to disable the auto-lock feature)\n\n" + "Do you wish to proceed?") + title = f"PET4L - spending more than {MAX_INPUTS_NO_WARNING} inputs" return myPopUp(parentWindow, "warn", title, warning) return QMessageBox.Yes -def clean_for_html(text): +def clean_for_html(text: Optional[str]) -> str: if text is None: return "" return text.replace("<", "{").replace(">", "}") -def clear_screen(): +def clear_screen() -> None: os.system('clear') -def getCallerName(inDecorator=False): +def getCallerName(inDecorator: bool = False) -> Optional[str]: try: - if inDecorator: - return sys._getframe(3).f_code.co_name - return sys._getframe(2).f_code.co_name + frame = sys._getframe(3 if inDecorator else 2) + return frame.f_code.co_name except Exception: return None -def getFunctionName(inDecorator=False): +def getFunctionName(inDecorator: bool = False) -> Optional[str]: try: - if inDecorator: - return sys._getframe(2).f_code.co_name - return sys._getframe(1).f_code.co_name + frame = sys._getframe(2 if inDecorator else 1) + return frame.f_code.co_name except Exception: return None -def getRemotePET4Lversion(): +def getRemotePET4Lversion() -> str: import requests try: resp = requests.get("https://raw.githubusercontent.com/PIVX-Project/PET4L/master/src/version.txt") @@ -105,78 +105,65 @@ def getRemotePET4Lversion(): data = resp.json() return data['number'] else: - raise Exception - + raise ValueError("Invalid response from GitHub") except Exception: redirect_print("Invalid response getting version from GitHub\n") return "0.0.0" -def getVersion(): - version_file = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'version.txt') +def getVersion() -> Dict[str, Any]: + version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'version.txt') with open(version_file, encoding="utf-8") as data_file: data = json.load(data_file) - return data -def getTxidTxidn(txid, txidn): +def getTxidTxidn(txid: Optional[str], txidn: Optional[int]) -> Optional[str]: if txid is None or txidn is None: return None else: - return txid + '-' + str(txidn) + return f"{txid}-{txidn}" -def initLogs(): +def initLogs() -> None: 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 - ) + logging.basicConfig(filename=filename, filemode=filemode, format=format, level=level) -def ipport(ip, port): +def ipport(ip: Optional[str], port: Optional[str]) -> Optional[str]: if ip is None or port is None: return None elif ip.endswith('.onion'): - return ip + ':' + port + return f"{ip}:{port}" else: ipAddr = ip_address(ip) if ipAddr.version == 4: - return ip + ':' + port + return f"{ip}:{port}" elif ipAddr.version == 6: - return "[" + ip + "]:" + port + return f"[{ip}]:{port}" else: - raise Exception("invalid IP version number") + raise ValueError("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) +def myPopUp(parentWindow: Any, messType: str, messTitle: str, messText: str, defaultButton: QMessageBox.StandardButton = QMessageBox.No) -> int: + message_type = QT_MESSAGE_TYPE.get(messType, QMessageBox.Question) + mess = QMessageBox(message_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) +def myPopUp_sb(parentWindow: Any, messType: str, messTitle: str, messText: str, singleButton: QMessageBox.StandardButton = QMessageBox.Ok) -> int: + message_type = QT_MESSAGE_TYPE.get(messType, QMessageBox.Information) + mess = QMessageBox(message_type, messTitle, messText, singleButton, parent=parentWindow) + mess.setStandardButtons(singleButton) return mess.exec_() -def is_hex(s): +def is_hex(s: str) -> bool: try: int(s, 16) return True @@ -184,122 +171,100 @@ def is_hex(s): return False -def now(): +def now() -> int: return int(time.time()) -def persistCacheSetting(cache_key, cache_value): +def persistCacheSetting(cache_key: str, cache_value: Any) -> Any: settings = QSettings('PIVX', 'PET4L') if not settings.contains(cache_key): - printDbg("Cache key %s not found" % str(cache_key)) + printDbg(f"Cache key {cache_key} not found") 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) - + settings.setValue(cache_key, json.dumps(cache_value) if isinstance(cache_value, (list, dict)) else cache_value) return cache_value -def printDbg(what): +def printDbg(what: str) -> None: logging.info(what) log_line = printDbg_msg(what) redirect_print(log_line) -def printDbg_msg(what): +def printDbg_msg(what: str) -> str: what = clean_for_html(what) timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(now())) - log_line = '<b style="color: yellow">{}</b> : {}<br>'.format(timestamp, what) + log_line = f'<b style="color: yellow">{timestamp}</b> : {what}<br>' return log_line -def printError( - caller_name, - function_name, - what -): - logging.error("%s | %s | %s" % (caller_name, function_name, what)) +def printError(caller_name: Optional[str], function_name: Optional[str], what: str) -> None: + logging.error(f"{caller_name} | {function_name} | {what}") log_line = printException_msg(caller_name, function_name, what, None, True) redirect_print(log_line) -def printException( - caller_name, - function_name, - err_msg, - errargs=None -): +def printException(caller_name: Optional[str], function_name: Optional[str], err_msg: str, errargs: Optional[Any] = None) -> None: what = err_msg if errargs is not None: - what += " ==> %s" % str(errargs) - logging.warning("%s | %s | %s" % (caller_name, function_name, what)) + what += f" ==> {errargs}" + logging.warning(f"{caller_name} | {function_name} | {what}") text = printException_msg(caller_name, function_name, err_msg, errargs) redirect_print(text) -def printException_msg( - caller_name, - function_name, - err_msg, - errargs=None, - is_error=False -): - if is_error: - msg = '<b style="color: red">ERROR</b><br>' - else: - msg = '<b style="color: red">EXCEPTION</b><br>' - msg += '<span style="color:white">caller</span> : %s<br>' % caller_name - msg += '<span style="color:white">function</span> : %s<br>' % function_name +def printException_msg(caller_name: Optional[str], function_name: Optional[str], err_msg: str, errargs: Optional[Any] = None, is_error: bool = False) -> str: + msg = '<b style="color: red">ERROR</b><br>' if is_error else '<b style="color: red">EXCEPTION</b><br>' + msg += f'<span style="color:white">caller</span> : {caller_name}<br>' + msg += f'<span style="color:white">function</span> : {function_name}<br>' msg += '<span style="color:red">' if errargs: - msg += 'err: %s<br>' % str(errargs) - - msg += '===> %s</span><br>' % err_msg + msg += f'err: {errargs}<br>' + msg += f'===> {err_msg}</span><br>' return msg -def printOK(what): +def printOK(what: str) -> None: logging.debug(what) - msg = '<b style="color: #cc33ff">===> ' + what + '</b><br>' + msg = f'<b style="color: #cc33ff">===> {what}</b><br>' redirect_print(msg) -def splitString(text, n): +def splitString(text: str, n: int) -> str: arr = [text[i:i + n] for i in range(0, len(text), n)] return '\n'.join(arr) -def readCacheSettings(): +def readCacheSettings() -> Dict[str, Any]: 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["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) - cache["hwAcc"] = settings.value('cache_hwAcc', DefaultCache["hwAcc"], type=int) - cache["spathFrom"] = settings.value('cache_spathFrom', DefaultCache["spathFrom"], type=int) - cache["spathTo"] = settings.value('cache_spathTo', DefaultCache["spathTo"], type=int) - cache["intExt"] = settings.value('cache_intExt', DefaultCache["intExt"], type=int) + cache = { + "lastAddress": settings.value('cache_lastAddress', DefaultCache["lastAddress"], type=str), + "window_width": settings.value('cache_winWidth', DefaultCache["window_width"], type=int), + "window_height": settings.value('cache_winHeight', DefaultCache["window_height"], type=int), + "splitter_x": settings.value('cache_splitterX', DefaultCache["splitter_x"], type=int), + "splitter_y": settings.value('cache_splitterY', DefaultCache["splitter_y"], type=int), + "console_hidden": settings.value('cache_consoleHidden', DefaultCache["console_hidden"], type=bool), + "selectedHW_index": settings.value('cache_HWindex', DefaultCache["selectedHW_index"], type=int), + "selectedRPC_index": settings.value('cache_RPCindex', DefaultCache["selectedRPC_index"], type=int), + "isTestnetRPC": settings.value('cache_isTestnetRPC', DefaultCache["isTestnetRPC"], type=bool), + "hwAcc": settings.value('cache_hwAcc', DefaultCache["hwAcc"], type=int), + "spathFrom": settings.value('cache_spathFrom', DefaultCache["spathFrom"], type=int), + "spathTo": settings.value('cache_spathTo', DefaultCache["spathTo"], type=int), + "intExt": settings.value('cache_intExt', DefaultCache["intExt"], type=int) + } add_defaultKeys_to_dict(cache, DefaultCache) return cache - except: + except Exception: return DefaultCache -def redirect_print(what): - with redirect_stdout(WriteStream(wqueue)): - print(what) +def redirect_print(what: str) -> None: + with io.StringIO() as buffer: + with redirect_stdout(buffer): + print(what) -def saveCacheSettings(cache): +def saveCacheSettings(cache: Dict[str, Any]) -> None: settings = QSettings('PIVX', 'PET4L') settings.setValue('cache_lastAddress', cache.get('lastAddress')) settings.setValue('cache_winWidth', cache.get('window_width')) @@ -316,28 +281,25 @@ def saveCacheSettings(cache): settings.setValue('cache_intExt', cache.get('intExt')) -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 sec_to_time(seconds: int) -> str: + days, seconds = divmod(seconds, 86400) + hrs, seconds = divmod(seconds, 3600) + mins, seconds = divmod(seconds, 60) + return f"{days} days, {hrs} hrs, {mins} mins, {seconds} secs" -def timeThis(function, *args): +def timeThis(function: Callable[..., Any], *args: Any) -> Tuple[Optional[Any], Optional[float]]: try: - start = time.clock() + start = time.perf_counter() val = function(*args) - end = time.clock() + end = time.perf_counter() return val, (end - start) except Exception: return None, None class DisconnectedException(Exception): - def __init__(self, message, hwDevice): + def __init__(self, message: str, hwDevice: Any): # Call the base class constructor super().__init__(message) # clear device @@ -345,27 +307,39 @@ def __init__(self, message, hwDevice): # Stream object to redirect sys.stdout and sys.stderr to a queue -class WriteStream(object): - def __init__(self, queue): +class WriteStream: + def __init__(self, queue: Any): self.queue = queue - def write(self, text): + def write(self, text: str) -> None: self.queue.put(text) - def flush(self): + def flush(self) -> None: pass +class WrapperWriteStream: + def __init__(self, write_stream: WriteStream): + self.write_stream = write_stream + + def write(self, message: str) -> int: + self.write_stream.write(message) + return len(message) + + def flush(self) -> None: + self.write_stream.flush() + + # 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) + def __init__(self, queue: Any, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) self.queue = queue - def run(self): + def run(self) -> None: while True: text = self.queue.get() self.mysignal.emit(text) diff --git a/src/pivx_b58.py b/src/pivx_b58.py index 1518dda..093aa23 100644 --- a/src/pivx_b58.py +++ b/src/pivx_b58.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*-import sys +# -*- coding: utf-8 -*- # Copyright (c) 2017-2019 Random.Zebra (https://github.com/random-zebra/) # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. @@ -8,17 +8,16 @@ __b58base = len(__b58chars) b58chars = __b58chars -long = int _bchr = lambda x: bytes([x]) _bord = lambda x: x -def b58encode(v): +def b58encode(v: bytes) -> str: """ encode v, which is a string of bytes, to base58. """ long_value = 0 - for (i, c) in enumerate(v[::-1]): + for i, c in enumerate(v[::-1]): long_value += (256 ** i) * _bord(c) result = '' @@ -40,11 +39,10 @@ def b58encode(v): return (__b58chars[0] * nPad) + result -def b58decode(v, length=None): - """ decode v into a string of len bytes - """ +def b58decode(v: str, length: int = None) -> bytes: + """ decode v into a string of len bytes """ long_value = 0 - for (i, c) in enumerate(v[::-1]): + for i, c in enumerate(v[::-1]): long_value += __b58chars.find(c) * (__b58base ** i) result = bytes() @@ -63,6 +61,6 @@ def b58decode(v, length=None): result = _bchr(0) * nPad + result if length is not None and len(result) != length: - return None + return b'' return result diff --git a/src/pivx_hashlib.py b/src/pivx_hashlib.py index 628d7f2..58164d0 100644 --- a/src/pivx_hashlib.py +++ b/src/pivx_hashlib.py @@ -12,20 +12,20 @@ from pivx_b58 import b58encode, b58decode -def double_sha256(data): +def double_sha256(data: bytes) -> bytes: return hashlib.sha256(hashlib.sha256(data).digest()).digest() -def single_sha256(data): +def single_sha256(data: bytes) -> bytes: return hashlib.sha256(data).digest() -def generate_privkey(isTestnet=False): +def generate_privkey(isTestnet: bool = False) -> str: """ Based on Andreas Antonopolous work from 'Mastering Bitcoin'. """ valid = False - privkey = 0 + privkey = "" while not valid: privkey = bitcoin.random_key() decoded_private_key = bitcoin.decode_privkey(privkey, 'hex') @@ -33,20 +33,20 @@ def generate_privkey(isTestnet=False): return base58fromhex(privkey, isTestnet) -def base58fromhex(hexstr, isTestnet): +def base58fromhex(hexstr: str, isTestnet: bool) -> str: base58_secret = TESTNET_WIF_PREFIX if isTestnet else WIF_PREFIX data = bytes([base58_secret]) + bytes.fromhex(hexstr) checksum = bitcoin.bin_dbl_sha256(data)[0:4] return b58encode(data + checksum) -def pubkey_to_address(pubkey, isTestnet=False, isCold=False): +def pubkey_to_address(pubkey: str, isTestnet: bool = False, isCold: bool = False) -> str: pubkey_bin = bytes.fromhex(pubkey) pkey_hash = bitcoin.bin_hash160(pubkey_bin) return pubkeyhash_to_address(pkey_hash, isTestnet, isCold) -def pubkeyhash_to_address(pkey_hash, isTestnet=False, isCold=False): +def pubkeyhash_to_address(pkey_hash: bytes, isTestnet: bool = False, isCold: bool = False) -> str: if isCold: base58_secret = TESTNET_STAKE_MAGIC_BYTE if isTestnet else STAKE_MAGIC_BYTE else: @@ -56,24 +56,22 @@ def pubkeyhash_to_address(pkey_hash, isTestnet=False, isCold=False): return b58encode(data + checksum) -def wif_to_privkey(string): - wif_compressed = 52 == len(string) +def wif_to_privkey(string: str) -> str | None: + wif_compressed = len(string) == 52 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 (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 + return None diff --git a/src/pivx_parser.py b/src/pivx_parser.py index ce95c88..d970c28 100644 --- a/src/pivx_parser.py +++ b/src/pivx_parser.py @@ -4,17 +4,18 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. +from typing import Dict, Tuple, List, Literal, Any, TypedDict from misc import getCallerName, getFunctionName, printException import utils from pivx_hashlib import pubkeyhash_to_address class HexParser: - def __init__(self, hex_str): + def __init__(self, hex_str: str): self.cursor = 0 self.hex_str = hex_str - def readInt(self, nbytes, byteorder="big", signed=False): + def readInt(self, nbytes: int, byteorder: Literal['little', 'big'] = 'big', signed: bool = False) -> int: 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]) @@ -22,7 +23,7 @@ def readInt(self, nbytes, byteorder="big", signed=False): self.cursor += nbytes * 2 return res - def readVarInt(self): + def readVarInt(self) -> int: r = self.readInt(1) if r == 253: return self.readInt(2, "little") @@ -32,7 +33,7 @@ def readVarInt(self): return self.readInt(8, "little") return r - def readString(self, nbytes, byteorder="big"): + def readString(self, nbytes: int, byteorder: str = "big") -> str: if self.cursor + nbytes * 2 > len(self.hex_str): raise Exception("HexParser range error") res = self.hex_str[self.cursor:self.cursor + nbytes * 2] @@ -43,18 +44,39 @@ def readString(self, nbytes, byteorder="big"): return res -def IsCoinBase(vin): +class VinType(TypedDict, total=False): + txid: str + vout: int + scriptSig: Dict[str, str] + sequence: int + coinbase: str + + +class VoutType(TypedDict): + value: int + scriptPubKey: Dict[str, Any] + + +class TxType(TypedDict): + version: int + vin: List[VinType] + vout: List[VoutType] + locktime: int + + +def IsCoinBase(vin: VinType) -> bool: 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.readVarInt() - vin["scriptSig"] = {} - vin["scriptSig"]["hex"] = p.readString(script_len, "big") - vin["sequence"] = p.readInt(4, "little") +def ParseTxInput(p: HexParser) -> VinType: + vin: VinType = { + "txid": p.readString(32, "little"), + "vout": p.readInt(4, "little"), + "scriptSig": { + "hex": p.readString(p.readVarInt(), "big") + }, + "sequence": p.readInt(4, "little") + } if IsCoinBase(vin): del vin["txid"] del vin["vout"] @@ -64,16 +86,16 @@ def ParseTxInput(p): return vin -def ParseTxOutput(p, isTestnet=False): - vout = {} - vout["value"] = p.readInt(8, "little") - script_len = p.readVarInt() - vout["scriptPubKey"] = {} - vout["scriptPubKey"]["hex"] = p.readString(script_len, "big") - vout["scriptPubKey"]["addresses"] = [] +def ParseTxOutput(p: HexParser, isTestnet: bool = False) -> VoutType: + vout: VoutType = { + "value": p.readInt(8, "little"), + "scriptPubKey": { + "hex": p.readString(p.readVarInt(), "big"), + "addresses": [] + } + } try: locking_script = bytes.fromhex(vout["scriptPubKey"]["hex"]) - # add addresses only if P2PKH, P2PK or P2CS if len(locking_script) in [25, 35, 51]: add_bytes = utils.extract_pkh_from_locking_script(locking_script) address = pubkeyhash_to_address(add_bytes, isTestnet) @@ -83,37 +105,28 @@ def ParseTxOutput(p, isTestnet=False): return vout -def ParseTx(hex_string, isTestnet=False): +def ParseTx(hex_string: str, isTestnet: bool = False) -> TxType: p = HexParser(hex_string) - tx = {} - - tx["version"] = p.readInt(4, "little") - - num_of_inputs = p.readVarInt() - tx["vin"] = [] - for i in range(num_of_inputs): - tx["vin"].append(ParseTxInput(p)) - - num_of_outputs = p.readVarInt() - tx["vout"] = [] - for i in range(num_of_outputs): - tx["vout"].append(ParseTxOutput(p, isTestnet)) - - tx["locktime"] = p.readInt(4, "little") + tx: TxType = { + "version": p.readInt(4, "little"), + "vin": [ParseTxInput(p) for _ in range(p.readVarInt())], + "vout": [ParseTxOutput(p, isTestnet) for _ in range(p.readVarInt())], + "locktime": p.readInt(4, "little") + } return tx -def IsPayToColdStaking(rawtx, out_n): +def IsPayToColdStaking(rawtx: str, out_n: int) -> Tuple[bool, bool]: tx = ParseTx(rawtx) script = tx['vout'][out_n]["scriptPubKey"]["hex"] return utils.IsPayToColdStaking(bytes.fromhex(script)), IsCoinStake(tx) -def IsCoinStake(json_tx): +def IsCoinStake(json_tx: TxType) -> bool: return json_tx['vout'][0]["scriptPubKey"]["hex"] == "" -def GetDelegatedStaker(rawtx, out_n, isTestnet): +def GetDelegatedStaker(rawtx: str, out_n: int, isTestnet: bool) -> str: tx = ParseTx(rawtx) script = tx['vout'][out_n]["scriptPubKey"]["hex"] if not utils.IsPayToColdStaking(bytes.fromhex(script)): diff --git a/src/qt/dlg_configureRPCservers.py b/src/qt/dlg_configureRPCservers.py index 94e7ede..81a76eb 100644 --- a/src/qt/dlg_configureRPCservers.py +++ b/src/qt/dlg_configureRPCservers.py @@ -4,17 +4,17 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. -from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QLabel, \ - QListWidget, QFrame, QFormLayout, QComboBox, QLineEdit, QListWidgetItem, \ - QWidget, QPushButton, QMessageBox - +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): +class ConfigureRPCserversDlg(QDialog): def __init__(self, main_wnd): - QDialog.__init__(self, parent=main_wnd) + super().__init__(parent=main_wnd) self.main_wnd = main_wnd self.setWindowTitle('RPC Servers Configuration') self.changing_index = None @@ -29,49 +29,48 @@ def clearEditFrame(self): self.ui.host_edt.clear() def initUI(self): - self.ui = Ui_ConfigureRPCserversDlg() + self.ui = UiConfigureRPCserversDlg() self.ui.setupUi(self) def insert_server_list(self, server): - id = server['id'] + 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 + " <b>Local Wallet</b>" + server_text = f"{server['protocol']}://{server['host']}" + if server_id == 0 and server['isCustom']: + server_text += " <b>Local Wallet</b>" elif not server['isCustom']: - server_text = "<em style='color: purple'>%s</em>" % server_text + server_text = f"<em style='color: purple'>{server_text}</em>" 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") + + edit_btn = QPushButton() + edit_btn.setIcon(self.main_wnd.mainWindow.editMN_icon) + edit_btn.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') + edit_btn.setDisabled(True) + edit_btn.setToolTip('Default servers are not editable') + edit_btn.clicked.connect(lambda: self.onAddServer(index)) + server_row.addWidget(edit_btn) + + remove_btn = QPushButton() + remove_btn.setIcon(self.main_wnd.mainWindow.removeMN_icon) + remove_btn.setToolTip("Remove server configuration") + if server_id == 0: + remove_btn.setDisabled(True) + remove_btn.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) - # -- + remove_btn.setDisabled(True) + remove_btn.setToolTip('Cannot remove default servers') + remove_btn.clicked.connect(lambda: self.onRemoveServer(index)) + server_row.addWidget(remove_btn) + 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) + self.serverItems[server_id] = QListWidgetItem() + self.serverItems[server_id].setSizeHint(server_line.sizeHint()) + self.ui.serversBox.addItem(self.serverItems[server_id]) + self.ui.serversBox.setItemWidget(self.serverItems[server_id], server_line) def loadServers(self): # Clear serversBox @@ -85,10 +84,7 @@ 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.protocol_select.setCurrentIndex(1 if server['protocol'] == 'https' else 0) self.ui.host_edt.setText(server['host']) def onAddServer(self, index=None): @@ -122,13 +118,11 @@ def onClose(self): 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')) + mess = f"Are you sure you want to remove server with index {index} ({self.main_wnd.mainWindow.rpcServersList[index].get('host')}) from list?" ans = myPopUp(self, QMessageBox.Question, 'PET4L - 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) + server_id = self.main_wnd.mainWindow.rpcServersList[index].get('id') + self.main_wnd.db.removeRPCServer(server_id) def onSave(self): # Get new config data @@ -136,27 +130,24 @@ def onSave(self): 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) + url_string = f"{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 + server_id = self.main_wnd.mainWindow.rpcServersList[self.changing_index].get('id') + self.main_wnd.db.editRPCServer(protocol, host, user, passwd, server_id) 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,), ) + if data.get('id') == server_id and data.get('isCustom'): + ThreadFuns.runInThread(self.main_wnd.mainWindow.updateRPCstatus, (True,)) # call onCancel self.onCancel() -class Ui_ConfigureRPCserversDlg(object): +class UiConfigureRPCserversDlg: def setupUi(self, ConfigureRPCserversDlg): ConfigureRPCserversDlg.setModal(True) # -- Layout diff --git a/src/qt/dlg_pinMatrix.py b/src/qt/dlg_pinMatrix.py index fbb9656..c5670b4 100644 --- a/src/qt/dlg_pinMatrix.py +++ b/src/qt/dlg_pinMatrix.py @@ -10,10 +10,10 @@ from misc import myPopUp_sb -class PinMatrix_dlg(QDialog): +class PinMatrixDlg(QDialog): def __init__(self, text='', title='Enter PIN', fHideBtns=True): - QDialog.__init__(self) - self.text = text if text != '' else "Check device and enter PIN" + super().__init__() + self.text = text if text else "Check device and enter PIN" self.hideBtns = fHideBtns self.pin = '' self.setWindowTitle(title) @@ -46,7 +46,7 @@ def onOK(self): myPopUp_sb(self, "warn", 'Wrong PIN!', text) def setupUI(self): - Ui_pinMatrixDlg.setupUi(self, self) + UiPinMatrixDlg.setupUi(self, self) # Connect buttons matrix for i in range(9): self.btn[i].clicked.connect(lambda _, b=i + 1: self.btn_clicked(str(b))) @@ -56,14 +56,14 @@ def setupUI(self): self.btn_cancel.clicked.connect(self.onCancel) -class Ui_pinMatrixDlg(object): +class UiPinMatrixDlg: def setupUi(self, PinMatrixDlg): PinMatrixDlg.setModal(True) layout = QVBoxLayout(PinMatrixDlg) layout.setContentsMargins(10, 8, 10, 10) # Header - title = QLabel("<b>%s</b>" % PinMatrixDlg.text) + title = QLabel(f"<b>{PinMatrixDlg.text}</b>") title.setAlignment(Qt.AlignCenter) layout.addWidget(title) diff --git a/src/qt/dlg_signmessage.py b/src/qt/dlg_signmessage.py index 2a9b55d..8dbf7bb 100644 --- a/src/qt/dlg_signmessage.py +++ b/src/qt/dlg_signmessage.py @@ -32,18 +32,18 @@ from utils import checkPivxAddr, ecdsa_verify_addr -class SignMessage_dlg(QDialog): +class SignMessageDlg(QDialog): def __init__(self, main_wnd): - QDialog.__init__(self, parent=main_wnd) + super().__init__(parent=main_wnd) self.setWindowTitle('Sign/Verify Message') self.initUI(main_wnd) def initUI(self, main_wnd): - self.ui = Ui_SignMessageDlg() + self.ui = UiSignMessageDlg() self.ui.setupUi(self, main_wnd) -class Ui_SignMessageDlg(object): +class UiSignMessageDlg: def setupUi(self, SignMessageDlg, main_wnd): SignMessageDlg.setModal(True) SignMessageDlg.setMinimumWidth(600) @@ -61,7 +61,7 @@ def setupUi(self, SignMessageDlg, main_wnd): class TabSign: def __init__(self, main_wnd): self.main_wnd = main_wnd - self.ui = TabSign_gui() + self.ui = TabSignGui() self.loadAddressComboBox() # connect signals/buttons self.ui.addressComboBox.currentIndexChanged.connect(lambda: self.onChangeSelectedAddress()) @@ -102,9 +102,9 @@ def findPubKey(self): pk = device.scanForPubKey(self.hwAcc, self.spath, self.currIsTestnet) if pk is None: - mess = "Unable to find public key. The action was refused on the device or another application " - mess += "might have taken over the USB communication with the device.<br><br>" - mess += "The operation was canceled." + mess = ("Unable to find public key. The action was refused on the device or another application " + "might have taken over the USB communication with the device.<br><br>" + "The operation was canceled.") myPopUp_sb(self.main_wnd, QMessageBox.Critical, 'PET4L - PK not found', mess) return self.updateGenericAddress(pk) @@ -124,10 +124,9 @@ def findSpath_done(self): addy = self.ui.addressLineEdit.text().strip() starting_spath = self.curr_starting_spath spath_count = self.curr_spath_count - mess = "Scanned addresses <b>%d</b> to <b>%d</b> of HW account <b>%d</b>.<br>" % ( - starting_spath, starting_spath + spath_count - 1, self.hwAcc) - mess += "Unable to find the address <i>%s</i>.<br>Maybe it's on a different account.<br><br>" % addy - mess += "Do you want to scan %d more addresses of account n.<b>%d</b> ?" % (spath_count, self.hwAcc) + mess = (f"Scanned addresses <b>{starting_spath}</b> to <b>{starting_spath + spath_count - 1}</b> of HW account <b>{self.hwAcc}</b>.<br>" + f"Unable to find the address <i>{addy}</i>.<br>Maybe it's on a different account.<br><br>" + f"Do you want to scan {spath_count} more addresses of account n.<b>{self.hwAcc}</b> ?") ans = myPopUp(self.main_wnd, QMessageBox.Question, 'PET4L - spath search', mess) if ans == QMessageBox.Yes: # Look for 10 more addresses @@ -165,9 +164,8 @@ def onSave(self): "All Files (*);; Text Files (*.txt)", options=options) try: if fileName: - save_file = open(fileName, 'w', encoding="utf-8") - save_file.write(self.ui.signatureTextEdt.toPlainText()) - save_file.close() + with open(fileName, 'w', encoding="utf-8") as save_file: + save_file.write(self.ui.signatureTextEdt.toPlainText()) myPopUp_sb(self.main_wnd, QMessageBox.Information, 'PET4L - saved', "Signature saved to file") return except Exception as e: @@ -188,7 +186,7 @@ def onSearchPK(self): if not checkPivxAddr(addy, self.currIsTestnet): net = "testnet" if self.currIsTestnet else "mainnet" - mess = "PIVX address not valid. Insert valid PIVX %s address" % net + mess = f"PIVX address not valid. Insert valid PIVX {net} address" myPopUp_sb(self.main_wnd, QMessageBox.Warning, 'PET4L - invalid address', mess) return @@ -251,9 +249,9 @@ def setSignEnabled(self, enabled): self.ui.hiddenLine.setVisible(not enabled) tooltip = "" if not enabled: - tooltip = "You need to find the address PK in your hardware device first.\n" \ - "Insert the account number (usually 0) and either a PIVX address\n" \ - "or the spath_id (address number) and click 'Search HW'." + tooltip = ("You need to find the address PK in your hardware device first.\n" + "Insert the account number (usually 0) and either a PIVX address\n" + "or the spath_id (address number) and click 'Search HW'.") self.ui.addressLabel.setText("") self.ui.signBtn.setToolTip(tooltip) @@ -263,14 +261,14 @@ def updateGenericAddress(self, pk): # double check address addy = self.ui.addressLineEdit.text().strip() if addy != genericAddy: - mess = "Error! retrieved address (%s) different from input (%s)" % (genericAddy, addy) + mess = f"Error! retrieved address ({genericAddy}) different from input ({addy})" myPopUp_sb(self.main_wnd, QMessageBox.Critical, 'PET4L - address mismatch', mess) self.ui.addressLabel.setText("") return # update generic address self.setSignEnabled(True) self.currAddress = genericAddy - self.currHwPath = "%d'/0/%d" % (self.hwAcc, self.spath) + self.currHwPath = f"{self.hwAcc}'/0/{self.spath}" self.ui.addressLabel.setText(self.currAddress) self.ui.editBtn.setVisible(True) @@ -278,7 +276,7 @@ def updateGenericAddress(self, pk): class TabVerify: def __init__(self, main_wnd): self.main_wnd = main_wnd - self.ui = TabVerify_gui() + self.ui = TabVerifyGui() # connect signals/buttons self.ui.verifyBtn.clicked.connect(lambda: self.onVerify()) @@ -307,21 +305,21 @@ def onVerify(self): self.ui.signatureTextEdt.toPlainText(), self.ui.addressLineEdit.text().strip()) except Exception as e: - mess = "Error decoding signature:\n" + str(e) + mess = f"Error decoding signature:\n{e}" myPopUp_sb(self.main_wnd, QMessageBox.Warning, 'PET4L - invalid signature', mess) ok = False if ok: mess = "<span style='color: green'>Signature OK" else: mess = "<span style='color: red'>Signature doesn't verify" - mess = "<b>" + mess + "</span></b>" + mess = f"<b>{mess}</span></b>" self.ui.resultLabel.setText(mess) self.ui.resultLabel.setVisible(True) -class TabSign_gui(QWidget): +class TabSignGui(QWidget): def __init__(self): - QWidget.__init__(self) + super().__init__() layout = QVBoxLayout() layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(13) @@ -426,7 +424,7 @@ def __init__(self): self.copyBtn.setVisible(False) row5.addWidget(self.copyBtn) self.saveBtn = QPushButton("Save") - self.saveBtn.setToolTip("Save signature to ca file") + self.saveBtn.setToolTip("Save signature to a file") self.saveBtn.setVisible(False) row5.addWidget(self.saveBtn) layout.addLayout(row5) @@ -434,9 +432,9 @@ def __init__(self): self.setLayout(layout) -class TabVerify_gui(QWidget): +class TabVerifyGui(QWidget): def __init__(self): - QWidget.__init__(self) + super().__init__() layout = QVBoxLayout() layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(13) diff --git a/src/qt/guiHeader.py b/src/qt/guiHeader.py index fdaf92e..75f2b80 100644 --- a/src/qt/guiHeader.py +++ b/src/qt/guiHeader.py @@ -12,7 +12,7 @@ class GuiHeader(QWidget): def __init__(self, caller, *args, **kwargs): - QWidget.__init__(self) + super().__init__(*args, **kwargs) layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) # --- 1) Check Box @@ -28,7 +28,7 @@ def __init__(self, caller, *args, **kwargs): 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.setToolTip(f"{caller.rpcStatusMess}") self.rpcLed.setPixmap(caller.ledGrayH_icon) self.centralBox.addWidget(self.rpcLed, 0, 3) self.lastPingBox = QWidget() @@ -65,7 +65,7 @@ def __init__(self, caller, *args, **kwargs): 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.setToolTip(f"status: {caller.hwStatusMess}") self.hwLed.setPixmap(caller.ledGrayH_icon) self.centralBox.addWidget(self.hwLed, 1, 3) layout.addLayout(self.centralBox) diff --git a/src/qt/gui_tabRewards.py b/src/qt/gui_tabRewards.py index 31c1da5..91cc2d0 100644 --- a/src/qt/gui_tabRewards.py +++ b/src/qt/gui_tabRewards.py @@ -8,16 +8,18 @@ import os.path 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, QProgressBar +from PyQt5.QtWidgets import ( + QWidget, QPushButton, QHBoxLayout, QGroupBox, QVBoxLayout, QLineEdit, QComboBox, + QProgressBar, QLabel, QFormLayout, QDoubleSpinBox, QTableWidget, QTableWidgetItem, + QAbstractItemView, QHeaderView, QSpinBox +) sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) -class TabRewards_gui(QWidget): +class TabRewardsGui(QWidget): def __init__(self, imgDir, *args, **kwargs): - QWidget.__init__(self) + super().__init__(*args, **kwargs) self.imgDir = imgDir self.initRewardsForm() mainVertical = QVBoxLayout() @@ -34,6 +36,7 @@ def initRewardsForm(self): layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(13) layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) + # --- ROW 1 line1 = QHBoxLayout() line1.addWidget(QLabel("Account HW")) @@ -69,6 +72,7 @@ def initRewardsForm(self): self.btn_reload.setToolTip("Reload data from ledger device") line1.addWidget(self.btn_reload) layout.addRow(line1) + # --- ROW 2: address and copy btn hBox = QHBoxLayout() self.addySelect = QComboBox() @@ -79,6 +83,7 @@ def initRewardsForm(self): self.btn_Copy.setToolTip("Copy address to clipboard") hBox.addWidget(self.btn_Copy) layout.addRow(hBox) + # --- ROW 3: UTXOs self.rewardsList = QVBoxLayout() self.rewardsList.statusLabel = QLabel('<b style="color:red">Reload Rewards</b>') @@ -86,7 +91,6 @@ def initRewardsForm(self): self.rewardsList.addWidget(self.rewardsList.statusLabel) self.rewardsList.box = QTableWidget() self.rewardsList.box.setMinimumHeight(230) - # self.rewardsList.box.setMaximumHeight(140) self.rewardsList.box.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.rewardsList.box.setSelectionMode(QAbstractItemView.MultiSelection) self.rewardsList.box.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -95,6 +99,7 @@ def initRewardsForm(self): 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) @@ -111,8 +116,10 @@ def initRewardsForm(self): item.setText("TX Output N") item.setTextAlignment(Qt.AlignCenter) self.rewardsList.box.setHorizontalHeaderItem(3, item) + self.rewardsList.addWidget(self.rewardsList.box) layout.addRow(self.rewardsList) + # --- ROW 3 hBox2 = QHBoxLayout() self.btn_selectAllRewards = QPushButton("Select All") @@ -129,6 +136,7 @@ def initRewardsForm(self): hBox2.addWidget(self.selectedRewardsLine) hBox2.addStretch(1) layout.addRow(hBox2) + # --- ROW 4 hBox3 = QHBoxLayout() self.destinationLine = QLineEdit() @@ -145,6 +153,7 @@ def initRewardsForm(self): self.btn_sendRewards = QPushButton("Send") hBox3.addWidget(self.btn_sendRewards) layout.addRow(QLabel("Destination Address"), hBox3) + hBox4 = QHBoxLayout() hBox4.addStretch(1) self.loadingLine = QLabel("<b style='color:red'>Preparing TX.</b> Completed: ") @@ -157,8 +166,10 @@ def initRewardsForm(self): self.loadingLine.hide() self.loadingLinePercent.hide() layout.addRow(hBox4) + # --- Set Layout self.rewardsForm.setLayout(layout) + # --- ROW 5 self.btn_Cancel = QPushButton("Clear") diff --git a/src/rpcClient.py b/src/rpcClient.py index 64e8db6..11b8456 100644 --- a/src/rpcClient.py +++ b/src/rpcClient.py @@ -9,6 +9,7 @@ import http.client as httplib import ssl import threading +from typing import Union from constants import DEFAULT_PROTOCOL_VERSION, MINIMUM_FEE from misc import getCallerName, getFunctionName, printException, printDbg, now, timeThis @@ -19,7 +20,6 @@ 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)) @@ -35,13 +35,24 @@ def process_RPC_exceptions_int(*args, **kwargs): class RpcClient: - def __init__(self, rpc_protocol, rpc_host, rpc_user, rpc_password): + def __init__(self, rpc_protocol: str, rpc_host: str, rpc_user: str, rpc_password: str): # Lock for threads self.lock = threading.RLock() - self.rpc_url = "%s://%s:%s@%s" % (rpc_protocol, rpc_user, rpc_password, rpc_host) + self.rpc_url = f"{rpc_protocol}://{rpc_user}:{rpc_password}@{rpc_host}" + + if ":" in rpc_host: + host, port_str = rpc_host.split(":") + try: + port = int(port_str) + except ValueError: + raise ValueError(f"Invalid port specified: {port_str}") + else: + host = rpc_host + port = None + + self.httpConnection: Union[httplib.HTTPConnection, httplib.HTTPSConnection] - host, port = rpc_host.split(":") if rpc_protocol == "https": self.httpConnection = httplib.HTTPSConnection(host, port, timeout=20, context=ssl._create_unverified_context()) else: @@ -50,65 +61,47 @@ def __init__(self, rpc_protocol, rpc_host, rpc_user, rpc_password): self.conn = AuthServiceProxy(self.rpc_url, timeout=1000, connection=self.httpConnection) @process_RPC_exceptions - def getBlockCount(self): - n = 0 + def getBlockCount(self) -> int: with self.lock: - n = self.conn.getblockcount() - - return n + return self.conn.getblockcount() @process_RPC_exceptions - def getBlockHash(self, blockNum): - h = None + def getBlockHash(self, blockNum: int) -> str: with self.lock: - h = self.conn.getblockhash(blockNum) - - return h + return self.conn.getblockhash(blockNum) @process_RPC_exceptions - def getBudgetVotes(self, proposal): - votes = {} + def getBudgetVotes(self, proposal: str) -> dict: with self.lock: - votes = self.conn.getbudgetvotes(proposal) - - return votes + return self.conn.getbudgetvotes(proposal) @process_RPC_exceptions - def getFeePerKb(self): - res = MINIMUM_FEE + def getFeePerKb(self) -> float: 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 + return feePerKb if feePerKb > MINIMUM_FEE else MINIMUM_FEE @process_RPC_exceptions - def getMNStatus(self, address): - mnStatus = None + def getMNStatus(self, address: str) -> dict: with self.lock: mnStatusList = self.conn.listmasternodes(address) if not mnStatusList: - return None + return {} mnStatus = mnStatusList[0] mnStatus['mnCount'] = self.conn.getmasternodecount()['enabled'] - - return mnStatus + return mnStatus @process_RPC_exceptions - def getMasternodeCount(self): - ans = None + def getMasternodeCount(self) -> dict: with self.lock: - ans = self.conn.getmasternodecount() - - return ans + return self.conn.getmasternodecount() @process_RPC_exceptions - def getMasternodes(self): + def getMasternodes(self) -> dict: printDbg("RPC: Getting masternode list...") mnList = {} score = [] - masternodes = [] with self.lock: masternodes = self.conn.listmasternodes() @@ -120,7 +113,6 @@ def getMasternodes(self): else: lastpaid_ago = now() - mn.get('lastpaid') mn['score'] = min(lastpaid_ago, mn.get('activetime')) - else: mn['score'] = 0 @@ -138,17 +130,13 @@ def getMasternodes(self): return mnList @process_RPC_exceptions - def getNextSuperBlock(self): - n = 0 + def getNextSuperBlock(self) -> int: with self.lock: - n = self.conn.getnextsuperblock() - - return n + return self.conn.getnextsuperblock() @process_RPC_exceptions - def getProposalsProjection(self): + def getProposalsProjection(self) -> list: printDbg("RPC: Getting proposals projection...") - data = [] proposals = [] with self.lock: # get budget projection JSON data @@ -156,11 +144,12 @@ def getProposalsProjection(self): 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')) + new_proposal = { + 'Name': p.get('Name'), + 'Allotted': float(p.get("Alloted")), + 'Votes': p.get('Yeas') - p.get('Nays'), + 'Total_Allotted': float(p.get('TotalBudgetAlloted')) + } # append dictionary to list proposals.append(new_proposal) @@ -168,27 +157,20 @@ def getProposalsProjection(self): return proposals @process_RPC_exceptions - def getProtocolVersion(self): - res = DEFAULT_PROTOCOL_VERSION + def getProtocolVersion(self) -> int: with self.lock: prot_version = self.conn.getinfo().get('protocolversion') - res = int(prot_version) - - return res + return int(prot_version) if prot_version else DEFAULT_PROTOCOL_VERSION @process_RPC_exceptions - def getRawTransaction(self, txid): - res = None + def getRawTransaction(self, txid: str) -> str: with self.lock: - res = self.conn.getrawtransaction(txid) - - return res + return self.conn.getrawtransaction(txid) @process_RPC_exceptions - def getStatus(self): + def getStatus(self) -> tuple[bool, str, int, float, bool]: 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." + statusMess = "Unable to connect to a PIVX RPC server.\nEither the local PIVX wallet is not open, or the remote RPC server is not responding." n = 0 response_time = None with self.lock: @@ -204,58 +186,38 @@ def getStatus(self): return status, statusMess, n, response_time, isTestnet @process_RPC_exceptions - def isBlockchainSynced(self): - res = False - response_time = None + def isBlockchainSynced(self) -> tuple[bool, float]: with self.lock: status, response_time = timeThis(self.conn.mnsync, 'status') if status is not None: - res = status.get("IsBlockchainSynced") - - return res, response_time + return status.get("IsBlockchainSynced"), response_time + return False, response_time @process_RPC_exceptions - def mnBudgetRawVote(self, mn_tx_hash, mn_tx_index, proposal_hash, vote, time, vote_sig): - res = None + def mnBudgetRawVote(self, mn_tx_hash: str, mn_tx_index: int, proposal_hash: str, vote: str, time: int, vote_sig: str) -> str: with self.lock: - res = self.conn.mnbudgetrawvote(mn_tx_hash, mn_tx_index, proposal_hash, vote, time, vote_sig) - - return res + return self.conn.mnbudgetrawvote(mn_tx_hash, mn_tx_index, proposal_hash, vote, time, vote_sig) @process_RPC_exceptions - def decodemasternodebroadcast(self, work): + def decodemasternodebroadcast(self, work: str) -> str: printDbg("RPC: Decoding masternode broadcast...") - res = "" with self.lock: - res = self.conn.decodemasternodebroadcast(work.strip()) - - return res + return self.conn.decodemasternodebroadcast(work.strip()) @process_RPC_exceptions - def relaymasternodebroadcast(self, work): + def relaymasternodebroadcast(self, work: str) -> str: printDbg("RPC: Relaying masternode broadcast...") - res = "" with self.lock: - res = self.conn.relaymasternodebroadcast(work.strip()) - - return res + return self.conn.relaymasternodebroadcast(work.strip()) @process_RPC_exceptions - def sendRawTransaction(self, tx_hex): - dbg_mess = "RPC: Sending raw transaction" - dbg_mess += "..." - printDbg(dbg_mess) - tx_id = None + def sendRawTransaction(self, tx_hex: str) -> str: + printDbg("RPC: Sending raw transaction...") with self.lock: - tx_id = self.conn.sendrawtransaction(tx_hex, True) - - return tx_id + return self.conn.sendrawtransaction(tx_hex, True) @process_RPC_exceptions - def verifyMessage(self, pivxaddress, signature, message): + def verifyMessage(self, pivxaddress: str, signature: str, message: str) -> bool: printDbg("RPC: Verifying message...") - res = False with self.lock: - res = self.conn.verifymessage(pivxaddress, signature, message) - - return res + return self.conn.verifymessage(pivxaddress, signature, message) diff --git a/src/tabRewards.py b/src/tabRewards.py index 4e6c1c7..48023f2 100644 --- a/src/tabRewards.py +++ b/src/tabRewards.py @@ -7,15 +7,14 @@ import threading import simplejson as json -from PyQt5.Qt import QApplication -from PyQt5.QtCore import Qt +from PyQt5.Qt import QApplication, Qt 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, checkTxInputs from pivx_parser import ParseTx, IsPayToColdStaking, GetDelegatedStaker -from qt.gui_tabRewards import TabRewards_gui +from qt.gui_tabRewards import TabRewardsGui from threads import ThreadFuns from txCache import TxCache from utils import checkPivxAddr @@ -34,7 +33,7 @@ def __init__(self, caller): self.suggestedFee = MINIMUM_FEE # --- Initialize GUI - self.ui = TabRewards_gui(self.caller.imgDir) + self.ui = TabRewardsGui(self.caller.imgDir) self.caller.tabRewards = self.ui self.ui.btn_Copy.setIcon(self.caller.copy_icon) @@ -48,14 +47,14 @@ def __init__(self, caller): 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.btn_sendRewards.clicked.connect(lambda: self.onSendRewards()) - self.ui.btn_Cancel.clicked.connect(lambda: self.onCancel()) - self.ui.btn_Copy.clicked.connect(lambda: self.onCopy()) + self.ui.addySelect.currentIndexChanged.connect(self.onChangeSelected) + self.ui.rewardsList.box.itemClicked.connect(self.updateSelection) + self.ui.btn_reload.clicked.connect(self.loadSelection) + self.ui.btn_selectAllRewards.clicked.connect(self.onSelectAllRewards) + self.ui.btn_deselectAllRewards.clicked.connect(self.onDeselectAllRewards) + self.ui.btn_sendRewards.clicked.connect(self.onSendRewards) + self.ui.btn_Cancel.clicked.connect(self.onCancel) + self.ui.btn_Copy.clicked.connect(self.onCopy) # Connect Signals self.caller.sig_UTXOsLoading.connect(self.update_loading_utxos) @@ -72,10 +71,10 @@ def display_utxos(self): rewards = self.caller.parent.db.getRewardsList(self.curr_addr) if rewards is not None: - def item(value): + def item(value: str) -> QTableWidgetItem: item = QTableWidgetItem(value) item.setTextAlignment(Qt.AlignCenter) - item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + item.setFlags(item.flags() | Qt.ItemIsEnabled | Qt.ItemIsSelectable) return item # Clear up old list @@ -93,17 +92,17 @@ def item(value): self.ui.rewardsList.box.showRow(row) if utxo['staker'] != "": self.ui.rewardsList.box.item(row, 2).setIcon(self.caller.coldStaking_icon) - self.ui.rewardsList.box.item(row, 2).setToolTip("Staked by <b>%s</b>" % utxo['staker']) + self.ui.rewardsList.box.item(row, 2).setToolTip(f"Staked by <b>{utxo['staker']}</b>") # make immature rewards unselectable if utxo['coinstake']: required = 16 if self.caller.isTestnetRPC else 101 if utxo['confirmations'] < required: - for i in range(0, 4): + for i in range(4): self.ui.rewardsList.box.item(row, i).setFlags(Qt.NoItemFlags) ttip = self.ui.rewardsList.box.item(row, i).toolTip() self.ui.rewardsList.box.item(row, i).setToolTip( - ttip + "\n(Immature - %d confirmations required)" % required) + ttip + f"\n(Immature - {required} confirmations required)") self.ui.rewardsList.box.resizeColumnsToContents() @@ -115,15 +114,12 @@ def item(value): if not self.caller.rpcConnected: self.ui.resetStatusLabel('<b style="color:red">PIVX wallet not connected</b>') else: - self.ui.resetStatusLabel('<b style="color:red">Found no Rewards for %s</b>' % self.curr_addr) + self.ui.resetStatusLabel(f'<b style="color:red">Found no Rewards for {self.curr_addr}</b>') - def getSelection(self): + def getSelection(self) -> list: # 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) + rows = {item.row() for item in items} indexes = list(rows) # Get UTXO info from DB for each selection = [] @@ -139,7 +135,7 @@ def loadSelection(self): 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) + printDbg(f"Unable to connect - hw status: {self.caller.hwStatus}") return None self.ui.addySelect.clear() @@ -159,7 +155,7 @@ def loadSelection_thread(self, ctrl): self.caller.parent.cache["intExt"] = persistCacheSetting('cache_intExt', intExt) for i in range(spathFrom, spathTo + 1): - path = "%d'/%d/%d" % (hwAcc, intExt, i) + path = f"{hwAcc}'/{intExt}/{i}" address = self.caller.hwdevice.scanForAddress(hwAcc, i, intExt, isTestnet) try: balance = self.caller.apiClient.getBalance(address) @@ -167,9 +163,9 @@ def loadSelection_thread(self, ctrl): print(e) balance = 0 - itemLine = "%s -- %s" % (path, address) - if (balance): - itemLine += " [%s PIV]" % str(balance) + itemLine = f"{path} -- {address}" + if balance: + itemLine += f" [{balance} PIV]" self.ui.addySelect.addItem(itemLine, [path, address, balance]) @@ -192,7 +188,7 @@ def load_utxos_thread(self, ctrl): # Get raw tx u['rawtx'] = TxCache(self.caller)[u['txid']] if u['rawtx'] is None: - printDbg("Unable to get raw TX with hash=%s from RPC server." % u['txid']) + printDbg(f"Unable to get raw TX with hash={u['txid']} from RPC server.") # Don't save UTXO if raw TX is unavailable utxos.remove(u) u['staker'] = "" @@ -263,7 +259,7 @@ def onSendRewards(self): self.caller.onCheckHw() if self.caller.hwStatus != 2: myPopUp_sb(self.caller, "crit", 'PET4L - hw device check', "Connect to HW device first") - printDbg("Unable to connect to hardware device. The device status is: %d" % self.caller.hwStatus) + printDbg(f"Unable to connect to hardware device. The device status is: {self.caller.hwStatus}") return None # SEND @@ -277,15 +273,15 @@ def SendRewards(self, inputs=None, gui=None): # re-connect signals try: self.caller.hwdevice.api.sigTxdone.disconnect() - except: + except Exception: pass try: self.caller.hwdevice.api.sigTxabort.disconnect() - except: + except Exception: pass try: self.caller.hwdevice.api.tx_progress.disconnect() - except: + except Exception: pass self.caller.hwdevice.api.sigTxdone.connect(gui.FinishSend) self.caller.hwdevice.api.sigTxabort.connect(gui.AbortSend) @@ -301,7 +297,7 @@ def SendRewards(self, inputs=None, gui=None): num_of_inputs = len(self.selectedRewards) else: # bulk send - num_of_inputs = sum([len(x['utxos']) for x in inputs]) + num_of_inputs = sum(len(x['utxos']) for x in inputs) ans = checkTxInputs(self.caller, num_of_inputs) if ans is None or ans == QMessageBox.No: # emit sigTxAbort and return @@ -310,9 +306,9 @@ def SendRewards(self, inputs=None, gui=None): # LET'S GO if inputs is None: - printDbg("Sending from PIVX address %s to PIVX address %s " % (self.curr_addr, self.dest_addr)) + printDbg(f"Sending from PIVX address {self.curr_addr} to PIVX address {self.dest_addr} ") else: - printDbg("Sweeping rewards to PIVX address %s " % self.dest_addr) + printDbg(f"Sweeping rewards to PIVX address {self.dest_addr} ") self.ui.rewardsList.statusLabel.hide() printDbg("Preparing transaction. Please wait...") self.ui.loadingLine.show() @@ -374,10 +370,9 @@ def FinishSend(self, serialized_tx, amount_to_send): 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 = '<p>Broadcast signed transaction?</p><p>Destination address:<br><b>%s</b></p>' % destination - message += '<p>Amount: <b>%s</b> PIV<br>' % str(round(amount / 1e8, 8)) - message += 'Fees: <b>%s</b> PIV <br>Size: <b>%d</b> Bytes</p>' % ( - str(round(self.currFee / 1e8, 8)), len(tx_hex) / 2) + message = f'<p>Broadcast signed transaction?</p><p>Destination address:<br><b>{destination}</b></p>' + message += f'<p>Amount: <b>{round(amount / 1e8, 8)}</b> PIV<br>' + message += f'Fees: <b>{round(self.currFee / 1e8, 8)}</b> PIV <br>Size: <b>{len(tx_hex) / 2}</b> Bytes</p>' except Exception as e: printException(getCallerName(), getFunctionName(), "decoding exception", str(e)) message = '<p>Unable to decode TX- Broadcast anyway?</p>' @@ -392,7 +387,7 @@ def FinishSend(self, serialized_tx, amount_to_send): txid = self.caller.rpcClient.sendRawTransaction(tx_hex) if txid is None: raise Exception("Unable to send TX - connection to RPC server lost.") - printDbg("Transaction sent. ID: %s" % txid) + printDbg(f"Transaction sent. ID: {txid}") mess2_text = "<p>Transaction successfully sent.</p>" mess2 = QMessageBox(QMessageBox.Information, 'transaction Sent', mess2_text) mess2.setDetailedText(txid) @@ -431,15 +426,15 @@ def updateSelection(self, clicked_item=None): self.selectedRewards = self.getSelection() numOfInputs = len(self.selectedRewards) if numOfInputs: - for i in range(0, numOfInputs): - total += int(self.selectedRewards[i].get('satoshis')) + for reward in self.selectedRewards: + total += int(reward.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))) + printDbg(f"estimatedTxSize is {estimatedTxSize} kB") + printDbg(f"suggested fee is {self.suggestedFee} PIV ({feePerKb} PIV/kB)") self.ui.selectedRewardsLine.setText(str(round(total / 1e8, 8))) @@ -450,8 +445,6 @@ def updateSelection(self, clicked_item=None): def update_loading_utxos(self, percent): if percent < 100: - self.ui.resetStatusLabel('<em><b style="color:purple">Checking explorer... %d%%</b></em>' % percent) + self.ui.resetStatusLabel(f'<em><b style="color:purple">Checking explorer... {percent}%</b></em>') else: self.display_utxos() - - diff --git a/src/threads.py b/src/threads.py index 8ba0225..d0c3113 100644 --- a/src/threads.py +++ b/src/threads.py @@ -8,28 +8,41 @@ Based on project: https://github.com/Bertrand256/dash-masternode-tool """ + import threading import traceback from functools import partial from workerThread import WorkerThread +from typing import Callable, Any, Optional class ThreadFuns: @staticmethod - def runInThread(worker_fun, worker_fun_args, on_thread_finish=None, on_thread_exception=None, - skip_raise_exception=False): + def runInThread( + worker_fun: Callable[..., Any], + worker_fun_args: tuple, + on_thread_finish: Optional[Callable[[], None]] = None, + on_thread_exception: Optional[Callable[[Exception], None]] = None, + skip_raise_exception: bool = False + ) -> threading.Thread: """ 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 on_thread_exception: function to be called if an exception occurs in the thread :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): + def on_thread_finished_int( + thread_arg: WorkerThread, + on_thread_finish_arg: Optional[Callable[[], None]], + skip_raise_exception_arg: bool, + on_thread_exception_arg: Optional[Callable[[Exception], None]] + ): if thread_arg.worker_exception: if on_thread_exception_arg: on_thread_exception_arg(thread_arg.worker_exception) @@ -44,14 +57,13 @@ def on_thread_finished_int(thread_arg, on_thread_finish_arg, skip_raise_exceptio # 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)) + print(f'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) + 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 diff --git a/src/trezorClient.py b/src/trezorClient.py index cef6ee9..4eaa4e7 100644 --- a/src/trezorClient.py +++ b/src/trezorClient.py @@ -23,7 +23,7 @@ from threads import ThreadFuns from txCache import TxCache -from qt.dlg_pinMatrix import PinMatrix_dlg +from qt.dlg_pinMatrix import PinMatrixDlg def process_trezor_exceptions(func): @@ -47,21 +47,15 @@ def process_trezor_exceptions_int(*args, **kwargs): class TrezorApi(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) - # signal: sig_disconnected -emitted with DisconnectedException sig_disconnected = pyqtSignal(str) def __init__(self, model, main_wnd, *args, **kwargs): - QObject.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self.model = model # index of HW_devices self.main_wnd = main_wnd self.messages = [ @@ -71,7 +65,6 @@ def __init__(self, model, main_wnd, *args, **kwargs): "Wrong device model detected.", "Wrong PIN inserted" ] - # Device Lock for threads self.lock = threading.RLock() self.status = 0 self.client = None @@ -83,9 +76,7 @@ def append_inputs_to_TX(self, utxo, bip32_path, inputs): if utxo['staker'] != "": printException(getCallerName(), getFunctionName(), "Unable to sing P2CS on Trezor", "") return - # Update amount self.amount += int(utxo['satoshis']) - # Add input address_n = parse_path(bip32_path) prev_hash = binascii.unhexlify(utxo['txid']) it = trezor_proto.TxInputType( @@ -120,9 +111,7 @@ def initDevice(self): self.status = 0 devices = enumerate_devices() if not len(devices): - # No device connected return - # Use the first device for now d = devices[0] ui = TrezorUi() try: @@ -138,13 +127,10 @@ def initDevice(self): model = self.client.features.model or "1" if not self.checkModel(model): self.status = 3 - self.messages[3] = "Wrong device model (%s) detected.\nLooking for model %s." % ( - HW_devices[self.model][0], model - ) + self.messages[3] = f"Wrong device model ({model}) detected.\nLooking for model {HW_devices[self.model][0]}." return required_version = MINIMUM_FIRMWARE_VERSION[model] - printDbg("Current version is %s (minimum required: %s)" % (str(self.client.version), str(required_version))) - # Check device is unlocked + printDbg(f"Current version is {str(self.client.version)} (minimum required: {str(required_version)})") bip32_path = parse_path(MPATH + "%d'/0/%d" % (0, 0)) _ = btc.get_address(self.client, 'PIVX', bip32_path, False) self.status = 2 @@ -161,7 +147,6 @@ def load_prev_txes(self, rewardsArray): json_tx = ParseTx(raw_tx) txes[prev_hash] = self.json_to_tx(json_tx) - # completion percent emitted curr_utxo_checked += 1 completion = int(95 * curr_utxo_checked / num_of_txes) self.tx_progress.emit(completion) @@ -176,7 +161,6 @@ def json_to_tx(self, jtx): t.bin_outputs = [self.json_to_bin_output(output) for output in jtx["vout"]] return t - def json_to_input(self, input): i = trezor_proto.TxInputType() if "coinbase" in input: @@ -190,7 +174,6 @@ def json_to_input(self, input): i.sequence = input["sequence"] return i - def json_to_bin_output(self, output): o = trezor_proto.TxOutputBinType() o.amount = int(output["value"]) @@ -204,13 +187,11 @@ def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, i self.amount = 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'], inputs) @@ -230,10 +211,9 @@ def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, i self.mBox2 = QMessageBox(caller) self.messageText = "<p>Signing transaction...</p>" - # messageText += "From bip32_path: <b>%s</b><br><br>" % str(bip32_path) - self.messageText += "<p>Payment to:<br><b>%s</b></p>" % dest_address - self.messageText += "<p>Net amount:<br><b>%s</b> PIV</p>" % str(round(self.amount / 1e8, 8)) - self.messageText += "<p>Fees:<br><b>%s</b> PIV<p>" % str(round(int(tx_fee) / 1e8, 8)) + self.messageText += f"<p>Payment to:<br><b>{dest_address}</b></p>" + self.messageText += f"<p>Net amount:<br><b>{str(round(self.amount / 1e8, 8))}</b> PIV</p>" + self.messageText += f"<p>Fees:<br><b>{str(round(int(tx_fee) / 1e8, 8))}</b> PIV<p>" messageText = self.messageText + "Signature Progress: 0 %" self.mBox2.setText(messageText) self.setBoxIcon(self.mBox2, caller) @@ -281,17 +261,14 @@ def signMess(self, caller, hwpath, message, isTestnet=False): path = MPATH_TESTNET + hwpath else: path = MPATH + hwpath - # Connection pop-up self.mBox = QMessageBox(caller) - messageText = "Check display of your hardware device\n\n- message:\n\n%s\n\n-path:\t%s\n" % ( - splitString(message, 32), path) + messageText = f"Check display of your hardware device\n\n- message:\n\n{splitString(message, 32)}\n\n-path:\t{path}\n" self.mBox.setText(messageText) self.setBoxIcon(self.mBox, caller) self.mBox.setWindowTitle("CHECK YOUR TREZOR") self.mBox.setStandardButtons(QMessageBox.NoButton) self.mBox.show() - # Sign message ThreadFuns.runInThread(self.signMessageSign, (path, message, isTestnet), self.signMessageFinish) @process_trezor_exceptions @@ -331,29 +308,23 @@ def signTxSign(self, ctrl, inputs, outputs, txes, isTestnet=False): 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): - # -1 simply adds a waiting message to the actual progress if percent == -1: t = self.mBox2.text() messageText = t + "<br>Please confirm action on your Trezor device..." else: - messageText = self.messageText + "Signature Progress: <b style='color:red'>" + str(percent) + " %</b>" + messageText = self.messageText + f"Signature Progress: <b style='color:red'>{percent} %</b>" self.mBox2.setText(messageText) QApplication.processEvents() -# From trezorlib.btc def sign_tx(sig_percent, client, coin_name, inputs, outputs, details=None, prev_txes=None): - # set up a transactions dict txes = {None: trezor_proto.TransactionType(inputs=inputs, outputs=outputs)} - # preload all relevant transactions ahead of time for inp in inputs: if inp.script_type not in ( trezor_proto.InputScriptType.SPENDP2SHWITNESS, @@ -379,13 +350,11 @@ def sign_tx(sig_percent, client, coin_name, inputs, outputs, details=None, prev_ res = client.call(signtx) - # Prepare structure for signatures signatures = [None] * len(inputs) serialized_tx = b"" def copy_tx_meta(tx): tx_copy = trezor_proto.TransactionType(**tx) - # clear fields tx_copy.inputs_cnt = len(tx.inputs) tx_copy.inputs = [] tx_copy.outputs_cnt = len(tx.bin_outputs or tx.outputs) @@ -397,10 +366,9 @@ def copy_tx_meta(tx): R = trezor_proto.RequestType - percent = 0 # Used for signaling progress. 1-10 for inputs/outputs, 10-100 for sigs. + percent = 0 sig_percent.emit(percent) while isinstance(res, trezor_proto.TxRequest): - # If there's some part of signed transaction, let's add it if res.serialized: if res.serialized.serialized_tx: serialized_tx += res.serialized.serialized_tx @@ -409,16 +377,14 @@ def copy_tx_meta(tx): idx = res.serialized.signature_index sig = res.serialized.signature if signatures[idx] is not None: - raise ValueError("Signature for index %d already filled" % idx) + raise ValueError(f"Signature for index {idx} already filled") signatures[idx] = sig - # emit completion percent percent = 10 + int(90 * (idx + 1) / len(signatures)) sig_percent.emit(percent) if res.request_type == R.TXFINISHED: break - # Device asked for one more information, let's process it. current_tx = txes[res.details.tx_hash] if res.request_type == R.TXMETA: @@ -434,7 +400,6 @@ def copy_tx_meta(tx): res = client.call(trezor_proto.TxAck(tx=msg)) elif res.request_type == R.TXOUTPUT: - # Update just one percent then display additional waiting message (emitting -1) if percent == 9: percent += 1 sig_percent.emit(percent) @@ -466,10 +431,9 @@ def copy_tx_meta(tx): return signatures, serialized_tx -class TrezorUi(object): +class TrezorUi: def __init__(self): self.prompt_shown = False - pass def get_pin(self, code=None) -> str: if code == PIN_CURRENT: @@ -481,7 +445,7 @@ def get_pin(self, code=None) -> str: else: desc = "PIN" - pin = ask_for_pin_callback("Please enter {}".format(desc)) + pin = ask_for_pin_callback(f"Please enter {desc}") if pin is None: raise exceptions.Cancelled return pin @@ -500,7 +464,7 @@ def button_request(self, msg_code): def ask_for_pin_callback(msg, hide_numbers=True): - dlg = PinMatrix_dlg(title=msg, fHideBtns=hide_numbers) + dlg = PinMatrixDlg(title=msg, fHideBtns=hide_numbers) if dlg.exec_(): return dlg.getPin() else: diff --git a/src/utils.py b/src/utils.py index 911ebb7..d7c16e0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -35,13 +35,13 @@ def b64encode(text): def checkPivxAddr(address, isTestnet=False): try: - # check leading char 'D' or (for testnet) 'x' or 'y' + # Check leading char 'D' or (for testnet) 'x' or 'y' if isTestnet and address[0] not in P2PKH_PREFIXES_TNET + P2SH_PREFIXES_TNET: return False if not isTestnet and address[0] not in P2PKH_PREFIXES + P2SH_PREFIXES: return False - # decode and verify checksum + # 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: @@ -58,13 +58,13 @@ def compose_tx_locking_script(dest_address, isTestnet=False): :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 + 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 (((not isTestnet) and (dest_address[0] in P2PKH_PREFIXES)) or (isTestnet and (dest_address[0] in P2PKH_PREFIXES_TNET))): - # sequence of opcodes/arguments for p2pkh (pay-to-public-key-hash) + # 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') + \ @@ -73,7 +73,7 @@ def compose_tx_locking_script(dest_address, isTestnet=False): OP_CHECKSIG elif (((not isTestnet) and (dest_address[0] in P2SH_PREFIXES)) or (isTestnet and (dest_address[0] in P2SH_PREFIXES_TNET))): - # sequence of opcodes/arguments for p2sh (pay-to-script-hash) + # Sequence of opcodes/arguments for p2sh (pay-to-script-hash) scr = OP_HASH160 + \ int.to_bytes(len(pubkey_hash), 1, byteorder='little') + \ pubkey_hash + \ @@ -182,7 +182,7 @@ def ipmap(ip, port): ipv6map += a else: - raise Exception("invalid version number (%d)" % ipAddr.version) + raise Exception("Invalid version number (%d)" % ipAddr.version) ipv6map += int(port).to_bytes(2, byteorder='big').hex() if len(ipv6map) != 36: @@ -210,16 +210,16 @@ def num_to_varint(a): def read_varint(buffer, offset): - if (buffer[offset] < 0xfd): + if buffer[offset] < 0xfd: value_size = 1 value = buffer[offset] - elif (buffer[offset] == 0xfd): + elif buffer[offset] == 0xfd: value_size = 3 value = int.from_bytes(buffer[offset + 1: offset + 3], byteorder='little') - elif (buffer[offset] == 0xfe): + elif buffer[offset] == 0xfe: value_size = 5 value = int.from_bytes(buffer[offset + 1: offset + 5], byteorder='little') - elif (buffer[offset] == 0xff): + elif buffer[offset] == 0xff: value_size = 9 value = int.from_bytes(buffer[offset + 1: offset + 9], byteorder='little') else: @@ -236,7 +236,6 @@ def serialize_input_str(tx, prevout_n, sequence, script_sig): 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: diff --git a/src/watchdogThreads.py b/src/watchdogThreads.py index 574c866..9bc016e 100644 --- a/src/watchdogThreads.py +++ b/src/watchdogThreads.py @@ -12,13 +12,13 @@ from misc import printOK -class CtrlObject(object): +class CtrlObject: pass class RpcWatchdog(QObject): def __init__(self, control_tab, timer_off=10, timer_on=120, *args, **kwargs): - QObject.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self.firstLoop = True self.shutdown_flag = Event() self.control_tab = control_tab diff --git a/src/workerThread.py b/src/workerThread.py index 6b07dd2..0f8ba50 100644 --- a/src/workerThread.py +++ b/src/workerThread.py @@ -13,7 +13,7 @@ from misc import printError -class CtrlObject(object): +class CtrlObject: pass @@ -23,7 +23,7 @@ class WorkerThread(QThread): """ def __init__(self, worker_fun, worker_fun_args): - QThread.__init__(self) + super().__init__() self.worker_fun = worker_fun self.worker_fun_args = worker_fun_args # prepare control object passed to external thread function @@ -44,4 +44,5 @@ def run(self): self.worker_result = self.worker_fun(self.ctrl_obj, *self.worker_fun_args) except Exception as e: printError("worker thread", "run", str(e)) + self.worker_exception = e self.stop()