def export_to_file(self, *, tx: Transaction = None): if tx is None: tx = self.tx if isinstance(tx, PartialTransaction): tx.finalize_psbt() if tx.is_complete(): name = 'signed_%s.txn' % (tx.txid()[0:8]) else: name = self.wallet.basename() + time.strftime('-%Y%m%d-%H%M.psbt') fileName = self.main_window.getSaveFileName( _("Select where to save your signed transaction"), name, TRANSACTION_FILE_EXTENSION_FILTER) if not fileName: return if tx.is_complete(): # network tx hex with open(fileName, "w+") as f: network_tx_hex = tx.serialize_to_network() f.write(network_tx_hex + '\n') else: # if partial: PSBT bytes assert isinstance(tx, PartialTransaction) with open(fileName, "wb+") as f: f.write(tx.serialize_as_bytes()) self.show_message(_("Transaction exported successfully")) self.saved = True
def export_to_file(self, *, tx: Transaction = None): if tx is None: tx = self.tx if isinstance(tx, PartialTransaction): tx.finalize_psbt() if tx.is_complete(): name = 'signed_%s' % (tx.txid()[0:8]) extension = 'txn' default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX else: name = self.wallet.basename() + time.strftime('-%Y%m%d-%H%M') extension = 'psbt' default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX name = f'{name}.{extension}' fileName = self.main_window.getSaveFileName( _("Select where to save your transaction"), name, TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, default_extension=extension, default_filter=default_filter) if not fileName: return if tx.is_complete(): # network tx hex with open(fileName, "w+") as f: network_tx_hex = tx.serialize_to_network() f.write(network_tx_hex + '\n') else: # if partial: PSBT bytes assert isinstance(tx, PartialTransaction) with open(fileName, "wb+") as f: f.write(tx.serialize_as_bytes()) self.show_message(_("Transaction exported successfully")) self.saved = True
def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool: if not isinstance(tx, PartialTransaction): return False if tx.is_complete(): return False # TODO this is broken currently as it assumes tx.xpubs return cosigner_xpub in {bip32node.to_xpub() for bip32node in tx.xpubs}
def sign_transaction(self, tx: Transaction, password): # Build a PSBT in memory, upload it for signing. # - we can also work offline (without paired device present) if tx.is_complete(): return assert self.my_wallet, "Not clear which wallet associated with this Coldcard" client = self.get_client() assert client.dev.master_fingerprint == self.ckcc_xfp # makes PSBT required raw_psbt = build_psbt(tx, self.my_wallet) cc_finalize = not (type(self.my_wallet) is Multisig_Wallet) try: try: self.handler.show_message("Authorize Transaction...") client.sign_transaction_start(raw_psbt, cc_finalize) while 1: # How to kill some time, without locking UI? time.sleep(0.250) resp = client.sign_transaction_poll() if resp is not None: break rlen, rsha = resp # download the resulting txn. raw_resp = client.download_file(rlen, rsha) finally: self.handler.finished() except (CCUserRefused, CCBusyError) as exc: self.logger.info(f'Did not sign: {exc}') self.handler.show_error(str(exc)) return except BaseException as e: self.logger.exception('') self.give_error(e, True) return if cc_finalize: # We trust the coldcard to re-serialize final transaction ready to go tx.update(bh2u(raw_resp)) else: # apply partial signatures back into txn psbt = BasicPSBT() psbt.parse(raw_resp, client.label()) merge_sigs_from_psbt(tx, psbt)
def export_to_file(self, *, tx: Transaction = None): if tx is None: tx = self.tx if isinstance(tx, PartialTransaction): tx.finalize_psbt() txid = tx.txid() suffix = txid[0:8] if txid is not None else time.strftime( '%Y%m%d-%H%M') if tx.is_complete(): extension = 'txn' default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX else: extension = 'psbt' default_filter = TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX name = f'{self.wallet.basename()}-{suffix}.{extension}' fileName = getSaveFileName( parent=self, title=_("Select where to save your transaction"), filename=name, filter=TRANSACTION_FILE_EXTENSION_FILTER_SEPARATE, default_extension=extension, default_filter=default_filter, config=self.config, ) if not fileName: return if tx.is_complete(): # network tx hex with open(fileName, "w+") as f: network_tx_hex = tx.serialize_to_network() f.write(network_tx_hex + '\n') else: # if partial: PSBT bytes assert isinstance(tx, PartialTransaction) with open(fileName, "wb+") as f: f.write(tx.serialize_as_bytes()) self.show_message(_("Transaction exported successfully")) self.saved = True
def __init__(self, tx: Transaction, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' # We want to be a top-level window QDialog.__init__(self, parent=None) # Take a copy; it might get updated in the main window by # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. self.tx = tx = copy.deepcopy(tx) try: self.tx.deserialize() except BaseException as e: raise SerializationError(e) self.main_window = parent self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved self.saved = False self.desc = desc self.invoice = invoice # if the wallet can populate the inputs with more info, do it now. # as a result, e.g. we might learn an imported address tx is segwit, # in which case it's ok to display txid tx.add_inputs_info(self.wallet) self.setMinimumWidth(950) self.setWindowTitle(_("Transaction")) vbox = QVBoxLayout() self.setLayout(vbox) vbox.addWidget(QLabel(_("Transaction ID:"))) self.tx_hash_e = ButtonsLineEdit() qr_show = lambda: parent.show_qrcode( str(self.tx_hash_e.text()), 'Transaction ID', parent=self) qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" self.tx_hash_e.addButton(qr_icon, qr_show, _("Show as QR code")) self.tx_hash_e.setReadOnly(True) vbox.addWidget(self.tx_hash_e) self.add_tx_stats(vbox) vbox.addSpacing(10) self.add_io(vbox) self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) self.broadcast_button = b = QPushButton(_("Broadcast")) b.clicked.connect(self.do_broadcast) self.save_button = b = QPushButton(_("Save")) save_button_disabled = not tx.is_complete() b.setDisabled(save_button_disabled) if save_button_disabled: b.setToolTip(SAVE_BUTTON_DISABLED_TOOLTIP) else: b.setToolTip(SAVE_BUTTON_ENABLED_TOOLTIP) b.clicked.connect(self.save) self.export_button = b = QPushButton(_("Export")) b.clicked.connect(self.export) self.cancel_button = b = QPushButton(_("Close")) b.clicked.connect(self.close) b.setDefault(True) self.qr_button = b = QPushButton() b.setIcon(read_QIcon(qr_icon)) b.clicked.connect(self.show_qr) self.copy_button = CopyButton(lambda: str(self.tx), parent.app) # Action buttons self.buttons = [ self.sign_button, self.broadcast_button, self.cancel_button ] # Transaction sharing buttons self.sharing_buttons = [ self.copy_button, self.qr_button, self.export_button, self.save_button ] run_hook('transaction_dialog', self) hbox = QHBoxLayout() hbox.addLayout(Buttons(*self.sharing_buttons)) hbox.addStretch(1) hbox.addLayout(Buttons(*self.buttons)) vbox.addLayout(hbox) self.update()
def sign_transaction(self, tx: Transaction, password): if tx.is_complete(): return client = self.get_client() inputs = [] inputsPaths = [] pubKeys = [] chipInputs = [] redeemScripts = [] signatures = [] changePath = "" output = None p2shTransaction = False segwitTransaction = False pin = "" self.get_client() # prompt for the PIN before displaying the dialog if necessary # Fetch inputs of the transaction to sign derivations = self.get_tx_derivations(tx) for txin in tx.inputs(): if txin['type'] == 'coinbase': self.give_error("Coinbase not supported") # should never happen if txin['type'] in ['p2sh']: p2shTransaction = True if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: if not self.get_client_electrum().supports_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True if txin['type'] in ['p2wpkh', 'p2wsh']: if not self.get_client_electrum().supports_native_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) for i, x_pubkey in enumerate(x_pubkeys): if x_pubkey in derivations: signingPos = i s = derivations.get(x_pubkey) hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) break else: self.give_error("No matching x_key for sign_transaction") # should never happen redeemScript = Transaction.get_preimage_script(txin) txin_prev_tx = txin.get('prev_tx') if txin_prev_tx is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None inputs.append([txin_prev_tx_raw, txin['prevout_n'], redeemScript, txin['prevout_hash'], signingPos, txin.get('sequence', 0xffffffff - 1), txin.get('value')]) inputsPaths.append(hwAddress) pubKeys.append(pubkeys) # Sanity check if p2shTransaction: for txin in tx.inputs(): if txin['type'] != 'p2sh': self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen txOutput = var_int(len(tx.outputs())) for o in tx.outputs(): output_type, addr, amount = o.type, o.address, o.value txOutput += int_to_hex(amount, 8) script = tx.pay_script(output_type, addr) txOutput += var_int(len(script)//2) txOutput += script txOutput = bfh(txOutput) # Recognize outputs # - only one output and one change is authorized (for hw.1 and nano) # - at most one output can bypass confirmation (~change) (for all) if not p2shTransaction: if not self.get_client_electrum().supports_multi_output(): if len(tx.outputs()) > 2: self.give_error("Transaction with more than 2 outputs not supported") has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) for o in tx.outputs(): assert o.type == TYPE_ADDRESS info = tx.output_info.get(o.address) if (info is not None) and len(tx.outputs()) > 1 \ and not has_change: index = info.address_index # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed if info.is_change == any_output_on_change_branch: changePath = self.get_derivation()[2:] + "/%d/%d"%index has_change = True else: output = o.address else: output = o.address self.handler.show_message(_("Confirm Transaction on your Ledger device...")) try: # Get trusted inputs from the original transactions for utxo in inputs: sequence = int_to_hex(utxo[5], 4) if segwitTransaction: tmp = bfh(utxo[3])[::-1] tmp += bfh(int_to_hex(utxo[1], 4)) tmp += bfh(int_to_hex(utxo[6], 8)) # txin['value'] chipInputs.append({'value' : tmp, 'witness' : True, 'sequence' : sequence}) redeemScripts.append(bfh(utxo[2])) elif not p2shTransaction: txtmp = bitcoinTransaction(bfh(utxo[0])) trustedInput = self.get_client().getTrustedInput(txtmp, utxo[1]) trustedInput['sequence'] = sequence chipInputs.append(trustedInput) redeemScripts.append(txtmp.outputs[utxo[1]].script) else: tmp = bfh(utxo[3])[::-1] tmp += bfh(int_to_hex(utxo[1], 4)) chipInputs.append({'value' : tmp, 'sequence' : sequence}) redeemScripts.append(bfh(utxo[2])) # Sign all inputs firstTransaction = True inputIndex = 0 rawTx = tx.serialize_to_network() self.get_client().enableAlternate2fa(False) if segwitTransaction: self.get_client().startUntrustedTransaction(True, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) outputData['outputData'] = txOutput if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin if not pin: raise UserWarning() self.handler.show_message(_("Confirmed. Signing Transaction...")) while inputIndex < len(inputs): singleInput = [ chipInputs[inputIndex] ] self.get_client().startUntrustedTransaction(False, 0, singleInput, redeemScripts[inputIndex], version=tx.version) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 else: while inputIndex < len(inputs): self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, chipInputs, redeemScripts[inputIndex], version=tx.version) # we don't set meaningful outputAddress, amount and fees # as we only care about the alternateEncoding==True branch outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx)) outputData['outputData'] = txOutput if outputData['confirmationNeeded']: outputData['address'] = output self.handler.finished() pin = self.handler.get_auth( outputData ) # does the authenticate dialog and returns pin if not pin: raise UserWarning() self.handler.show_message(_("Confirmed. Signing Transaction...")) else: # Sign input with the provided PIN inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 firstTransaction = False except UserWarning: self.handler.show_error(_('Cancelled by user')) return except BTChipException as e: if e.sw in (0x6985, 0x6d00): # cancelled by user return elif e.sw == 0x6982: raise # pin lock. decorator will catch it else: self.logger.exception('') self.give_error(e, True) except BaseException as e: self.logger.exception('') self.give_error(e, True) finally: self.handler.finished() for i, txin in enumerate(tx.inputs()): signingPos = inputs[i][4] tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i])) tx.raw = tx.serialize()
def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool: if not isinstance(tx, PartialTransaction): return False if tx.is_complete(): return False return cosigner_xpub in {bip32node.to_xpub() for bip32node in tx.xpubs}
def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' # We want to be a top-level window QDialog.__init__(self, parent=None) # Take a copy; it might get updated in the main window by # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. self.tx = tx = copy.deepcopy(tx) try: self.tx.deserialize() except BaseException as e: raise SerializationError(e) self.main_window = parent self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved self.saved = False self.desc = desc self.invoice = invoice # if the wallet can populate the inputs with more info, do it now. # as a result, e.g. we might learn an imported address tx is segwit, # or that a beyond-gap-limit address is is_mine tx.add_info_from_wallet(self.wallet) self.setMinimumWidth(950) self.setWindowTitle(_("Transaction")) vbox = QVBoxLayout() self.setLayout(vbox) vbox.addWidget(QLabel(_("Transaction ID:"))) self.tx_hash_e = ButtonsLineEdit() qr_show = lambda: parent.show_qrcode( str(self.tx_hash_e.text()), 'Transaction ID', parent=self) qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" self.tx_hash_e.addButton(qr_icon, qr_show, _("Show as QR code")) self.tx_hash_e.setReadOnly(True) vbox.addWidget(self.tx_hash_e) self.add_tx_stats(vbox) vbox.addSpacing(10) self.inputs_header = QLabel() vbox.addWidget(self.inputs_header) self.inputs_textedit = QTextEditWithDefaultSize() vbox.addWidget(self.inputs_textedit) self.outputs_header = QLabel() vbox.addWidget(self.outputs_header) self.outputs_textedit = QTextEditWithDefaultSize() vbox.addWidget(self.outputs_textedit) self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) self.broadcast_button = b = QPushButton(_("Broadcast")) b.clicked.connect(self.do_broadcast) self.save_button = b = QPushButton(_("Save")) save_button_disabled = not tx.is_complete() b.setDisabled(save_button_disabled) if save_button_disabled: b.setToolTip(SAVE_BUTTON_DISABLED_TOOLTIP) else: b.setToolTip(SAVE_BUTTON_ENABLED_TOOLTIP) b.clicked.connect(self.save) self.cancel_button = b = QPushButton(_("Close")) b.clicked.connect(self.close) b.setDefault(True) self.export_actions_menu = export_actions_menu = QMenu() self.add_export_actions_to_menu(export_actions_menu) export_actions_menu.addSeparator() if isinstance(tx, PartialTransaction): export_for_coinjoin_submenu = export_actions_menu.addMenu( _("For CoinJoin; strip privates")) self.add_export_actions_to_menu(export_for_coinjoin_submenu, gettx=self._gettx_for_coinjoin) self.export_actions_button = QToolButton() self.export_actions_button.setText(_("Export")) self.export_actions_button.setMenu(export_actions_menu) self.export_actions_button.setPopupMode(QToolButton.InstantPopup) partial_tx_actions_menu = QMenu() ptx_merge_sigs_action = QAction(_("Merge signatures from"), self) ptx_merge_sigs_action.triggered.connect(self.merge_sigs) partial_tx_actions_menu.addAction(ptx_merge_sigs_action) ptx_join_txs_action = QAction(_("Join inputs/outputs"), self) ptx_join_txs_action.triggered.connect(self.join_tx_with_another) partial_tx_actions_menu.addAction(ptx_join_txs_action) self.partial_tx_actions_button = QToolButton() self.partial_tx_actions_button.setText(_("Combine")) self.partial_tx_actions_button.setMenu(partial_tx_actions_menu) self.partial_tx_actions_button.setPopupMode(QToolButton.InstantPopup) # Action buttons self.buttons = [] if isinstance(tx, PartialTransaction): self.buttons.append(self.partial_tx_actions_button) self.buttons += [ self.sign_button, self.broadcast_button, self.cancel_button ] # Transaction sharing buttons self.sharing_buttons = [self.export_actions_button, self.save_button] run_hook('transaction_dialog', self) hbox = QHBoxLayout() hbox.addLayout(Buttons(*self.sharing_buttons)) hbox.addStretch(1) hbox.addLayout(Buttons(*self.buttons)) vbox.addLayout(hbox) self.update()