def add_menu_items(self, menu: QMenu, account: AbstractAccount, main_window: ElectrumWindow) \ -> None: menu.clear() # This expects a reference to the main window, not the weakref. account_id = account.get_id() menu.addAction(_("&Information"), partial(self._show_account_information, account_id)) seed_menu = menu.addAction( _("View &Secured Data"), partial(self._view_secured_data, main_window=main_window, account_id=account_id)) seed_menu.setEnabled(self._can_view_secured_data(account)) menu.addAction(_("&Rename"), partial(self._rename_account, account_id)) menu.addSeparator() private_keys_menu = menu.addMenu(_("&Private keys")) import_menu = private_keys_menu.addAction( _("&Import"), partial(self._import_privkey, main_window=main_window, account_id=account_id)) import_menu.setEnabled(account.can_import_privkey()) export_menu = private_keys_menu.addAction( _("&Export"), partial(self._export_privkeys, main_window=main_window, account_id=account_id)) export_menu.setEnabled(account.can_export()) if account.can_import_address(): menu.addAction(_("Import addresses"), partial(self._import_addresses, account_id)) menu.addSeparator() hist_menu = menu.addMenu(_("&History")) hist_menu.addAction("Export", main_window.export_history_dialog) labels_menu = menu.addMenu(_("&Labels")) action = labels_menu.addAction( _("&Import"), partial(self._on_menu_import_labels, account_id)) labels_menu.addAction(_("&Export"), partial(self._on_menu_export_labels, account_id)) invoices_menu = menu.addMenu(_("Invoices")) self._import_invoices_action = invoices_menu.addAction( _("Import"), partial(self._on_menu_import_invoices, account_id)) self._import_invoices_action.setEnabled( main_window.is_send_view_active()) payments_menu = menu.addMenu(_("Payments")) ed_action = payments_menu.addAction( _("Export destinations"), partial(self._generate_destinations, account_id)) keystore = account.get_keystore() ed_action.setEnabled( keystore is not None and keystore.type() != KeystoreType.IMPORTED_PRIVATE_KEY)
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)
async def _broadcast_transaction(self, rawtx: str, tx_hash: bytes, account: AbstractAccount): result = await self.send_request('blockchain.transaction.broadcast', [rawtx]) account.maybe_set_transaction_dispatched(tx_hash) self.logger.debug("successful broadcast for %s", result) return result
def get_and_set_frozen_utxos_for_tx(self, tx: Transaction, child_wallet: AbstractAccount, freeze: bool=True) -> List[UTXO]: spendable_coins = child_wallet.get_utxos(exclude_frozen=False) input_keys = set( [(bitcoinx.hash_to_hex_str(input.prev_hash), input.prev_idx) for input in tx.inputs]) frozen_utxos = [utxo for utxo in spendable_coins if utxo.key() in input_keys] child_wallet.set_frozen_coin_state(frozen_utxos, freeze) return frozen_utxos
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)
async def _broadcast_transaction(self, rawtx: str, tx_hash: bytes, account: AbstractAccount): result = await self.send_request('blockchain.transaction.broadcast', [rawtx]) account.set_transaction_state(tx_hash=tx_hash, flags=(TxFlags.StateDispatched | TxFlags.HasByteData)) self.logger.debug("successful broadcast for %s", result) return result
def remove_signed_transaction(self, tx: Transaction, wallet: AbstractAccount): # must remove signed transactions after a failed broadcast attempt (to unlock utxos) # if it's a re-broadcast attempt (same txid) and we already have a StateDispatched or # StateCleared transaction then *no deletion* should occur tx_hash = tx.hash() signed_tx = wallet.get_transaction(tx_hash, flags=TxFlags.StateSigned) if signed_tx: wallet.delete_transaction(tx_hash)
def _get_addresses(klass, account: AbstractAccount) -> Dict[ScriptTemplate, int]: script_type = ScriptType.P2PKH if isinstance(account, MultisigAccount): script_type = ScriptType.MULTISIG_P2SH result: Dict[ScriptTemplate, int] = {} for keyinstance_id in account.get_keyinstance_ids(): template = account.get_script_template_for_id( keyinstance_id, script_type) result[template] = keyinstance_id return result
def show_key(self, account: AbstractAccount, keyinstance_id: int) -> None: keystore = cast(KeepKey_KeyStore, account.get_keystore()) client = self.get_client(keystore) derivation_path = account.get_derivation_path(keyinstance_id) assert derivation_path is not None subpath = '/'.join(str(x) for x in derivation_path) address_path = f"{keystore.derivation}/{subpath}" address_n = bip32_decompose_chain_string(address_path) script_type = self.types.SPENDADDRESS client.get_address(Net.KEEPKEY_DISPLAY_COIN_NAME, address_n, True, script_type=script_type)
def _on_account_change(self, new_account_id: int, new_account: AbstractAccount) -> None: self._account_id = new_account_id self._account = new_account script_type = new_account.get_default_script_type() # Hardware wallets will not sign OP_FALSE OP_RETURN. self._direct_splitting_enabled = self._account.is_deterministic() and \ new_account.can_spend() and \ not new_account.involves_hardware_wallet() # The faucet requires an address to send to. There are only P2PKH addresses. self._faucet_splitting_enabled = self._account.is_deterministic() and \ script_type == ScriptType.P2PKH self.update_layout()
def pull_thread(self, account: AbstractAccount, force: bool) -> Optional[Any]: account_data = self._accounts.get(account, None) if not account_data: raise Exception('Account {} not loaded'.format(account)) wallet_id = account_data[2] nonce = 1 if force else self.get_nonce(account) - 1 logger.debug(f"asking for labels since nonce {nonce}") response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id))) if response["labels"] is None: logger.debug('no new labels') return result = {} for label in response["labels"]: try: key = self.decode(account, label["externalId"]) value = self.decode(account, label["encryptedLabel"]) except Exception: continue try: json.dumps(key) json.dumps(value) except Exception: logger.error(f'no json {key}') continue result[key] = value logger.info(f"received {len(result):,d} labels") updates = {} for key, value in result.items(): # TODO(rt12) BACKLOG there is no account.labels any more. if force or not account.labels.get(key): updates[key] = value if DISABLE_INTEGRATION: return updates if len(updates): # TODO(rt12) BACKLOG there is no account.put or account storage at this time, or # even `account.labels`. account.labels.update(updates) # do not write to disk because we're in a daemon thread. The handed off writing to # the sqlite writer thread would achieve this. account.put('labels', account.labels) self.set_nonce(account, response["nonce"] + 1) self.on_pulled(account, updates)
def compare_key_path(account: AbstractAccount, txo_key: TxoKeyType, leading_path: Sequence[int]) -> bool: utxo = account._utxos.get(txo_key) if utxo is not None: key_path = account.get_derivation_path(utxo.keyinstance_id) if key_path is not None and key_path[:len(leading_path )] == leading_path: return True stxo_keyinstance_id = account._stxos.get(txo_key) if stxo_keyinstance_id is not None: key_path = account.get_derivation_path(stxo_keyinstance_id) if key_path is not None and key_path[:len(leading_path )] == leading_path: return True return False
def get_tx_status(account: AbstractAccount, tx_hash: bytes, height: int, conf: int, timestamp: Union[bool, int]) -> TxStatus: if not account.have_transaction_data(tx_hash): return TxStatus.MISSING metadata = account.get_transaction_metadata(tx_hash) if metadata.position == 0: if height + COINBASE_MATURITY > account._wallet.get_local_height(): return TxStatus.UNMATURED elif conf == 0: if height > 0: return TxStatus.UNVERIFIED return TxStatus.UNCONFIRMED return TxStatus.FINAL
def _get_derivations(klass, account: AbstractAccount) -> Dict[Sequence[int], int]: keypaths = account.get_key_paths() result: Dict[Sequence[int], int] = {} for keyinstance_id, derivation_path in keypaths.items(): result[derivation_path] = keyinstance_id return result
def do_send(self, window: 'ElectrumWindow', account: AbstractAccount, tx: Transaction) -> None: def on_done(window, future): try: future.result() except Exception as exc: window.on_exception(exc) else: window.show_message('\n'.join(( _("Your transaction was sent to the cosigning pool."), _("Open your cosigner wallet to retrieve it."), ))) def send_message(): server.put(item.keyhash_hex, message) account_id = account.get_id() for item in self._items: if self._is_theirs(window, account_id, item, tx): raw_tx_bytes = json.dumps(tx.to_dict()).encode() public_key = PublicKey.from_bytes(item.pubkey_bytes) message = public_key.encrypt_message_to_base64(raw_tx_bytes) WaitingDialog(item.window, _('Sending transaction to cosigning pool...'), send_message, on_done=partial(on_done, item.window))
def account_widgets(self, account: AbstractAccount): label = QLabel( _("The settings below only affect the account '{}'").format( account.display_name())) script_type_combo = QComboBox() def update_script_types(): default_script_type = account.get_default_script_type() combo_items = [v.name for v in account.get_valid_script_types()] script_type_combo.clear() script_type_combo.addItems(combo_items) script_type_combo.setCurrentIndex( script_type_combo.findText(default_script_type.name)) def on_script_type_change(index): script_type_name = script_type_combo.currentText() new_script_type = getattr(ScriptType, script_type_name) current_script_type = account.get_default_script_type() if current_script_type == new_script_type: return account.set_default_script_type(new_script_type) self._main_window.update_receive_address_widget() script_type_combo.currentIndexChanged.connect(on_script_type_change) update_script_types() return [ (label, ), (script_type_combo, ), ]
def _fetch_transaction_dto(self, account: AbstractAccount, tx_id) -> Optional[Dict]: tx_hash = hex_str_to_hash(tx_id) tx = account.get_transaction(tx_hash) if not tx: raise Fault(Errors.TRANSACTION_NOT_FOUND_CODE, Errors.TRANSACTION_NOT_FOUND_MESSAGE) return {"tx_hex": tx.to_hex()}
def remove_transaction(self, tx_hash: bytes, wallet: AbstractAccount): # removal of txs that are not in the StateSigned tx state is disabled for now as it may # cause issues with expunging utxos inadvertently. try: tx = wallet.get_transaction(tx_hash) tx_flags = wallet._wallet._transaction_cache.get_flags(tx_hash) is_signed_state = (tx_flags & TxFlags.StateSigned) == TxFlags.StateSigned # Todo - perhaps remove restriction to StateSigned only later (if safe for utxos state) if tx and is_signed_state: wallet.delete_transaction(tx_hash) if tx and not is_signed_state: raise Fault(Errors.DISABLED_FEATURE_CODE, Errors.DISABLED_FEATURE_MESSAGE) except MissingRowError: raise Fault(Errors.TRANSACTION_NOT_FOUND_CODE, Errors.TRANSACTION_NOT_FOUND_MESSAGE)
def get_nonce(self, account: AbstractAccount) -> int: # nonce is the nonce to be used with the next change if DISABLE_INTEGRATION: return 1 # TODO BACKLOG there is no working account get/set nonce = account.get('wallet_nonce', None) if nonce is None: nonce = 1 self.set_nonce(account, nonce) return nonce
def _transaction_state_dto(self, wallet: AbstractAccount, tx_ids: Optional[Iterable[str]]=None) -> Union[Fault, Dict[Any, Any]]: chain = self.app_state.daemon.network.chain() result = {} for tx_id in tx_ids: tx_hash = hex_str_to_hash(tx_id) if wallet.has_received_transaction(tx_hash): # height, conf, timestamp height, conf, timestamp = wallet.get_tx_height(tx_hash) block_id = None if timestamp: block_id = self.app_state.headers.header_at_height(chain, height).hex_str() result[tx_id] = { "block_id": block_id, "height": height, "conf": conf, "timestamp": timestamp, } return result
def start_account(self, account: AbstractAccount) -> None: nonce = self.get_nonce(account) logger.debug("Account %s nonce is %s", account.name(), nonce) mpk = ''.join(sorted(account.get_master_public_keys())) if not mpk: return mpk = mpk.encode('ascii') password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii') iv = hashlib.sha256(password).digest()[:16] wallet_id = hashlib.sha256(mpk).hexdigest() self._accounts[account] = (password, iv, wallet_id) if DISABLE_INTEGRATION: return # If there is an auth token we can try to actually start syncing t = threading.Thread(target=self.pull_thread_safe, args=(account, False)) t.setDaemon(True) t.start()
def account_widgets(self, account: AbstractAccount, tab: QWidget) -> None: label = QLabel( _("The settings below only affect the account '{}'").format( account.display_name())) script_type_combo = QComboBox() def update_script_types(): default_script_type = account.get_default_script_type() combo_items = [v.name for v in account.get_valid_script_types()] script_type_combo.clear() script_type_combo.addItems(combo_items) script_type_combo.setCurrentIndex( script_type_combo.findText(default_script_type.name)) def on_script_type_change(index): script_type_name = script_type_combo.currentText() new_script_type = getattr(ScriptType, script_type_name) current_script_type = account.get_default_script_type() if current_script_type == new_script_type: return account.set_default_script_type(new_script_type) self._main_window.update_receive_address_widget() script_type_combo.currentIndexChanged.connect(on_script_type_change) update_script_types() form = FormSectionWidget(minimum_label_width=120) form.add_title(_("Account: {}").format(account.display_name())) form.add_row(_("Default script type"), script_type_combo) vbox = QVBoxLayout() vbox.addWidget(form) vbox.addStretch(1) tab.setLayout(vbox)
def _add_account_to_list(self, account: AbstractAccount) -> None: account_id = account.get_id() item = QListWidgetItem() keystore = account.get_keystore() derivation_type = keystore.derivation_type if keystore is not None \ else DerivationType.NONE is_watching_only = keystore.is_watching_only( ) if keystore is not None else True icon_state = "inactive" if is_watching_only else "active" if derivation_type == DerivationType.ELECTRUM_MULTISIG: tooltip_text = _("Multi-signature account") icon_filename = "icons8-group-task-80-blueui-{}.png" elif derivation_type == DerivationType.HARDWARE: tooltip_text = _("Hardware wallet account") icon_filename = "icons8-usb-2-80-blueui-{}.png" elif derivation_type == DerivationType.IMPORTED: # This should not be watch only as imported public keys have no keystore. tooltip_text = _("Imported private key account") icon_filename = "icons8-key-80-plus-blueui-{}.png" elif derivation_type == DerivationType.ELECTRUM_OLD: tooltip_text = _("Old-style Electrum account") icon_filename = "icons8-password-1-80-blueui-{}.png" elif derivation_type == DerivationType.BIP32: tooltip_text = _("BIP32 account") icon_filename = "icons8-grand-master-key-80-blueui-{}.png" else: # This should always be watch only as imported public keys have no keystore. tooltip_text = _("Imported public key account") icon_filename = "icons8-key-80-plus-blueui-{}.png" if is_watching_only: tooltip_text += f" ({_('watch only')})" item.setIcon(read_QIcon(icon_filename.format(icon_state))) item.setData(Qt.UserRole, account_id) item.setText(account.display_name()) item.setToolTip(tooltip_text) self._selection_list.addItem(item) self._account_ids.append(account_id)
def _history_dto(self, account: AbstractAccount, tx_flags: int = None) -> List[Dict[Any, Any]]: result = [] entries = account._wallet._transaction_cache.get_entries(mask=tx_flags) for tx_hash, entry in entries: tx_values = account._wallet.get_transaction_deltas( tx_hash, account.get_id()) assert len(tx_values) == 1 result.append({ "txid": hash_to_hex_str(tx_hash), "height": entry.metadata.height, "tx_flags": entry.flags, "value": int(tx_values[0].total) }) return result
def parse_label_export_json(klass, account: AbstractAccount, text: str) -> LabelImportResult: updates: Dict[str, Any] = json.loads(text) results = LabelImportResult(LabelImportFormat.ACCOUNT) for tx_id, label_text in updates.get("transactions", []): if len(tx_id) == 64: # length of the transaction id (hex of hash) try: tx_hash = hex_str_to_hash(tx_id) except (TypeError, ValueError): pass else: results.transaction_labels[tx_hash] = label_text continue results.unknown_labels[tx_id] = label_text keydata: Optional[Dict[str, Any]] = updates.get("keys") if keydata is not None: account_fingerprint = account.get_fingerprint().hex() if isinstance(keydata.get("account_fingerprint"), str): results.account_fingerprint = keydata["account_fingerprint"] derivations = klass._get_derivations(account) for derivation_path_text, label_text in keydata["entries"]: try: derivation_path = tuple( bip32_decompose_chain_string(derivation_path_text)) except (TypeError, ValueError): pass else: # We never import key descriptions if the account does not match. if account_fingerprint == results.account_fingerprint: keyinstance_id = derivations.get(derivation_path) if keyinstance_id is not None: results.key_labels[keyinstance_id] = label_text continue results.unknown_labels[derivation_path_text] = label_text return results
def name_for_account(account: AbstractAccount) -> str: name = account.display_name() return f"{account.get_id()}: {name}"
def compare_key_path(account: AbstractAccount, keyinstance_id: int, leading_path: Sequence[int]) -> bool: key_path = account.get_derivation_path(keyinstance_id) return key_path is not None and key_path[:len(leading_path )] == leading_path
def set_nonce(self, account: AbstractAccount, nonce: int) -> None: logger.debug("set {} nonce to {}".format(account.name(), nonce)) # TODO BACKLOG there is no working account get/set account.put("wallet_nonce", nonce)
def on_pulled(self, account: AbstractAccount, updates: Any) -> None: app_state.app.labels_changed_signal.emit( account._wallet.get_storage_path(), account.get_id(), updates)
def _can_view_secured_data(self, account: AbstractAccount) -> None: return not account.is_watching_only() and not isinstance(account, MultisigAccount) \ and not account.is_hardware_wallet() \ and account.type() != AccountType.IMPORTED_PRIVATE_KEY