def showDialog(title, msg): icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap("iconMusicAnalyzer"), QtGui.QIcon.Normal, QtGui.QIcon.Off) msgBox = QMessageBox() msgBox.setWindowIcon(icon) msgBox.setText(msg) msgBox.setWindowTitle(title) msgBox.setStandardButtons(QMessageBox.Ok) returnValue = msgBox.exec() if returnValue == QMessageBox.Ok: msgBox.accept()
def addUserInDB(self, var1, var2, var3, var4, var5, var6, var7): if var7 == "": QMessageBox.critical( QWidget(), 'Ошибка!', "Please input birthday in format year-month-day!") else: var8 = datetime.strptime(var7, '%Y-%m-%d') db = postgresql.open("pq://*****:*****@localhost:5432/book") rowquery = db.prepare("select * from user_book") row = len(rowquery()) row = row + 1 query = db.prepare( "insert into user_book (id,first_name,second_name,patronymic,phone,department,email,date_of_birth) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)" ) query(row, var1, var2, var3, var4, var5, var6, var8.date()) QMessageBox.accept(QWidget(), 'Congratulations', "USer are add in book") self.close()
class MessageBox: def __init__(self, type, message): if (type == 'error' or type == 'info'): self.message_box = QMessageBox() self.message_box.setInformativeText(message) if (type == 'error'): self.message_box.setIcon(QMessageBox.Critical) self.message_box.setText("Ошибка") self.message_box.setWindowTitle("Ошибка") elif (type == 'info'): self.message_box.setIcon(QMessageBox.Information) self.message_box.setText("Информация") self.message_box.setWindowTitle("Информация") else: raise Exception("Неизвестный тип сообщения") def show(self): self.message_box.show() self.message_box.exec() def hide(self): self.message_box.accept()
class LedgerApi(QObject): # signal: sig1 (thread) is done - emitted by signMessageFinish sig1done = pyqtSignal(str) # signal: sigtx (thread) is done - emitted by signTxFinish sigTxdone = pyqtSignal(bytearray, str) # signal: sigtx (thread) is done (aborted) - emitted by signTxFinish sigTxabort = pyqtSignal() # signal: tx_progress percent - emitted by perepare_transfer_tx_bulk tx_progress = pyqtSignal(int) # signal: sig_progress percent - emitted by signTxSign sig_progress = pyqtSignal(int) # signal: sig_disconnected -emitted with DisconnectedException sig_disconnected = pyqtSignal(str) def __init__(self, main_wnd, *args, **kwargs): QObject.__init__(self, *args, **kwargs) self.main_wnd = main_wnd self.model = [x[0] for x in HW_devices].index("LEDGER Nano") self.messages = [ 'Device not initialized.', 'Unable to connect to the device. Please check that the PIVX app on the device is open, and try again.', 'Hardware device connected.' ] # Device Lock for threads self.lock = threading.RLock() self.status = 0 self.dongle = None printDbg("Creating HW device class") @process_ledger_exceptions def initDevice(self): printDbg("Initializing Ledger") with self.lock: self.status = 0 self.dongle = getDongle(False) printOK('Ledger Nano drivers found') self.chip = btchip(self.dongle) printDbg("Ledger Initialized") self.status = 1 ver = self.chip.getFirmwareVersion() printOK("Ledger HW device connected [v. %s]" % str(ver.get('version'))) # Check device is unlocked bip32_path = MPATH + "%d'/0/%d" % (0, 0) _ = self.chip.getWalletPublicKey(bip32_path) self.status = 2 self.sig_progress.connect(self.updateSigProgress) def closeDevice(self, message=''): printDbg("Closing LEDGER client") self.sig_disconnected.emit(message) self.status = 0 with self.lock: if self.dongle is not None: try: self.dongle.close() except: pass self.dongle = None @process_ledger_exceptions def append_inputs_to_TX(self, utxo, bip32_path): self.amount += int(utxo['satoshis']) raw_tx = TxCache(self.main_wnd)[utxo['txid']] # parse the raw transaction, so that we can extract the UTXO locking script we refer to prev_transaction = bitcoinTransaction(bytearray.fromhex(raw_tx)) utxo_tx_index = utxo['vout'] if utxo_tx_index < 0 or utxo_tx_index > len(prev_transaction.outputs): raise Exception('Incorrect value of outputIndex for UTXO %s-%d' % (utxo['txid'], utxo['vout'])) trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) self.trusted_inputs.append(trusted_input) # Hash check curr_pubkey = compress_public_key( self.chip.getWalletPublicKey(bip32_path)['publicKey']) pubkey_hash = bin_hash160(curr_pubkey) pubkey_hash_from_script = extract_pkh_from_locking_script( prev_transaction.outputs[utxo_tx_index].script) if pubkey_hash != pubkey_hash_from_script: text = "Error: The hashes for the public key for the BIP32 path, 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( ) printDbg(text) self.arg_inputs.append({ 'locking_script': prev_transaction.outputs[utxo['vout']].script, 'pubkey': curr_pubkey, 'bip32_path': bip32_path, 'outputIndex': utxo['vout'], 'txid': utxo['txid'], 'p2cs': (utxo['staker'] != "") }) @process_ledger_exceptions def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, useSwiftX=False, isTestnet=False): with self.lock: # For each UTXO create a Ledger 'trusted input' self.trusted_inputs = [] # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) self.arg_inputs = [] self.amount = 0 num_of_sigs = sum([len(mnode['utxos']) for mnode in rewardsArray]) curr_utxo_checked = 0 for mnode in rewardsArray: # Add proper HW path (for current device) on each utxo if isTestnet: mnode['path'] = MPATH_TESTNET + mnode['path'] else: mnode['path'] = MPATH + mnode['path'] # Create a TX input with each utxo for utxo in mnode['utxos']: self.append_inputs_to_TX(utxo, mnode['path']) # completion percent emitted curr_utxo_checked += 1 completion = int(95 * curr_utxo_checked / num_of_sigs) self.tx_progress.emit(completion) self.amount -= int(tx_fee) self.amount = int(self.amount) arg_outputs = [{ 'address': dest_address, 'valueSat': self.amount }] # there will be multiple outputs soon self.new_transaction = bitcoinTransaction( ) # new transaction object to be used for serialization at the last stage self.new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) self.tx_progress.emit(99) for o in arg_outputs: output = bitcoinOutput() output.script = compose_tx_locking_script( o['address'], isTestnet) output.amount = int.to_bytes(o['valueSat'], 8, byteorder='little') self.new_transaction.outputs.append(output) self.tx_progress.emit(100) # join all outputs - will be used by Ledger for signing transaction self.all_outputs_raw = self.new_transaction.serializeOutputs() self.mBox2 = QMessageBox(caller) self.messageText = "<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)) if useSwiftX: self.messageText += "<p>Fees (SwiftX flat rate):<br><b>%s</b> PIV<p>" % str( round(int(tx_fee) / 1e8, 8)) else: self.messageText += "<p>Fees:<br><b>%s</b> PIV<p>" % str( round(int(tx_fee) / 1e8, 8)) messageText = self.messageText + "Signature Progress: 0 %" self.mBox2.setText(messageText) self.mBox2.setIconPixmap( caller.tabMain.ledgerImg.scaledToHeight( 200, Qt.SmoothTransformation)) self.mBox2.setWindowTitle("CHECK YOUR LEDGER") self.mBox2.setStandardButtons(QMessageBox.NoButton) self.mBox2.setMaximumWidth(500) self.mBox2.show() ThreadFuns.runInThread(self.signTxSign, (), self.signTxFinish) @process_ledger_exceptions def scanForAddress(self, account, spath, isTestnet=False): with self.lock: if not isTestnet: curr_path = MPATH + "%d'/0/%d" % (account, spath) curr_addr = self.chip.getWalletPublicKey(curr_path).get( 'address')[12:-2] else: curr_path = MPATH_TESTNET + "%d'/0/%d" % (account, spath) pubkey = compress_public_key( self.chip.getWalletPublicKey(curr_path).get( 'publicKey')).hex() curr_addr = pubkey_to_address(pubkey, isTestnet) return curr_addr @process_ledger_exceptions def scanForPubKey(self, account, spath, isTestnet=False): hwpath = "%d'/0/%d" % (account, spath) if isTestnet: curr_path = MPATH_TESTNET + hwpath else: curr_path = MPATH + hwpath with self.lock: nodeData = self.chip.getWalletPublicKey(curr_path) return compress_public_key(nodeData.get('publicKey')).hex() @process_ledger_exceptions def signMess(self, caller, hwpath, message, isTestnet=False): if isTestnet: path = MPATH_TESTNET + hwpath else: path = MPATH + hwpath # Ledger doesn't accept characters other that ascii printable: # https://ledgerhq.github.io/btchip-doc/bitcoin-technical.html#_sign_message message = message.encode('ascii', 'ignore') message_sha = splitString(single_sha256(message).hex(), 32) # Connection pop-up mBox = QMessageBox(caller) 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" mBox.setText(warningText) mBox.setWindowTitle("WARNING") mBox.setStandardButtons(QMessageBox.Retry | QMessageBox.Abort) # Ask confirmation with self.lock: info = self.chip.signMessagePrepare(path, message) while info['confirmationNeeded'] and info['confirmationType'] == 34: ans = mBox.exec_() if ans == QMessageBox.Abort: raise Exception("Reconnect HW device") # we need to reconnect the device self.initDevice() info = self.chip.signMessagePrepare(path, message) printOK('Signing Message') self.mBox = QMessageBox(caller) messageText = "Check display of your hardware device\n\n- message hash:\n\n%s\n\n-path:\t%s\n" % ( message_sha, path) self.mBox.setText(messageText) self.mBox.setIconPixmap( caller.tabMain.ledgerImg.scaledToHeight( 200, Qt.SmoothTransformation)) self.mBox.setWindowTitle("CHECK YOUR LEDGER") self.mBox.setStandardButtons(QMessageBox.NoButton) self.mBox.show() # Sign message ThreadFuns.runInThread(self.signMessageSign, (), self.signMessageFinish) @process_ledger_exceptions def signMessageSign(self, ctrl): self.signature = None with self.lock: try: self.signature = self.chip.signMessageSign() except: pass def signMessageFinish(self): with self.lock: self.mBox.accept() if self.signature != None: if len(self.signature) > 4: rLength = self.signature[3] r = self.signature[4:4 + rLength] if len(self.signature) > 4 + rLength + 1: sLength = self.signature[4 + rLength + 1] if len(self.signature) > 4 + rLength + 2: s = self.signature[4 + rLength + 2:] if rLength == 33: r = r[1:] if sLength == 33: s = s[1:] work = bytes(chr(27 + 4 + (self.signature[0] & 0x01)), "utf-8") + r + s printOK("Message signed") sig1 = work.hex() else: printDbg( 'client.signMessageSign() returned invalid response (code 3): ' + self.signature.hex()) sig1 = "None" else: printDbg( 'client.signMessageSign() returned invalid response (code 2): ' + self.signature.hex()) sig1 = "None" else: printDbg( 'client.signMessageSign() returned invalid response (code 1): ' + self.signature.hex()) sig1 = "None" else: printOK("Signature refused by the user") sig1 = "None" self.sig1done.emit(sig1) @process_ledger_exceptions def signTxSign(self, ctrl): self.tx_raw = None with self.lock: starting = True curr_input_signed = 0 # sign all inputs on Ledger and add inputs in the self.new_transaction object for serialization for idx, new_input in enumerate(self.arg_inputs): try: self.chip.startUntrustedTransaction( starting, idx, self.trusted_inputs, new_input['locking_script']) self.chip.finalizeInputFull(self.all_outputs_raw) sig = self.chip.untrustedHashSign(new_input['bip32_path'], lockTime=0) except BTChipException as e: if e.args[0] != "Invalid status 6985": raise e # Signature refused by the user return new_input['signature'] = sig inputTx = bitcoinInput() inputTx.prevOut = bytearray.fromhex( new_input['txid'])[::-1] + int.to_bytes( new_input['outputIndex'], 4, byteorder='little') inputTx.script = bytearray([len(sig)]) + sig if new_input['p2cs']: inputTx.script += bytearray([0x00]) inputTx.script += bytearray([0x21]) + new_input['pubkey'] inputTx.sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) self.new_transaction.inputs.append(inputTx) starting = False # signature percent emitted curr_input_signed += 1 completion = int(100 * curr_input_signed / len(self.arg_inputs)) self.sig_progress.emit(completion) self.new_transaction.lockTime = bytearray([0, 0, 0, 0]) self.tx_raw = bytearray(self.new_transaction.serialize()) self.sig_progress.emit(100) def signTxFinish(self): self.mBox2.accept() if self.tx_raw is not None: # Signal to be catched by FinishSend on TabRewards / dlg_sewwpAll self.sigTxdone.emit(self.tx_raw, str(round(self.amount / 1e8, 8))) else: printOK("Transaction refused by the user") self.sigTxabort.emit() def updateSigProgress(self, percent): messageText = self.messageText + "Signature Progress: <b style='color:red'>" + str( percent) + " %</b>" self.mBox2.setText(messageText) QApplication.processEvents()
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) self.model = model # index of HW_devices self.main_wnd = main_wnd self.messages = [ 'Trezor not initialized. Connect and unlock it', 'Error setting up Trezor Client.', 'Hardware device connected.', "Wrong device model detected.", "Wrong PIN inserted" ] # Device Lock for threads self.lock = threading.RLock() self.status = 0 self.client = None printDbg("Creating HW device class") self.sig_progress.connect(self.updateSigProgress) @process_trezor_exceptions def append_inputs_to_TX(self, utxo, bip32_path, inputs): # 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(address_n=address_n, prev_hash=prev_hash, prev_index=int(utxo['vout'])) inputs.append(it) def checkModel(self, model): if HW_devices[self.model][0] == "TREZOR One": return model == "1" else: return model == "T" def closeDevice(self, message=''): printDbg("Closing TREZOR client") self.sig_disconnected.emit(message) self.status = 0 with self.lock: if self.client is not None: try: self.client.close() except: pass self.client = None @process_trezor_exceptions def initDevice(self): printDbg("Initializing Trezor") with self.lock: 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: self.client = TrezorClient(d, ui) except IOError: raise Exception("TREZOR device is currently in use") printOK("Trezor HW device connected [v. %s.%s.%s]" % (self.client.features.major_version, self.client.features.minor_version, self.client.features.patch_version)) self.status = 1 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) 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 bip32_path = parse_path(MPATH + "%d'/0/%d" % (0, 0)) _ = btc.get_address(self.client, 'PIVX', bip32_path, False) self.status = 2 def load_prev_txes(self, rewardsArray): curr_utxo_checked = 0 txes = {} num_of_txes = sum([len(mnode['utxos']) for mnode in rewardsArray]) for mn in rewardsArray: for utxo in mn['utxos']: prev_hash = bytes.fromhex(utxo["txid"]) if prev_hash not in txes: raw_tx = TxCache(self.main_wnd)[utxo['txid']] 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) self.tx_progress.emit(100) return txes def json_to_tx(self, jtx): t = trezor_proto.TransactionType() t.version = jtx["version"] t.lock_time = jtx["locktime"] t.inputs = [self.json_to_input(input) for input in jtx["vin"]] 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: i.prev_hash = b"\0" * 32 i.prev_index = 0xFFFFFFFF # signed int -1 i.script_sig = bytes.fromhex(input["coinbase"]) else: i.prev_hash = bytes.fromhex(input["txid"]) i.prev_index = input["vout"] i.script_sig = bytes.fromhex(input["scriptSig"]["hex"]) i.sequence = input["sequence"] return i def json_to_bin_output(self, output): o = trezor_proto.TxOutputBinType() o.amount = int(output["value"]) o.script_pubkey = bytes.fromhex(output["scriptPubKey"]["hex"]) return o def prepare_transfer_tx_bulk(self, caller, rewardsArray, dest_address, tx_fee, useSwiftX=False, isTestnet=False): inputs = [] outputs = [] c_name = "PIVX" if isTestnet: c_name += " Testnet" coin = coins.by_name[c_name] with self.lock: 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) self.amount = int(self.amount) self.amount -= int(tx_fee) if self.amount < 0: raise Exception('Invalid TX: inputs + fee != outputs') outputs.append( trezor_proto.TxOutputType( address=dest_address, address_n=None, amount=self.amount, script_type=trezor_proto.OutputScriptType.PAYTOSCRIPTHASH)) txes = self.load_prev_txes(rewardsArray) 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)) if useSwiftX: self.messageText += "<p>Fees (SwiftX flat rate):<br><b>%s</b> PIV<p>" % str( round(int(tx_fee) / 1e8, 8)) else: self.messageText += "<p>Fees:<br><b>%s</b> PIV<p>" % str( round(int(tx_fee) / 1e8, 8)) messageText = self.messageText + "Signature Progress: 0 %" self.mBox2.setText(messageText) self.setBoxIcon(self.mBox2, caller) self.mBox2.setWindowTitle("CHECK YOUR TREZOR") self.mBox2.setStandardButtons(QMessageBox.NoButton) self.mBox2.setMaximumWidth(500) self.mBox2.show() ThreadFuns.runInThread(self.signTxSign, (inputs, outputs, txes, isTestnet), self.signTxFinish) @process_trezor_exceptions def scanForAddress(self, account, spath, isTestnet=False): with self.lock: if not isTestnet: curr_path = parse_path(MPATH + "%d'/0/%d" % (account, spath)) curr_addr = btc.get_address(self.client, 'PIVX', curr_path, False) else: curr_path = parse_path(MPATH_TESTNET + "%d'/0/%d" % (account, spath)) curr_addr = btc.get_address(self.client, 'PIVX Testnet', curr_path, False) return curr_addr @process_trezor_exceptions def scanForPubKey(self, account, spath, isTestnet=False): hwpath = "%d'/0/%d" % (account, spath) if isTestnet: path = MPATH_TESTNET + hwpath else: path = MPATH + hwpath curr_path = parse_path(path) with self.lock: result = btc.get_public_node(self.client, curr_path) return result.node.public_key.hex() def setBoxIcon(self, box, caller): if HW_devices[self.model][0] == "TREZOR One": box.setIconPixmap( caller.tabMain.trezorOneImg.scaledToHeight( 200, Qt.SmoothTransformation)) else: box.setIconPixmap( caller.tabMain.trezorImg.scaledToHeight( 200, Qt.SmoothTransformation)) def signMess(self, caller, hwpath, message, isTestnet=False): if isTestnet: 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) 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 def signMessageSign(self, ctrl, path, mess, isTestnet): self.signature = None if isTestnet: hw_coin = "PIVX Testnet" else: hw_coin = "PIVX" with self.lock: bip32_path = parse_path(path) signed_mess = btc.sign_message(self.client, hw_coin, bip32_path, mess) self.signature = signed_mess.signature def signMessageFinish(self): with self.lock: self.mBox.accept() if self.signature is None: printOK("Signature refused by the user") self.sig1done.emit("None") else: self.sig1done.emit(self.signature.hex()) @process_trezor_exceptions def signTxSign(self, ctrl, inputs, outputs, txes, isTestnet=False): self.tx_raw = None if isTestnet: hw_coin = "PIVX Testnet" else: hw_coin = "PIVX" with self.lock: signed = sign_tx(self.sig_progress, self.client, hw_coin, inputs, outputs, prev_txes=txes) self.tx_raw = bytearray(signed[1]) self.sig_progress.emit(100) def signTxFinish(self): self.mBox2.accept() if self.tx_raw is not None: # Signal to be catched by FinishSend on TabRewards / dlg_sewwpAll self.sigTxdone.emit(self.tx_raw, str(round(self.amount / 1e8, 8))) else: printOK("Transaction refused by the user") self.sigTxabort.emit() def updateSigProgress(self, percent): # -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>" self.mBox2.setText(messageText) QApplication.processEvents()
class OvirtClient(QWidget): """ This class will handle the main window where all user's VMs will be listed. This will be rendered only if authentication was successfull since it doesn't make sense otherwise. Additionally, a Thread will be started checking VM status changes and update the board accordingly. """ stopThread = False # Sentinel for stopping the Thread execution autologoutWarn = False # Has the user been warned about autologout yet? openviewer_vms = [] # Initiated VMs in terms of the viewer updatesignal = pyqtSignal( int, str) # Signal to update the status icons on status changes reloadsignal = pyqtSignal() # Signal to reload the main widget warnlogoutsignal = pyqtSignal( ) # Signal to warn the user about an imminent autologout logoutsignal = pyqtSignal( bool ) # Signal to logout the current user and require credentials again lastclick = int(time( )) # Timestamp of the last click. If a timeout policy is defined and # the time exceeds this value, an autologout will be performed. def __init__(self): QWidget.__init__(self) self.initUI() def vm_based_resize(self, vmnum): """ Description: Depending on the number of VMs which the user has permissions on, we need to resize the main window accordingly in height.The fewer they are, the smaller the window will be. If MAXHEIGHT is reached, a scrollbar will also be shown (that's processed in a different place, though) Parameters: The number of VMs. Returns: The actual window height. """ global MAXHEIGHT if not vmnum: # If user has no machines, resize window to the minimum winheight = 150 no_machines = QLabel(_('no_vms')) no_machines.setWordWrap(True) no_machines.setAlignment(Qt.AlignCenter) self.grid.addWidget(no_machines, 0, 0) self.setMinimumHeight(winheight) else: # User has at least one VM if vmnum > 5: # More than 5 means resizing the window to the maximum winheight = MAXHEIGHT self.setFixedHeight(winheight) else: # Otherwise, resize depending on the number of VMs winheight = vmnum * 100 + 50 self.setFixedHeight(winheight) return winheight def get_os_icon(self, os): """ Description: Depending on the VM's OS, this method returns which icon should be used to illustrate it. Arguments: oVirt-style string representing the VM's OS Returns: The file name under IMGDIR that should be shown. """ if 'ubuntu' in os: return 'ubuntu' elif 'rhel' in os: return 'redhat' elif 'centos' in os: return 'centos' elif 'debian' in os: return 'debian' elif 'linux' in os: return 'linux' elif 'win' in os: return 'windows' return 'unknown' def generate_toolbar(self): """ Description: There will be a toolbar in the main widget with some buttons, this method will render them. Arguments: None Returns: Nothing """ global IMGDIR, conf self.toolBar = QToolBar(self) refreshAction = QAction(QIcon(IMGDIR + 'refresh.png'), _('refresh'), self) refreshAction.setShortcut('Ctrl+R') refreshAction.triggered.connect(self.refresh_grid) self.toolBar.addAction(refreshAction) self.forgetCredsAction = QAction(QIcon(IMGDIR + 'forget.png'), _('forget_credentials'), self) self.forgetCredsAction.setShortcut('Ctrl+F') self.forgetCredsAction.triggered.connect(self.forget_creds) if not isfile(conf.USERCREDSFILE): self.forgetCredsAction.setDisabled(True) self.toolBar.addAction(self.forgetCredsAction) aboutAction = QAction(QIcon(IMGDIR + 'about.png'), _('about'), self) aboutAction.setShortcut('Ctrl+I') aboutAction.triggered.connect(self.about) self.toolBar.addAction(aboutAction) exitAction = QAction(QIcon(IMGDIR + 'exit.png'), _('exit'), self) exitAction.setShortcut('Ctrl+Q') exitAction.triggered.connect(self.quit_button) self.toolBar.addAction(exitAction) self.grid.addWidget(self.toolBar, 0, 3, Qt.AlignRight) def make_button(self, filename, tooltip, alignment=Qt.AlignCenter): """ Description: Creates a QLabel which will contain an image and will simulate a button. No associated text will be shown. Arguments: 1. filename: The filename of the icon/image to show 2. tooltip: Some text to show as a tooltip to the image 3. alignment: Alignment of the button (center by default) Returns: The created QLabel button """ global STANDARDCELLCSS, IMGDIR filepath = '%s%s%s' % (IMGDIR, filename, '.png') icon = QImage(filepath) image = QLabel() image.setToolTip('<span style="color:#B9B900">%s</span>' % (tooltip)) image.setStyleSheet(STANDARDCELLCSS) image.setPixmap(QPixmap.fromImage(icon)) image.setAlignment(alignment) return image def compare_vms(self, vm1, vm2): """ Description: VM list will be sorted by names. This method is the comparison method for two VMs. We just sort them alphabetically. Arguments: Two VMs (oVirt VM objects) Returns: -1, 0 or 1 depending on the name-based sorting """ if vm1.name.lower() < vm2.name.lower(): return -1 if vm1.name.lower() == vm2.name.lower(): return 0 return 1 def p22p3_compare_vms(self, cmpfunct): """ Wrapper for Python 2 -> 3 conversion for the old cmp= method of sorted()""" class K: def __init__(self, obj, *args): self.obj = obj def __lt__(self, other): return cmpfunct(self.obj, other.obj) < 0 def __gt__(self, other): return cmpfunct(self.obj, other.obj) > 0 def __eq__(self, other): return cmpfunct(self.obj, other.obj) == 0 def __le__(self, other): return cmpfunct(self.obj, other.obj) <= 0 def __ge__(self, other): return cmpfunct(self.obj, other.obj) >= 0 def __ne__(self, other): return cmpfunct(self.obj, other.obj) != 0 return K def current_vm_status(self, vmstatus): """ Description: Single translation between oVirt-like status to human-readable status Arguments: oVirt-like status Returns: Human-readable status """ if vmstatus == 'up': hrstatus = _('up') elif vmstatus == 'down': hrstatus = _('down') elif vmstatus == 'powering_down': hrstatus = _('powering_down') elif vmstatus == 'wait_for_launch': hrstatus = _('wait_for_launch') elif vmstatus == 'powering_up': hrstatus = _('powering_up') elif vmstatus == 'reboot_in_progress': hrstatus = _('rebooting') else: hrstatus = 'unknown' return hrstatus def toggle_vm_action(self, vmstatus): """ Description: Returns the available action for the current VM's status. If machine is up, available action is turn it off and viceversa. Arguments: Current vm status Returns: Toggle action for the current status. """ if vmstatus == 'up': vmaction = _('shut_down') if vmstatus == 'down': vmaction = _('power_on') return vmaction def toggle_action_text(self, vmstatus): """ Description: One of the columns shows the current VM's status. This method returns the toggle tooltip text so the user know what will happen if they click on the status icon. Arguments: Current vm status Returns: The tooltip's informative text. """ rettxt = '%s <b>%s</b>.' % (_('current_vm_status'), self.current_vm_status(vmstatus)) if vmstatus == 'up': rettxt += ' %s %s' % (_('click_to_action'), _('shut_down')) if vmstatus == 'down': rettxt += ' %s %s' % (_('click_to_action'), _('power_on')) return rettxt def change_status(self, rowid): """ Description: If the user clicks on the column which determines VM's status, we'll allow them to change VM's status. This method shows a confirmation dialog and if accepted, it will be notified to oVirt. Arguments: The row id that has been clicked. This relationship is stored using the VmData class. Returns: Nothing """ global conf self.lastclick = int(time()) # Last click timestamp update curvmstatus = self.vmdata[rowid].vmstatus if curvmstatus != 'up' and curvmstatus != 'down': QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('vm_in_unchangeable_status')) return reply = QMessageBox.question( None, _('apptitle') + ': ' + _('confirm'), '%s <b>%s</b>. %s: <b>%s</b>.' % (_('current_vm_status'), self.current_vm_status(curvmstatus), _('confirm_vm_status_change'), self.toggle_vm_action(curvmstatus)), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: try: vms_service = conf.OVIRTCONN.vms_service() vm = vms_service.list(search='id=%s' % (self.vmdata[rowid].vmid))[0] except Error: QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('unexpected_connection_drop')) quit() if curvmstatus == 'up': try: vm_service = vms_service.vm_service( id=self.vmdata[rowid].vmid) vm_service.shutdown() QMessageBox.information( None, _('apptitle') + ': ' + _('success'), _('shutting_down_vm')) except Error: QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('vm_in_unchangeable_status')) if curvmstatus == 'down': try: vm_service = vms_service.vm_service( id=self.vmdata[rowid].vmid) vm_service.start() QMessageBox.information( None, _('apptitle') + ': ' + _('success'), _('powering_up_vm')) except Error: QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('vm_in_unchangeable_status')) def get_viewer_ticket(self, vmid): """ Description: Connecting to the machine involves two steps, the first one is obtaining a 'ticket' string for the connection request. This is done making a request to the oVirt API and then parsing the resulting XML document to get the ticket hash. Also, the request may return more than one ticket: One for SPICE and another for VNC. In this case, we'll return the one that the user defined in the settings file (SPICE as default). Arguments: The VM UUID in oVirt-format Returns: The ticket hash string """ global conf req = urllib.request.Request( '%s/%s/%s/%s' % (conf.CONFIG['ovirturl'], 'vms', vmid, 'graphicsconsoles')) # Python 2 -> 3 conversion: encodestring expects a byte-like string, not str base64str = encodestring( ('%s:%s' % (conf.USERNAME + '@' + conf.CONFIG['ovirtdomain'], conf.PASSWORD)).encode()).decode().replace('\n', '') req.add_header('Authorization', 'Basic ' + base64str) req.add_header('filter', 'true') unverified_ctxt = SSLContext(PROTOCOL_TLSv1) tickethash = urllib.request.urlopen(req, context=unverified_ctxt).read() xmlcontent = ET.fromstring(tickethash) ticket = None for data in xmlcontent.findall('graphics_console'): proto = data.findall('protocol')[0] if proto.text.lower() == conf.CONFIG['prefproto'].lower(): return data.get('id') else: ticket = data.get('id') return ticket def store_vv_file(self, vmid, ticket): """ Description: Connecting to the machine involves two steps, the second one is obtaining a 'vv' file with the connection parameters, which we can later pipe to virt-viewer and the connection will be opened. Arguments: 1. vmid: The VM UUID in oVirt-format. 2. ticket: The ticket obtained in the first step (method get_viewer_ticket) Returns: The temporary filename with all the parameters to connect to the machine (piped to virt-viewer) """ global conf if not ticket: return False req = urllib.request.Request( '%s/%s/%s/%s/%s' % (conf.CONFIG['ovirturl'], 'vms', vmid, 'graphicsconsoles', ticket)) # Python 2 -> 3 conversion: encodestring expects a byte-like string, not str base64str = encodestring( ('%s:%s' % (conf.USERNAME + '@' + conf.CONFIG['ovirtdomain'], conf.PASSWORD)).encode()).decode().replace('\n', '') req.add_header('Authorization', 'Basic ' + base64str) req.add_header('Content-Type', 'application/xml') req.add_header('Accept', 'application/x-virt-viewer') req.add_header('filter', 'true') unverified_ctxt = SSLContext(PROTOCOL_TLSv1) try: contents = urllib.request.urlopen(req, context=unverified_ctxt).read() if conf.CONFIG['fullscreen'] == '1': contents = contents.replace('fullscreen=0', 'fullscreen=1') filename = '/tmp/viewer-' + str(randint(10000, 99999)) f = open(filename, 'wb') f.write(contents) f.close() return filename except urllib.request.HTTPError as em: QMessageBox.critical( None, _('apptitle') + ': ' + _('error'), _('unexpected_request_error') + '(' + str(em.code) + '): ' + em.reason + '. ' + _('check_vm_config_updated')) return None def viewer_exit(self, vmname): self.openviewer_vms.remove( vmname) # Remove the VM from the list of opened viewers self.reloadsignal.emit( ) # Enforce a reload signal to update the status icon ASAP def create_viewer_thread(self, vmname, filename): global conf def runInThread(vmname, onExit, popenArgs): viewer = Popen(popenArgs) viewer.wait() onExit(vmname) return thread = threading.Thread(target=runInThread, args=(vmname, self.viewer_exit, [ conf.CONFIG['remote_viewer_path'], '-t', vmname, '-f', '--', 'file://%s' % (filename) ])) thread.start() # Returns immediately after the thread starts return thread def connect2machine(self, vmid, vmname): """ Description: Connecting to the machine involves two steps, this method does both and makes sure everything is ok to call virt-viewer afterwards. Arguments: 1. vmid: The VM UUID in oVirt-format. 2. vmname: Just for displaying purposes, the VM name Returns: Nothing. Opens the view-viewer display. """ viewer_ticket = self.get_viewer_ticket(vmid) filename = self.store_vv_file(vmid, viewer_ticket) if filename: self.create_viewer_thread(vmname, filename) else: if vmname in self.openviewer_vms: self.openviewer_vms.remove( vmname) # Remove the VM from the list of opened viewers QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('no_viewer_file')) def connect(self, rowid): """ Description: Whenever the user clicks on the 'connect' row, this method will make sure the VM status is up and only then will call the connect2machine method. Arguments: The row id that has been clicked. This relationship is stored using the VmData class. Returns: Nothing """ self.lastclick = int(time()) # Last click timestamp update vmid = self.vmdata[rowid].vmid vmname = self.vmdata[rowid].vmname vmstatus = self.vmdata[rowid].vmstatus if vmstatus != 'up': QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('cannot_connect_if_vm_not_up')) return if vmname in self.openviewer_vms: QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('cannot_open_more_viewer_sessions')) return self.openviewer_vms.append(vmname) self.refresh_grid( ) # Enforce a dashboard reload to make the icon refresh self.connect2machine(vmid, vmname) def acquire_vm_from_vmpool(self, rowid): """ Description: A machine will be acquired by a user if they click on the icon of a VmPool Arguments: The row id that has been clicked. This relationship is stored using the VmData class. Returns: Nothing """ self.lastclick = int(time()) # Last click timestamp update vmtype = self.vmdata[rowid].vmtype if vmtype == 'vmpool': try: QMessageBox.information(None, _('apptitle') + ': ' + _('info'), _('acquiring_vm_from_pool')) vmpool_service = conf.OVIRTCONN.vm_pools_service() vmp = vmpool_service.pool_service(id=self.vmdata[rowid].vmid) vmp.allocate_vm() self.refresh_grid() except Error as e: QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), str(e)) else: QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('object_is_not_a_vmpool')) def refresh_grid(self): """ Description: Invoked when the user clicks on the 'Refresh' button in the toolbar. Reloads the board. Arguments: None Returns: Nothing """ self.lastclick = int(time()) # Last click timestamp update self.load_vms() def about(self): """ Description: Invoked when the user clicks on the 'About' button in the toolbar. Arguments: None Returns: Nothing """ self.lastclick = int(time()) # Last click timestamp update About() def forget_creds(self): """ Description: Invoked when the user clicks on the 'Forget credentials' button in the toolbar. Arguments: None Returns: Nothing """ global conf self.lastclick = int(time()) # Last click timestamp update reply = QMessageBox.question(None, _('apptitle') + ': ' + _('confirm'), _('confirm_forget_creds'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: remove(conf.USERCREDSFILE) self.forgetCredsAction.setDisabled(True) QMessageBox.information(None, _('apptitle') + ': ' + _('success'), _('creds_forgotten')) def list_vmpools(self, row, delta, step, vmpools): """ Description: Creates one row per VmPool that the user has access to. Arguments: 1. The index of the first row to loop over. 2. Delta step to sum to the progress bar. 3. The current step of the progress bar 4. The oVirt list of VmPools. Returns: The final step of the progress bar """ # For cleanness reasons, we'll firstly show available VmPools for vm in vmpools: vmname = vm.name # OS icon ostype = 'vmpool' imageOsicon = self.make_button(ostype, '<b>' + _('vmpool') + '</b>') # Machine name gridvmname = QLabel(vmname) gridvmname.setStyleSheet(STANDARDCELLCSS) gridvmname.setAlignment(Qt.AlignCenter) # Acquire VM button connect = self.make_button('grab', _('grab_vm_vmpool')) connect.mousePressEvent = lambda x, r=row: self.acquire_vm_from_vmpool( r) # Fill row with known info self.grid.addWidget(imageOsicon, row, 0) self.grid.addWidget(gridvmname, row, 1) self.grid.addWidget(connect, row, 2) # Store the correspondence between row number <-> VMPool data vmd = VmData() vmd.vmid = vm.id vmd.vmname = vm.name vmd.vmstatus = None vmd.vmtype = 'vmpool' self.vmdata[row] = vmd row += 1 step += delta self.pbar.setValue(step) return step def list_vms(self, row, delta, step, vms): """ Description: Creates one row per VM that the user has access to. Arguments: 1. The index of the first row to loop over. 2. Delta step to sum to the progress bar. 3. The current step of the progress bar 4. The oVirt list of VMs. Returns: Nothing """ # Now we'll show up the VMs for vm in vms: vmname = vm.name vmstatus = vm.status.value # OS icon ostype = self.get_os_icon(vm.os.type.lower()) imageOsicon = self.make_button( ostype, '<b>%s</b> OS' % (ostype.capitalize())) # Machine name gridvmname = QLabel(vmname) gridvmname.setStyleSheet(STANDARDCELLCSS) gridvmname.setAlignment(Qt.AlignCenter) # Connect button. Depending on whether it has already been hit, a different icon # will be shown and the behavior will also be different. if vmname not in self.openviewer_vms: connect = self.make_button('connect', _('connect')) connect.mousePressEvent = lambda x, r=row: self.connect(r) else: connect = self.make_button('viewer', _('viewer_already_opened')) # Status icon curaction = self.current_vm_status(vmstatus) imageSticon = self.make_button(vmstatus, self.toggle_action_text(vmstatus)) imageSticon.mousePressEvent = lambda x, r=row: self.change_status(r ) # Fill row with known info self.grid.addWidget(imageOsicon, row, 0) self.grid.addWidget(gridvmname, row, 1) self.grid.addWidget(imageSticon, row, 2) self.grid.addWidget(connect, row, 3) # Store the correspondence between row number <-> VM data vmd = VmData() vmd.vmid = vm.id vmd.vmname = vm.name vmd.vmstatus = vmstatus vmd.vmtype = 'vm' self.vmdata[row] = vmd row += 1 step += delta self.pbar.setValue(step) def load_vms(self): """ Description: Main core VM loader method. Will connect to oVirt, get the VM list and render them. Arguments: None Returns: Nothing """ global conf, MAXHEIGHT, BACKGROUNDCSS, STANDARDCELLCSS QObjectCleanupHandler().add(self.layout()) if not conf.USERNAME: quit() # Used to store row <-> VM correspondence self.vmdata = {} step = 0 self.pbarlayout = QGridLayout(self) self.pbar = QProgressBar(self) self.grid = QGridLayout() self.grid.setHorizontalSpacing(0) # Initially, set the layout to the progress bar self.pbar.setGeometry(250, 250, 200, 100) self.pbar.setValue(0) self.pbarlayout.addWidget(self.pbar, 0, 0) self.setLayout(self.pbarlayout) self.setStyleSheet(BACKGROUNDCSS) try: # Try getting the VM list from oVirt vms_serv = conf.OVIRTCONN.vms_service() vmpools_serv = conf.OVIRTCONN.vm_pools_service() vms = sorted(vms_serv.list(), key=self.p22p3_compare_vms(self.compare_vms)) vmpools = sorted(vmpools_serv.list(), key=self.p22p3_compare_vms(self.compare_vms)) except Error: QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('unexpected_connection_drop')) quit() # Set the main widget height based on the number of VMs winheight = self.vm_based_resize(len(vms) + len(vmpools)) if len(vms) + len(vmpools) > 0: delta = int(100 / (len(vms) + len(vmpools))) else: delta = int(100) if vmpools: step = self.list_vmpools(1, delta, step, vmpools) if vms: self.list_vms(len(vmpools) + 1, delta, step, vms) # Once loading has concluded, progress bar is dismissed and the layout set to the QGridLayout self.pbar.hide() QObjectCleanupHandler().add(self.layout()) # First row is special: Number of VMs + Toolbar total_machines = QLabel( _('total_machines') + ': <font color="#AA8738">' + str(len(vms)) + '</font>, ' + _('total_vmpools') + ': <font color="#AA8738">' + str(len(vmpools)) + '</font>', self) self.grid.addWidget(total_machines, 0, 0, 1, 3, Qt.AlignCenter) self.generate_toolbar() # We wrap the main widget inside another widget with a vertical scrollbar wrapper = QWidget() wrapper.setLayout(self.grid) scroll = QScrollArea() scroll.setWidget(wrapper) scroll.setWidgetResizable(True) scroll.setFixedHeight(winheight) layout = QVBoxLayout() layout.addWidget(scroll) layout.setContentsMargins(0, 0, 0, 20) self.setLayout(layout) def update_status_icon(self, i, newstatus): """ Description: Invoked when the background thread emits the signal announcing a status change, so the corresponding VM status icon should be updated. Arguments: i: Row that has changed their status. The VM can be matched with VmData(). newstatus: The new status for the VM. Returns: Nothing """ imageSticon = self.make_button(newstatus, self.toggle_action_text(newstatus)) imageSticon.mousePressEvent = lambda x, r=i: self.change_status(r) self.grid.addWidget(imageSticon, i, 2) def logout_warn(self): """ Description: Called if the warn_autologout setting has been set in the config. It will warn user to reset their idle, otherwise an enforced logout will be performed by calling the logout() method. Arguments: None Returns: Nothing """ self.autologoutwarnwin = QMessageBox(None) self.autologoutwarnwin.setIcon(QMessageBox.Warning) self.autologoutwarnwin.setText(_('auto_logout_warn')) self.autologoutwarnwin.setWindowTitle(_('auto_logout_warn_title')) self.autologoutwarnwin.setStandardButtons(QMessageBox.Ok) self.autologoutwarnwin.buttonClicked.connect( self.autologoutwarn_accepted) self.autologoutwarnwin.exec_() def logout(self, reconnect=False): """ Description: Invoked when the autologout parameter is set in the config and the idle time is overreached. This should require authentication again. Arguments: None Returns: Nothing """ if conf.OVIRTCONN: try: conf.SOCKOBJ.close() except Error: pass self.stopThread = True conf.USERNAME = None self.autologoutWarn = False # Hide the layout so next user doesn't see the previous content self.hide() try: self.autologoutwarnwin.accept() except AttributeError: # Usually when the notify_autologout setting has not been set pass # This will be called upon autologout events if reconnect: creds = Credentials(None) creds.finished.connect(self.start_vmpane) creds.exec_() def autologoutwarn_accepted(self): """ Description: Callback issued when the user accepts the message box warning about an imminent autologout event. Arguments: None Returns: Nothing """ self.lastclick = int(time()) self.autologoutWarn = False # This will make the warning be shown next times as well def refresh_statuses(self): """ Description: Background thread that will look for VM status changes and send a signal to the main thread so the corresponding icons are updated. Also, if there's a change in the number of VMs that the user controls, the main Widgets will be reloaded. It'll also check the autologout setting & act accordingly. Arguments: None Returns: Nothing ("infinite" loop) """ global UPDATESLEEPINTERVAL autologout = False while 1 and not self.stopThread: if conf.OVIRTCONN: try: vms_service = conf.OVIRTCONN.vms_service() ovirt_num_machines = len(vms_service.list()) except Error: sys.exit('[ERROR] ' + _('unexpected_connection_drop')) if ovirt_num_machines != len(self.vmdata): # If the number of VMs has changed, we should reload the main widget self.reloadsignal.emit() else: for i in self.vmdata: vmid = self.vmdata[i].vmid vmstatus = self.vmdata[i].vmstatus try: ovirtvm = vms_service.list(search='id=%s' % (vmid))[0] except Error: sys.exit('[ERROR] ' + _('unexpected_connection_drop')) if ovirtvm: curstatus = ovirtvm.status.value if vmstatus != curstatus: # If there has been a status change, emit the signal to update icons self.vmdata[i].vmstatus = curstatus self.updatesignal.emit(i, curstatus) # If there is any currently open viewer, we'll reset the idle time so we don't close the session # while there still is any open session. if self.openviewer_vms: self.lastclick = int(time()) # Last click timestamp update # If the autologout warning has not been shown yet and it's configured, we do so if conf.CONFIG['autologout'] != 0 and conf.CONFIG['notify_autologout'] != 0 and not self.autologoutWarn and \ (int(time() - self.lastclick) >= (conf.CONFIG['autologout'] - conf.CONFIG['notify_autologout']) * 60): self.autologoutWarn = True self.warnlogoutsignal.emit() # If there's no credentials file and autologout is set, we check for the last # click and if surpassed, a logout will be performed. if conf.CONFIG['autologout'] != 0 and not isfile( conf.USERCREDSFILE): if (int(time()) - self.lastclick) >= ( conf.CONFIG['autologout'] * 60): self.stopThread = True autologout = True sleep(UPDATESLEEPINTERVAL) else: return if autologout: self.logoutsignal.emit(True) def restart_thread(self): """ Description: Simply starts or restarts the background thread Arguments: None Returns: Nothing """ self.stopThread = False self.lastclick = int(time()) self.thread = threading.Thread(target=self.refresh_statuses, args=()) self.thread.daemon = True # Daemonize thread self.thread.start() def start_vmpane(self): """ Description: This method will be called when the Credentials dialog is closed. This should happen on successful authentication. We should then load the main widget and fill it with the VMs that the user has permissions on. Additionally, a background thread will be started to check VM status changed so the main widget is updated should this happen. Arguments: None Returns: Nothing """ self.show() self.load_vms() self.center() self.restart_thread() def confirm_quit(self): """ Description: Asks for confirmation from the user's side to close the app. Arguments: None Returns: True if the user wants to close the app, False otherwise. """ global conf reply = QMessageBox.question(None, _('apptitle') + ': ' + _('confirm'), _('confirm_quit'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.logout(reconnect=False) return True else: return False def quit_button(self): """ Description: Triggered when the Exit button in the Toolbar is hit. Confirmation will be required. Arguments: None Returns: Nothing, exits if user confirms. """ self.lastclick = int(time()) # Last click timestamp update if self.confirm_quit(): quit() def closeEvent(self, event): """ Description: The red 'x'. If the user hits it, we'll ask for confirmation. Arguments: None Returns: Nothing, exits if users confirms. """ if self.confirm_quit(): event.accept() else: event.ignore() def initUI(self): """ Description: Sets the size of the widget, the window title, centers the window, connects signals to methods and opens the Credentials dialog if needed. Arguments: None Returns: Nothing. """ global conf, MAXWIDTH, MAXHEIGHT, IMGDIR, VERSION self.setFixedSize(MAXWIDTH, MAXHEIGHT) self.center() self.setWindowTitle(_('apptitle') + ' ' + VERSION) self.setWindowIcon(QIcon(IMGDIR + 'appicon.png')) self.show() self.updatesignal.connect(self.update_status_icon) self.logoutsignal.connect(self.logout) self.warnlogoutsignal.connect(self.logout_warn) self.reloadsignal.connect(self.load_vms) if not conf.USERNAME: creds = Credentials(self) creds.finished.connect(self.start_vmpane) creds.exec_() def center(self): """ Description: Just centers the window Arguments: None Returns: Nothing """ qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft())
class HWdevice(QObject): # signal: sig1 (thread) is done - emitted by signMessageFinish sig1done = pyqtSignal(str) # signal: sigtx (thread) is done - emitted by signTxFinish sigTxdone = pyqtSignal(bytearray, str) # 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(str) def __init__(self, *args, **kwargs): QObject.__init__(self, *args, **kwargs) # Device Lock for threads self.lock = threading.Lock() printDbg("Creating HW device class") self.initDevice() # Connect signal self.sig_progress.connect(self.updateSigProgress) @process_ledger_exceptions def initDevice(self): try: self.lock.acquire() self.status = 0 if hasattr(self, 'dongle'): self.dongle.close() self.dongle = getDongle(False) printOK('Ledger Nano S drivers found') self.chip = btchip(self.dongle) printDbg("Ledger Initialized") ver = self.chip.getFirmwareVersion() printOK("Ledger HW device connected [v. %s]" % str(ver.get('version'))) self.status = 2 except Exception as e: if hasattr(self, 'dongle'): self.status = 1 self.dongle.close() finally: self.lock.release() # Status codes: # 0 - not connected # 1 - not in pivx app # 2 - fine def getStatus(self): messages = { 0: 'Unable to connect to the device.', 1: 'Unable to connect to the device. Please check that the PIVX app on the device is open, and try again.', 2: 'Hardware device connected.'} return self.status, messages[self.status] @process_ledger_exceptions def prepare_transfer_tx(self, caller, bip32_path, utxos_to_spend, dest_address, tx_fee, rawtransactions, useSwiftX=False): # For each UTXO create a Ledger 'trusted input' self.trusted_inputs = [] # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) self.arg_inputs = [] self.amount = 0 self.lock.acquire() try: for idx, utxo in enumerate(utxos_to_spend): self.amount += int(utxo['value']) raw_tx = bytearray.fromhex(rawtransactions[utxo['tx_hash']]) if not raw_tx: raise Exception("Can't find raw transaction for txid: " + rawtransactions[utxo['tx_hash']]) # parse the raw transaction, so that we can extract the UTXO locking script we refer to prev_transaction = bitcoinTransaction(raw_tx) utxo_tx_index = utxo['tx_ouput_n'] if utxo_tx_index < 0 or utxo_tx_index > len(prev_transaction.outputs): raise Exception('Incorrect value of outputIndex for UTXO %s' % str(idx)) trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) # completion percent emitted completion = int(45*idx / len(utxos_to_spend)) self.tx_progress.emit(completion) self.trusted_inputs.append(trusted_input) # Hash check curr_pubkey = compress_public_key(self.chip.getWalletPublicKey(bip32_path)['publicKey']) pubkey_hash = bin_hash160(curr_pubkey) pubkey_hash_from_script = extract_pkh_from_locking_script(prev_transaction.outputs[utxo_tx_index].script) if pubkey_hash != pubkey_hash_from_script: text = "Error: The hashes for the public key for the BIP32 path, 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() printDbg(text) self.arg_inputs.append({ 'locking_script': prev_transaction.outputs[utxo['tx_ouput_n']].script, 'pubkey': curr_pubkey, 'bip32_path': bip32_path, 'outputIndex': utxo['tx_ouput_n'], 'txid': utxo['tx_hash'] }) # completion percent emitted completion = int(95*idx / len(utxos_to_spend)) self.tx_progress.emit(completion) self.amount -= int(tx_fee) self.amount = int(self.amount) arg_outputs = [{'address': dest_address, 'valueSat': self.amount}] # there will be multiple outputs soon self.new_transaction = bitcoinTransaction() # new transaction object to be used for serialization at the last stage self.new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) # completion percent emitted self.tx_progress.emit(99) finally: self.lock.release() try: for o in arg_outputs: output = bitcoinOutput() output.script = compose_tx_locking_script(o['address']) output.amount = int.to_bytes(o['valueSat'], 8, byteorder='little') self.new_transaction.outputs.append(output) except Exception: raise # completion percent emitted self.tx_progress.emit(100) # join all outputs - will be used by Ledger for signing transaction self.all_outputs_raw = self.new_transaction.serializeOutputs() self.mBox2 = QMessageBox(caller) self.messageText = "<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)) if useSwiftX: self.messageText += "<p>Fees (SwiftX flat rate):<br><b>%s</b> PIV<p>" % str(round(int(tx_fee) / 1e8, 8)) else: self.messageText += "<p>Fees:<br><b>%s</b> PIV<p>" % str(round(int(tx_fee) / 1e8, 8)) messageText = self.messageText + "Signature Progress: 0 %" self.mBox2.setText(messageText) self.mBox2.setText(messageText) self.mBox2.setIconPixmap(caller.tabMain.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) self.mBox2.setWindowTitle("CHECK YOUR LEDGER") self.mBox2.setStandardButtons(QMessageBox.NoButton) self.mBox2.setMaximumWidth(500) self.mBox2.show() ThreadFuns.runInThread(self.signTxSign, (), self.signTxFinish) @process_ledger_exceptions def prepare_transfer_tx_bulk(self, caller, mnodes, dest_address, tx_fee, rawtransactions, useSwiftX=False): # For each UTXO create a Ledger 'trusted input' self.trusted_inputs = [] # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) self.arg_inputs = [] self.amount = 0 self.lock.acquire() num_of_sigs = sum([len(mnode['utxos']) for mnode in mnodes]) curr_utxo_checked = 0 try: for i, mnode in enumerate(mnodes): for idx, utxo in enumerate(mnode['utxos']): self.amount += int(utxo['value']) raw_tx = bytearray.fromhex(rawtransactions[utxo['tx_hash']]) if not raw_tx: raise Exception("Can't find raw transaction for txid: " + rawtransactions[utxo['tx_hash']]) # parse the raw transaction, so that we can extract the UTXO locking script we refer to prev_transaction = bitcoinTransaction(raw_tx) utxo_tx_index = utxo['tx_ouput_n'] if utxo_tx_index < 0 or utxo_tx_index > len(prev_transaction.outputs): raise Exception('Incorrect value of outputIndex for UTXO %s' % str(idx)) trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) self.trusted_inputs.append(trusted_input) # Hash check curr_pubkey = compress_public_key(self.chip.getWalletPublicKey(mnode['path'])['publicKey']) pubkey_hash = bin_hash160(curr_pubkey) pubkey_hash_from_script = extract_pkh_from_locking_script(prev_transaction.outputs[utxo_tx_index].script) if pubkey_hash != pubkey_hash_from_script: text = "Error: The hashes for the public key for the BIP32 path, 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() printDbg(text) self.arg_inputs.append({ 'locking_script': prev_transaction.outputs[utxo['tx_ouput_n']].script, 'pubkey': curr_pubkey, 'bip32_path': mnode['path'], 'outputIndex': utxo['tx_ouput_n'], 'txid': utxo['tx_hash'] }) # completion percent emitted curr_utxo_checked += 1 completion = int(95*curr_utxo_checked / num_of_sigs) self.tx_progress.emit(completion) self.amount -= int(tx_fee) self.amount = int(self.amount) arg_outputs = [{'address': dest_address, 'valueSat': self.amount}] # there will be multiple outputs soon self.new_transaction = bitcoinTransaction() # new transaction object to be used for serialization at the last stage self.new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) self.tx_progress.emit(99) except Exception: raise finally: self.lock.release() try: for o in arg_outputs: output = bitcoinOutput() output.script = compose_tx_locking_script(o['address']) output.amount = int.to_bytes(o['valueSat'], 8, byteorder='little') self.new_transaction.outputs.append(output) except Exception: raise self.tx_progress.emit(100) # join all outputs - will be used by Ledger for signing transaction self.all_outputs_raw = self.new_transaction.serializeOutputs() self.mBox2 = QMessageBox(caller) self.messageText = "<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)) if useSwiftX: self.messageText += "<p>Fees (SwiftX flat rate):<br><b>%s</b> PIV<p>" % str(round(int(tx_fee) / 1e8, 8)) else: self.messageText += "<p>Fees:<br><b>%s</b> PIV<p>" % str(round(int(tx_fee) / 1e8, 8)) messageText = self.messageText + "Signature Progress: 0 %" self.mBox2.setText(messageText) self.mBox2.setIconPixmap(caller.tabMain.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) self.mBox2.setWindowTitle("CHECK YOUR LEDGER") self.mBox2.setStandardButtons(QMessageBox.NoButton) self.mBox2.setMaximumWidth(500) self.mBox2.show() ThreadFuns.runInThread(self.signTxSign, (), self.signTxFinish) def scanForAddress(self, account, spath, isTestnet=False): printOK("Scanning for Address n. %d on account n. %d" % (spath, account)) curr_path = MPATH + "%d'/0/%d" % (account, spath) self.lock.acquire() try: if not isTestnet: curr_addr = self.chip.getWalletPublicKey(curr_path).get('address')[12:-2] else: pubkey = compress_public_key(self.chip.getWalletPublicKey(curr_path).get('publicKey')).hex() curr_addr = pubkey_to_address(pubkey, isTestnet) except Exception as e: err_msg = 'error in scanForAddress' printException(getCallerName(), getFunctionName(), err_msg, e.args) return None finally: self.lock.release() return curr_addr def scanForBip32(self, account, address, starting_spath=0, spath_count=10, isTestnet=False): found = False spath = -1 printOK("Scanning for Bip32 path of address: %s" % address) for i in range(starting_spath, starting_spath+spath_count): curr_path = MPATH + "%d'/0/%d" % (account, i) printDbg("checking path... %s" % curr_path) self.lock.acquire() try: if not isTestnet: curr_addr = self.chip.getWalletPublicKey(curr_path).get('address')[12:-2] else: pubkey = compress_public_key(self.chip.getWalletPublicKey(curr_path).get('publicKey')).hex() curr_addr = pubkey_to_address(pubkey, isTestnet) if curr_addr == address: found = True spath = i break sleep(0.01) except Exception as e: err_msg = 'error in scanForBip32' printException(getCallerName(), getFunctionName(), err_msg, e.args) finally: self.lock.release() return (found, spath) def scanForPubKey(self, account, spath): self.lock.acquire() printOK("Scanning for PubKey of address n. %d on account n. %d" % (spath, account)) curr_path = MPATH + "%d'/0/%d" % (account, spath) try: nodeData = self.chip.getWalletPublicKey(curr_path) except Exception as e: err_msg = 'error in scanForPubKey' printException(getCallerName(), getFunctionName(), err_msg, e.args) return None finally: self.lock.release() return compress_public_key(nodeData.get('publicKey')).hex() @process_ledger_exceptions def signMess(self, caller, path, message): # Ledger doesn't accept characters other that ascii printable: # https://ledgerhq.github.io/btchip-doc/bitcoin-technical.html#_sign_message message = message.encode('ascii', 'ignore') message_sha = splitString(single_sha256(message).hex(),32); # Connection pop-up mBox = QMessageBox(caller.ui) warningText = "Another application (such as Ledger Wallet app) has probably taken over " warningText += "the communication with the Ledger device.<br><br>To continue, close that application and " warningText += "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); # Ask confirmation self.lock.acquire() info = self.chip.signMessagePrepare(path, message) self.lock.release() while info['confirmationNeeded'] and info['confirmationType'] == 34: ans = mBox.exec_() if ans == QMessageBox.Abort: raise Exception("Reconnect HW device") # we need to reconnect the device self.dongle.close() self.initDevice() self.lock.acquire() info = self.chip.signMessagePrepare(path, message) self.lock.release() printOK('Signing Message') self.mBox = QMessageBox(caller.ui) messageText = "Check display of your hardware device\n\n" + "- masternode message hash:\n\n%s\n\n-path:\t%s\n" % (message_sha, path) self.mBox.setText(messageText) self.mBox.setIconPixmap(caller.ui.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) self.mBox.setWindowTitle("CHECK YOUR LEDGER") self.mBox.setStandardButtons(QMessageBox.NoButton) self.mBox.show() # Sign message ThreadFuns.runInThread(self.signMessageSign, (), self.signMessageFinish) @process_ledger_exceptions def signMessageSign(self, ctrl): self.lock.acquire() try: self.signature = self.chip.signMessageSign() except: self.signature = None finally: self.lock.release() def signMessageFinish(self): self.mBox.accept() if self.signature != None: if len(self.signature) > 4: rLength = self.signature[3] r = self.signature[4 : 4 + rLength] if len(self.signature) > 4 + rLength + 1: sLength = self.signature[4 + rLength + 1] if len(self.signature) > 4 + rLength + 2: s = self.signature[4 + rLength + 2:] if rLength == 33: r = r[1:] if sLength == 33: s = s[1:] work = bytes(chr(27 + 4 + (self.signature[0] & 0x01)), "utf-8") + r + s printOK("Message signed") sig1 = work.hex() else: printDbg('client.signMessageSign() returned invalid response (code 3): ' + self.signature.hex()) sig1 = "None" else: printDbg('client.signMessageSign() returned invalid response (code 2): ' + self.signature.hex()) sig1 = "None" else: printDbg('client.signMessageSign() returned invalid response (code 1): ' + self.signature.hex()) sig1 = "None" else: printOK("Signature refused by the user") sig1 = "None" self.sig1done.emit(sig1) def signTxSign(self, ctrl): self.lock.acquire() try: starting = True curr_input_signed = 0 # sign all inputs on Ledger and add inputs in the self.new_transaction object for serialization for idx, new_input in enumerate(self.arg_inputs): self.chip.startUntrustedTransaction(starting, idx, self.trusted_inputs, new_input['locking_script']) self.chip.finalizeInputFull(self.all_outputs_raw) sig = self.chip.untrustedHashSign(new_input['bip32_path'], lockTime=0) new_input['signature'] = sig inputTx = bitcoinInput() inputTx.prevOut = bytearray.fromhex(new_input['txid'])[::-1] + int.to_bytes(new_input['outputIndex'], 4, byteorder='little') inputTx.script = bytearray([len(sig)]) + sig + bytearray([0x21]) + new_input['pubkey'] inputTx.sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) self.new_transaction.inputs.append(inputTx) starting = False # signature percent emitted curr_input_signed += 1 completion = int(100*curr_input_signed / len(self.arg_inputs)) self.sig_progress.emit(str(completion)) self.new_transaction.lockTime = bytearray([0, 0, 0, 0]) self.tx_raw = bytearray(self.new_transaction.serialize()) self.sig_progress.emit("100") except Exception as e: if e.sw != 0x6985: self.status = 0 printException(getCallerName(), getFunctionName(), e.message, e.args) self.tx_raw = None finally: self.lock.release() if self.status == 0: self.dongle.close() self.initDevice() def signTxFinish(self): self.mBox2.accept() try: 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() except Exception as e: printDbg(e) def updateSigProgress(self, text): messageText = self.messageText + "Signature Progress: <b style='color:red'>" + text + " %</b>" self.mBox2.setText(messageText) QApplication.processEvents()
class HWdevice(QObject): # signal: sig1 (thread) is done - emitted by signMessageFinish sig1done = pyqtSignal(str) # signal: sigtx (thread) is done - emitted by signTxFinish sigTxdone = pyqtSignal(bytearray, str) def __init__(self, *args, **kwargs): QObject.__init__(self, *args, **kwargs) # Device Lock for threads printDbg("Creating HW device class") self.initDevice() @process_ledger_exceptions def initDevice(self): try: if hasattr(self, 'dongle'): self.dongle.close() self.dongle = getDongle(False) printOK('Ledger Nano S drivers found') self.chip = btchip(self.dongle) printDbg("Ledger Initialized") self.initialized = True ver = self.chip.getFirmwareVersion() printOK("Ledger HW device connected [v. %s]" % str(ver.get('version'))) except Exception as e: err_msg = 'error Initializing Ledger' printException(getCallerName(), getFunctionName(), err_msg, e.args) self.initialized = False if hasattr(self, 'dongle'): self.dongle.close() # Status codes: # 0 - not connected # 1 - not in pivx app # 2 - fine @process_ledger_exceptions def getStatusCode(self): try: if self.initialized: if not self.checkApp(): statusCode = 1 else: statusCode = 2 else: statusCode = 0 except Exception as e: err_msg = 'error in getStatusCode' printException(getCallerName(), getFunctionName(), err_msg, e.args) statusCode = 0 return statusCode @process_ledger_exceptions def getStatusMess(self, statusCode=None): if statusCode == None or not statusCode in [0, 1, 2]: statusCode = self.getStatusCode() messages = { 0: 'Unable to connect to the device', 1: 'Open PIVX app on Ledger device', 2: 'HW DEVICE CONNECTED!' } return messages[statusCode] @process_ledger_exceptions def checkApp(self): printDbg("Checking app") try: firstAddress = self.chip.getWalletPublicKey(MPATH + "0'/0/0").get( 'address')[12:-2] if firstAddress[0] == 'D': printOK("found PIVX app on ledger device") return True except Exception as e: err_msg = 'error in checkApp' printException(getCallerName(), getFunctionName(), err_msg, e.args) return False @process_ledger_exceptions def prepare_transfer_tx(self, caller, bip32_path, utxos_to_spend, dest_address, tx_fee, rawtransactions): # For each UTXO create a Ledger 'trusted input' self.trusted_inputs = [] # https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand) self.arg_inputs = [] self.amount = 0 for idx, utxo in enumerate(utxos_to_spend): self.amount += int(utxo['value']) raw_tx = bytearray.fromhex(rawtransactions[utxo['tx_hash']]) if not raw_tx: raise Exception("Can't find raw transaction for txid: " + rawtransactions[utxo['tx_hash']]) # parse the raw transaction, so that we can extract the UTXO locking script we refer to prev_transaction = bitcoinTransaction(raw_tx) utxo_tx_index = utxo['tx_ouput_n'] if utxo_tx_index < 0 or utxo_tx_index > len( prev_transaction.outputs): raise Exception('Incorrect value of outputIndex for UTXO %s' % str(idx)) trusted_input = self.chip.getTrustedInput(prev_transaction, utxo_tx_index) self.trusted_inputs.append(trusted_input) # Hash check curr_pubkey = compress_public_key( self.chip.getWalletPublicKey(bip32_path)['publicKey']) pubkey_hash = bin_hash160(curr_pubkey) pubkey_hash_from_script = extract_pkh_from_locking_script( prev_transaction.outputs[utxo_tx_index].script) if pubkey_hash != pubkey_hash_from_script: text = "Error: different public key hashes for the BIP32 path and the UTXO" text += "locking script. Your signed transaction will not be validated by the network.\n" text += "pubkey_hash: %s\n" % str(pubkey_hash) text += "pubkey_hash_from_script: %s\n" % str( pubkey_hash_from_script) printDbg(text) self.arg_inputs.append({ 'locking_script': prev_transaction.outputs[utxo['tx_ouput_n']].script, 'pubkey': curr_pubkey, 'bip32_path': bip32_path, 'outputIndex': utxo['tx_ouput_n'], 'txid': utxo['tx_hash'] }) self.amount -= int(tx_fee) self.amount = int(self.amount) arg_outputs = [{ 'address': dest_address, 'valueSat': self.amount }] # there will be multiple outputs soon self.new_transaction = bitcoinTransaction( ) # new transaction object to be used for serialization at the last stage self.new_transaction.version = bytearray([0x01, 0x00, 0x00, 0x00]) try: for o in arg_outputs: output = bitcoinOutput() output.script = compose_tx_locking_script(o['address']) output.amount = int.to_bytes(o['valueSat'], 8, byteorder='little') self.new_transaction.outputs.append(output) except Exception: raise # join all outputs - will be used by Ledger for signing transaction self.all_outputs_raw = self.new_transaction.serializeOutputs() self.mBox2 = QMessageBox(caller) messageText = "Check display of your hardware device<br><br><br>" messageText += "From bip32_path: <b>%s</b><br><br>" % str(bip32_path) messageText += "To PIVX Address: <b>%s</b><br><br>" % dest_address messageText += "PIV amount: <b>%s</b><br>" % str( round(self.amount / 1e8, 8)) messageText += "plus PIV for fee: <b>%s</b><br><br>" % str( round(int(tx_fee) / 1e8, 8)) self.mBox2.setText(messageText) self.mBox2.setIconPixmap( caller.tabMain.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) self.mBox2.setWindowTitle("CHECK YOUR LEDGER") self.mBox2.setStandardButtons(QMessageBox.NoButton) self.mBox2.setMaximumWidth(500) self.mBox2.show() ThreadFuns.runInThread(self.signTxSign, (), self.signTxFinish) @process_ledger_exceptions def scanForAddress(self, account, spath, isTestnet=False): printOK("Scanning for Address of path_id %s on account n° %s" % (str(spath), str(account))) curr_path = MPATH + "%d'/0/%d" % (account, spath) try: if not isTestnet: curr_addr = self.chip.getWalletPublicKey(curr_path).get( 'address')[12:-2] else: pubkey = compress_public_key( self.chip.getWalletPublicKey(curr_path).get( 'publicKey')).hex() curr_addr = pubkey_to_address(pubkey, isTestnet) except Exception as e: err_msg = 'error in scanForAddress' printException(getCallerName(), getFunctionName(), err_msg, e.args) return None return curr_addr @process_ledger_exceptions def scanForBip32(self, account, address, starting_spath=0, spath_count=10, isTestnet=False): found = False spath = -1 printOK("Scanning for Bip32 path of address: %s" % address) for i in range(starting_spath, starting_spath + spath_count): curr_path = MPATH + "%d'/0/%d" % (account, i) printDbg("checking path... %s" % curr_path) try: if not isTestnet: curr_addr = self.chip.getWalletPublicKey(curr_path).get( 'address')[12:-2] else: pubkey = compress_public_key( self.chip.getWalletPublicKey(curr_path).get( 'publicKey')).hex() curr_addr = pubkey_to_address(pubkey, isTestnet) if curr_addr == address: found = True spath = i break sleep(0.01) except Exception as e: err_msg = 'error in scanForBip32' printException(getCallerName(), getFunctionName(), err_msg, e.args) return (found, spath) @process_ledger_exceptions def scanForPubKey(self, account, spath): printOK("Scanning for Address of path_id %s on account n° %s" % (str(spath), str(account))) curr_path = MPATH + "%d'/0/%d" % (account, spath) try: nodeData = self.chip.getWalletPublicKey(curr_path) except Exception as e: err_msg = 'error in scanForPubKey' printException(getCallerName(), getFunctionName(), err_msg, e.args) return None return compress_public_key(nodeData.get('publicKey')).hex() @process_ledger_exceptions def signMess(self, caller, path, message): # Ledger doesn't accept characters other that ascii printable: # https://ledgerhq.github.io/btchip-doc/bitcoin-technical.html#_sign_message message = message.encode('ascii', 'ignore') message_sha = splitString(single_sha256(message).hex(), 32) # Connection pop-up mBox = QMessageBox(caller.ui) warningText = "Another application (such as Ledger Wallet app) has probably taken over " warningText += "the communication with the Ledger device.<br><br>To continue, close that application and " warningText += "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) # Ask confirmation info = self.chip.signMessagePrepare(path, message) while info['confirmationNeeded'] and info['confirmationType'] == 34: ans = mBox.exec_() # we need to reconnect the device self.dongle.close() self.initialized = False self.initDevice() if ans == QMessageBox.Abort: raise Exception('Message Signature failed') info = self.chip.signMessagePrepare(path, message) printOK('Signing Message') self.mBox = QMessageBox(caller.ui) messageText = "Check display of your hardware device\n\n" + "- masternode message hash:\n\n%s\n\n-path:\t%s\n" % ( message_sha, path) self.mBox.setText(messageText) self.mBox.setIconPixmap( caller.ui.ledgerImg.scaledToHeight(200, Qt.SmoothTransformation)) self.mBox.setWindowTitle("CHECK YOUR LEDGER") self.mBox.setStandardButtons(QMessageBox.NoButton) self.mBox.show() # Sign message ThreadFuns.runInThread(self.signMessageSign, (), self.signMessageFinish) @process_ledger_exceptions def signMessageSign(self, ctrl): try: self.signature = self.chip.signMessageSign() except: self.signature = None @process_ledger_exceptions def signMessageFinish(self): self.mBox.accept() if self.signature != None: if len(self.signature) > 4: rLength = self.signature[3] r = self.signature[4:4 + rLength] if len(self.signature) > 4 + rLength + 1: sLength = self.signature[4 + rLength + 1] if len(self.signature) > 4 + rLength + 2: s = self.signature[4 + rLength + 2:] if rLength == 33: r = r[1:] if sLength == 33: s = s[1:] work = bytes(chr(27 + 4 + (self.signature[0] & 0x01)), "utf-8") + r + s printOK("Message signed") sig1 = work.hex() else: printDbg( 'client.signMessageSign() returned invalid response (code 3): ' + self.signature.hex()) sig1 = "None" else: printDbg( 'client.signMessageSign() returned invalid response (code 2): ' + self.signature.hex()) sig1 = "None" else: printDbg( 'client.signMessageSign() returned invalid response (code 1): ' + self.signature.hex()) sig1 = "None" else: printOK("Signature refused by the user") sig1 = "None" self.sig1done.emit(sig1) @process_ledger_exceptions def signTxSign(self, ctrl): try: starting = True # sign all inputs on Ledger and add inputs in the self.new_transaction object for serialization for idx, new_input in enumerate(self.arg_inputs): self.chip.startUntrustedTransaction( starting, idx, self.trusted_inputs, new_input['locking_script']) self.chip.finalizeInputFull(self.all_outputs_raw) sig = self.chip.untrustedHashSign(new_input['bip32_path'], lockTime=0) new_input['signature'] = sig inputTx = bitcoinInput() inputTx.prevOut = bytearray.fromhex( new_input['txid'])[::-1] + int.to_bytes( new_input['outputIndex'], 4, byteorder='little') inputTx.script = bytearray([len(sig)]) + sig + bytearray( [0x21]) + new_input['pubkey'] inputTx.sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) self.new_transaction.inputs.append(inputTx) starting = False self.new_transaction.lockTime = bytearray([0, 0, 0, 0]) self.tx_raw = bytearray(self.new_transaction.serialize()) except Exception as e: printException(getCallerName(), getFunctionName(), "Signature Exception", e.args) self.tx_raw = None @process_ledger_exceptions def signTxFinish(self): self.mBox2.accept() try: if self.tx_raw is not None: # Signal to be catched by FinishSend on TabRewards self.sigTxdone.emit(self.tx_raw, str(round(self.amount / 1e8, 8))) else: printOK("Transaction refused by the user") except Exception as e: printDbg(e)