def show_send_to_cosigner_button(self, account: AbstractAccount, tx: Transaction) -> bool: if tx.is_complete() or account.can_sign(tx): return False account_id = account.get_id() return any( self._is_theirs(account_id, item, tx) for item in self._items)
def show_send_to_cosigner_button(self, window: 'ElectrumWindow', account: AbstractAccount, tx: Transaction) -> bool: if window.network is None: return False if tx.is_complete() or account.can_sign(tx): return False account_id = account.get_id() return any(self._is_theirs(window, account_id, item, tx) for item in self._items)
def broadcast_transaction(self, tx_hex: Optional[str]=None, wallet_name: Optional[str]=None, wallet_memo: Optional[str]=None) -> str: wallet = None if wallet_name and wallet_memo: wallet = self._get_wallet(wallet_name) tx = Transaction(tx_hex) try: tx_id = app_state.daemon.network.broadcast_transaction_and_wait(tx) except aiorpcx.jsonrpc.RPCError as e: if e.code == 1 and e.message.find("too-long-mempool-chain") != -1: return jsonrpclib.Fault(100, "too-long-mempool-chain") print("raising rpc error", e.code, e.message) raise e if tx.is_complete() and wallet_name and wallet_memo: wallet.set_label(tx_id, wallet_memo) return tx_id
def sign_transaction(self, tx: Transaction, password: str, tx_context: TransactionContext) -> None: if tx.is_complete(): return assert not len(tx_context.prev_txs ), "This keystore does not require input transactions" # path of the xpubs that are involved xpub_path: Dict[str, str] = {} for txin in tx.inputs: for x_pubkey in txin.x_pubkeys: if not x_pubkey.is_bip32_key(): continue xpub = x_pubkey.bip32_extended_key() if xpub == self.get_master_public_key(): xpub_path[xpub] = self.get_derivation() assert self.plugin is not None self.plugin.sign_transaction(self, tx, xpub_path)
def _get_tx_info(self, tx: Transaction) -> TxInfo: value_delta = 0 can_broadcast = False label = '' fee = height = conf = date_created = date_mined = None state = TxFlags.Unset wallet = self._wallet if tx.is_complete(): metadata = wallet._transaction_cache.get_metadata(self._tx_hash) if metadata is None: # The transaction is not known to the wallet. status = _("External signed transaction") state = TxFlags.StateReceived | TxFlags.StateSigned can_broadcast = wallet._network is not None else: date_created = metadata.date_added # It is possible the wallet has the transaction but it is not associated with # any accounts. We still need to factor that in. fee = metadata.fee if metadata.height is not None and metadata.height > 0: chain = app_state.headers.longest_chain() try: header = app_state.headers.header_at_height( chain, metadata.height) date_mined = header.timestamp except MissingHeader: pass label = wallet.get_transaction_label(self._tx_hash) state = wallet._transaction_cache.get_flags( self._tx_hash) & TxFlags.STATE_MASK if state & TxFlags.StateSettled: height = metadata.height conf = max(wallet.get_local_height() - height + 1, 0) status = _("{:,d} confirmations (in block {:,d})").format( conf, height) elif state & TxFlags.StateCleared: if metadata.height > 0: status = _('Not verified') else: status = _('Unconfirmed') elif state & TxFlags.StateReceived: status = _("Received") can_broadcast = wallet._network is not None elif state & TxFlags.StateDispatched: status = _("Dispatched") elif state & TxFlags.StateSigned: status = _("Signed") can_broadcast = wallet._network is not None else: status = _('Unknown') account_deltas = wallet.get_transaction_deltas(self._tx_hash) value_delta = sum(row.total for row in account_deltas) # # It is possible that the wallet does not have the transaction. # if delta_result.match_count == 0: # pass # else: # value_delta += delta_result.total else: state = TxFlags.StateReceived # For now all inputs must come from the same account. for input in tx.inputs: value_delta -= input.value for output in tx.outputs: # If we know what type of script it is, we sign it's spend (or co-sign it). # We are sending to ourselves or we are receiving change. if output.script_type != ScriptType.NONE: value_delta += output.value s, r = tx.signature_count() status = _("Unsigned") if s == 0 else _( 'Partially signed') + ' (%d/%d)' % (s, r) if value_delta < 0: if fee is not None: # We remove the fee as the user can work out the fee is part of the sum themselves. amount = value_delta + fee else: amount = value_delta elif value_delta > 0: amount = value_delta else: amount = None return TxInfo(self._tx_hash, state, status, label, can_broadcast, amount, fee, height, conf, date_mined, date_created)
def sign_transaction(self, tx: Transaction, password: str) -> None: if tx.is_complete(): return try: p2pkhTransaction = True inputhasharray = [] hasharray = [] pubkeyarray = [] # Build hasharray from inputs for txin in tx.inputs: if txin.type() != ScriptType.P2PKH: p2pkhTransaction = False for x_pubkey in txin.x_pubkeys: if self.is_signature_candidate(x_pubkey): key_derivation = x_pubkey.bip32_path() assert len(key_derivation) == 2 inputPath = "%s/%d/%d" % (self.get_derivation(), *key_derivation) inputHash = tx.preimage_hash(txin) hasharray_i = { 'hash': inputHash.hex(), 'keypath': inputPath } hasharray.append(hasharray_i) inputhasharray.append(inputHash) break else: self.give_error("No matching x_key for sign_transaction" ) # should never happen # Build pubkeyarray from annotated change outputs. # The user is on their own if they have unannotated non-change self-outputs. for txout in tx.outputs: if txout.x_pubkeys: for xpubkey in [ xpk for xpk in txout.x_pubkeys if self.is_signature_candidate(xpk) ]: key_path_text = compose_chain_string( xpubkey.derivation_path())[1:] changePath = self.get_derivation( ) + key_path_text # "/1/0", no "m" pubkeyarray.append({ 'pubkey': xpubkey.to_public_key().to_hex(), 'keypath': changePath, }) # Special serialization of the unsigned transaction for # the mobile verification app. # At the moment, verification only works for p2pkh transactions. if p2pkhTransaction: class CustomTXSerialization(Transaction): @classmethod def input_script(self, txin, estimate_size=False): type_ = txin.type() if type_ == ScriptType.P2PKH: return Transaction.get_preimage_script(txin) if type_ == ScriptType.MULTISIG_P2SH: # Multisig verification has partial support, but is # disabled. This is the expected serialization though, so we # leave it here until we activate it. return '00' + push_script( Transaction.get_preimage_script(txin)) raise RuntimeError(f'unsupported type {type_}') tx_dbb_serialized = CustomTXSerialization.from_hex( tx.serialize()).serialize() else: # We only need this for the signing echo / verification. tx_dbb_serialized = None # Build sign command dbb_signatures: List[Dict[str, Any]] = [] steps = math.ceil(1.0 * len(hasharray) / self.maxInputs) for step in range(int(steps)): hashes = hasharray[step * self.maxInputs:(step + 1) * self.maxInputs] msg_data: Dict[str, Any] = { "sign": { "data": hashes, "checkpub": pubkeyarray, }, } if tx_dbb_serialized is not None: msg_data["sign"]["meta"] = sha256d(tx_dbb_serialized).hex() msg = json.dumps(msg_data).encode('ascii') assert self.plugin is not None dbb_client: DigitalBitbox_Client = self.plugin.get_client(self) if not dbb_client.is_paired(): raise Exception("Could not sign transaction.") reply = dbb_client.hid_send_encrypt(msg) if 'error' in reply: raise Exception(reply['error']['message']) if 'echo' not in reply: raise Exception("Could not sign transaction.") if self.plugin.is_mobile_paired( ) and tx_dbb_serialized is not None: reply['tx'] = tx_dbb_serialized self.plugin.comserver_post_notification(reply) if steps > 1: self.handler.show_message( _("Signing large transaction. Please be patient ...") + "\n\n" + _("To continue, touch the Digital Bitbox's blinking light for " "3 seconds.") + " " + _("(Touch {} of {})").format((step + 1), steps) + "\n\n" + _("To cancel, briefly touch the blinking light or wait for the timeout." ) + "\n\n") else: self.handler.show_message( _("Signing transaction...") + "\n\n" + _("To continue, touch the Digital Bitbox's blinking light for " "3 seconds.") + "\n\n" + _("To cancel, briefly touch the blinking light or wait for the timeout." )) # Send twice, first returns an echo for smart verification reply = dbb_client.hid_send_encrypt(msg) self.handler.finished() if 'error' in reply: if reply["error"].get('code') in (600, 601): # aborted via LED short touch or timeout raise UserCancelled() raise Exception(reply['error']['message']) if 'sign' not in reply: raise Exception("Could not sign transaction.") dbb_signatures.extend(reply['sign']) # Fill signatures if len(dbb_signatures) != len(tx.inputs): raise RuntimeError("Incorrect number of transactions signed") for txin, siginfo, pre_hash in zip(tx.inputs, dbb_signatures, inputhasharray): if txin.is_complete(): continue for pubkey_index, x_pubkey in enumerate(txin.x_pubkeys): compact_sig = bytes.fromhex(siginfo['sig']) if 'recid' in siginfo: # firmware > v2.1.1 recid = int(siginfo['recid'], 16) recoverable_sig = compact_sig + bytes([recid]) pk = PublicKey.from_recoverable_signature( recoverable_sig, pre_hash, None) elif 'pubkey' in siginfo: # firmware <= v2.1.1 pk = PublicKey.from_hex(siginfo['pubkey']) if pk != x_pubkey.to_public_key(): continue full_sig = (compact_signature_to_der(compact_sig) + bytes([Transaction.nHashType() & 255])) txin.signatures[pubkey_index] = full_sig except UserCancelled: raise except Exception as e: self.give_error(e, True) else: logger.debug("Transaction is_complete %s", tx.is_complete())
def sign_transaction(self, tx: Transaction, password: str, tx_context: TransactionContext) -> None: if tx.is_complete(): return assert self.handler is not None client = self.get_client() inputs: List[YInput] = [] inputsPaths = [] chipInputs = [] redeemScripts = [] signatures = [] changePath = "" changeAmount = None output = None outputAmount = None pin = "" self.get_client( ) # prompt for the PIN before displaying the dialog if necessary # Fetch inputs of the transaction to sign foundP2SHSpend = False allSpendsAreP2SH = True for txin in tx.inputs: foundP2SHSpend = foundP2SHSpend or txin.type( ) == ScriptType.MULTISIG_P2SH allSpendsAreP2SH = allSpendsAreP2SH and txin.type( ) == ScriptType.MULTISIG_P2SH for i, x_pubkey in enumerate(txin.x_pubkeys): if self.is_signature_candidate(x_pubkey): txin_xpub_idx = i inputPath = "%s/%d/%d" % (self.get_derivation()[2:], *x_pubkey.bip32_path()) break else: self.give_error("No matching x_key for sign_transaction" ) # should never happen inputs.append( YInput(txin.value, Transaction.get_preimage_script_bytes(txin), txin_xpub_idx, txin.sequence)) inputsPaths.append(inputPath) # Sanity check if foundP2SHSpend and not allSpendsAreP2SH: self.give_error( "P2SH / regular input mixed in same transaction not supported") # Concatenate all the tx outputs as binary txOutput = pack_list(tx.outputs, XTxOutput.to_bytes) # Recognize outputs - only one output and one change is authorized if not foundP2SHSpend: keystore_fingerprint = self.get_fingerprint() assert tx.output_info is not None for tx_output, output_metadatas in zip(tx.outputs, tx.output_info): info = output_metadatas.get(keystore_fingerprint) if (info is not None) and len(tx.outputs) != 1: key_derivation, xpubs, m = info key_subpath = compose_chain_string(key_derivation)[1:] changePath = self.get_derivation()[2:] + key_subpath changeAmount = tx_output.value else: output = classify_tx_output(tx_output) outputAmount = tx_output.value self.handler.show_message( _("Confirm Transaction on your Ledger device...")) try: for i, utxo in enumerate(inputs): txin = tx.inputs[i] sequence = int_to_hex(utxo.sequence, 4) prevout_bytes = txin.prevout_bytes() value_bytes = prevout_bytes + pack_le_int64(utxo.value) chipInputs.append({ 'value': value_bytes, 'witness': True, 'sequence': sequence }) redeemScripts.append(utxo.script_sig) # Sign all inputs inputIndex = 0 rawTx = tx.serialize() self.get_client().enableAlternate2fa(False) self.get_client().startUntrustedTransaction( True, inputIndex, chipInputs, redeemScripts[inputIndex]) outputData = self.get_client().finalizeInputFull(txOutput) outputData['outputData'] = txOutput transactionOutput = outputData['outputData'] if outputData['confirmationNeeded']: outputData['address'] = cast(ScriptTemplate, output).to_string() self.handler.finished() # the authenticate dialog and returns pin auth_pin = self.handler.get_auth(self, outputData) if not auth_pin: raise UserWarning() pin = auth_pin self.handler.show_message( _("Confirmed. Signing Transaction...")) while inputIndex < len(inputs): singleInput = [chipInputs[inputIndex]] self.get_client().startUntrustedTransaction( False, 0, singleInput, redeemScripts[inputIndex]) inputSignature = self.get_client().untrustedHashSign( inputsPaths[inputIndex], pin, lockTime=tx.locktime, sighashType=tx.nHashType()) inputSignature[0] = 0x30 # force for 1.4.9+ signatures.append(inputSignature) inputIndex = inputIndex + 1 except UserWarning: self.handler.show_error(_('Cancelled by user')) return except BTChipException as e: if e.sw == 0x6985: # cancelled by user return else: logger.exception("") self.give_error(e, True) except Exception as e: logger.exception("") self.give_error(e, True) finally: self.handler.finished() for txin, input, signature in zip(tx.inputs, inputs, signatures): txin.signatures[input.txin_xpub_idx] = signature
def get_tx_info(self, tx: Transaction) -> TxInfo: value_delta = 0 can_broadcast = False label = '' fee = height = conf = timestamp = None tx_hash = tx.hash() if tx.is_complete(): entry = self._wallet._transaction_cache.get_cached_entry(tx_hash) metadata = entry.metadata fee = metadata.fee label = self._account.get_transaction_label(tx_hash) value_delta = self._account.get_transaction_delta(tx_hash) if value_delta is None: # When the transaction is fully signed and updated before the delta changes # are committed to the database (pending write). value_delta = 0 if self._account.has_received_transaction(tx_hash): if (entry.flags & TxFlags.StateSettled or entry.flags & TxFlags.StateCleared and metadata.height > 0): chain = app_state.headers.longest_chain() try: header = app_state.headers.header_at_height( chain, metadata.height) timestamp = header.timestamp except MissingHeader: pass if entry.flags & TxFlags.StateSettled: height = metadata.height conf = max( self._wallet.get_local_height() - height + 1, 0) status = _( "{:,d} confirmations (in block {:,d})").format( conf, height) else: status = _('Not verified') else: status = _('Unconfirmed') else: status = _("Signed") can_broadcast = self._wallet._network is not None else: for input in tx.inputs: value_delta -= input.value for output in tx.outputs: # If we know what type of script it is, we sign it's spend (or co-sign it). if output.script_type != ScriptType.NONE: value_delta += output.value s, r = tx.signature_count() status = _("Unsigned") if s == 0 else _( 'Partially signed') + ' (%d/%d)' % (s, r) if value_delta < 0: if fee is not None: amount = value_delta + fee else: amount = value_delta elif value_delta > 0: amount = value_delta else: amount = None return TxInfo(tx_hash, status, label, can_broadcast, amount, fee, height, conf, timestamp)
def show_button(self, wallet: Abstract_Wallet, tx: Transaction) -> bool: if tx.is_complete() or wallet.can_sign(tx): return False return any(self.is_theirs(wallet, item, tx) for item in self.items)