qt: generalize wizard HWW xpub
This commit is contained in:
@@ -86,7 +86,6 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard):
|
||||
'last': lambda d: d['wallet_exists'] and not d['wallet_needs_hw_unlock']
|
||||
},
|
||||
'hw_unlock': {
|
||||
# 'last': True,
|
||||
'gui': WCChooseHWDevice,
|
||||
'next': lambda d: self.on_hardware_device(d, new_wallet=False)
|
||||
}
|
||||
@@ -830,7 +829,7 @@ class WCMultisig(WizardComponent):
|
||||
|
||||
class WCImport(WizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses'))
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Import Bitcoin Addresses or Private Keys'))
|
||||
message = _(
|
||||
'Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.')
|
||||
header_layout = QHBoxLayout()
|
||||
@@ -1111,7 +1110,7 @@ class WCChooseHWDevice(WizardComponent, Logger):
|
||||
|
||||
class WCWalletPasswordHardware(WizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Password HW'))
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Encrypt using hardware'))
|
||||
self.plugins = wizard.plugins
|
||||
|
||||
self.playout = PasswordLayoutForHW(MSG_HW_STORAGE_ENCRYPTION)
|
||||
@@ -1133,7 +1132,7 @@ class WCWalletPasswordHardware(WizardComponent):
|
||||
|
||||
class WCHWUnlock(WizardComponent, Logger):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('unlock'))
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Unlocking hardware'))
|
||||
Logger.__init__(self)
|
||||
self.plugins = wizard.plugins
|
||||
self.plugin = None
|
||||
@@ -1157,19 +1156,14 @@ class WCHWUnlock(WizardComponent, Logger):
|
||||
try:
|
||||
self.password = client.get_password_for_storage_encryption()
|
||||
except Exception as e:
|
||||
# TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
|
||||
self.error = repr(e)
|
||||
self.error = repr(e) # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
|
||||
self.logger.error(repr(e))
|
||||
self.unlock_done()
|
||||
self.busy = False
|
||||
self.validate()
|
||||
|
||||
t = threading.Thread(target=unlock_task, args=(client,), daemon=True)
|
||||
t.start()
|
||||
|
||||
def unlock_done(self):
|
||||
self.logger.debug(f'Done unlock')
|
||||
self.busy = False
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
if self.password and not self.error:
|
||||
self.apply()
|
||||
@@ -1180,3 +1174,85 @@ class WCHWUnlock(WizardComponent, Logger):
|
||||
def apply(self):
|
||||
if self.valid:
|
||||
self.wizard_data['password'] = self.password
|
||||
|
||||
|
||||
class WCHWXPub(WizardComponent, Logger):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Retrieving extended public key from hardware'))
|
||||
Logger.__init__(self)
|
||||
self.plugins = wizard.plugins
|
||||
self.plugin = None
|
||||
self._busy = True
|
||||
|
||||
self.xpub = None
|
||||
self.root_fingerprint = None
|
||||
self.label = None
|
||||
self.soft_device_id = None
|
||||
|
||||
self.ok_l = WWLabel(_('Hardware keystore added to wallet'))
|
||||
self.ok_l.setAlignment(Qt.AlignCenter)
|
||||
self.layout().addWidget(self.ok_l)
|
||||
|
||||
def on_ready(self):
|
||||
_name, _info = self.wizard_data['hardware_device']
|
||||
self.plugin = self.plugins.get_plugin(_info.plugin_name)
|
||||
self.title = _('Retrieving extended public key from {} ({})').format(_info.model_name, _info.label)
|
||||
|
||||
device_id = _info.device.id_
|
||||
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
|
||||
if not client.handler:
|
||||
client.handler = self.plugin.create_handler(self.wizard)
|
||||
|
||||
cosigner = self.wizard.current_cosigner(self.wizard_data)
|
||||
xtype = cosigner['script_type']
|
||||
derivation = cosigner['derivation_path']
|
||||
|
||||
def get_xpub_task(client, derivation, xtype):
|
||||
try:
|
||||
self.xpub = self.get_xpub_from_client(client, derivation, xtype)
|
||||
self.root_fingerprint = client.request_root_fingerprint_from_device()
|
||||
self.label = client.label()
|
||||
self.soft_device_id = client.get_soft_device_id()
|
||||
except Exception as e:
|
||||
self.error = repr(e) # TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
|
||||
self.logger.error(repr(e))
|
||||
self.logger.debug(f'Done retrieve xpub: {self.xpub}')
|
||||
self.busy = False
|
||||
self.validate()
|
||||
|
||||
t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True)
|
||||
t.start()
|
||||
|
||||
def get_xpub_from_client(self, client, derivation, xtype): # override for HWW specific client if needed
|
||||
return client.get_xpub(derivation, xtype)
|
||||
|
||||
def validate(self):
|
||||
if self.xpub and not self.error:
|
||||
self.apply()
|
||||
valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
|
||||
if not valid:
|
||||
self.error = '\n'.join([
|
||||
_('Could not add hardware keystore to wallet'),
|
||||
error
|
||||
])
|
||||
self.valid = valid
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def apply(self):
|
||||
_name, _info = self.wizard_data['hardware_device']
|
||||
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
|
||||
cosigner_data['hw_type'] = _info.plugin_name
|
||||
cosigner_data['master_key'] = self.xpub
|
||||
cosigner_data['root_fingerprint'] = self.root_fingerprint
|
||||
cosigner_data['label'] = self.label
|
||||
cosigner_data['soft_device_id'] = self.soft_device_id
|
||||
|
||||
|
||||
class WCHWUninitialized(WizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Hardware not initialized'))
|
||||
|
||||
def on_ready(self):
|
||||
_name, _info = self.wizard_data['hardware_device']
|
||||
self.layout().addWidget(WWLabel(_('This {} is not initialized. Cannot continue').format(_info.model_name)))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import threading
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -15,7 +14,7 @@ from PyQt5.QtCore import Qt, QMetaObject, Q_RETURN_ARG, pyqtSlot
|
||||
from electrum.gui.qt.util import (
|
||||
WindowModalDialog,
|
||||
OkButton,
|
||||
ButtonsTextEdit, WWLabel,
|
||||
ButtonsTextEdit,
|
||||
)
|
||||
|
||||
from electrum.i18n import _
|
||||
@@ -24,9 +23,7 @@ from electrum.plugin import hook
|
||||
from .bitbox02 import BitBox02Plugin
|
||||
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
|
||||
from ..hw_wallet.plugin import only_hook_if_libraries_available
|
||||
from electrum.gui.qt.wizard.wizard import WizardComponent
|
||||
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock
|
||||
from electrum.logging import Logger
|
||||
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWUninitialized, WCHWXPub
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.gui.qt.wizard.wallet import QENewWalletWizard
|
||||
@@ -80,9 +77,9 @@ class Plugin(BitBox02Plugin, QtPluginBase):
|
||||
def extend_wizard(self, wizard: 'QENewWalletWizard'):
|
||||
super().extend_wizard(wizard)
|
||||
views = {
|
||||
'bitbox_start': { 'gui': WCScriptAndDerivation },
|
||||
'bitbox_xpub': { 'gui': WCBitboxXPub },
|
||||
'bitbox_not_initialized': {'gui': WCBitboxNope},
|
||||
'bitbox_start': {'gui': WCScriptAndDerivation},
|
||||
'bitbox_xpub': {'gui': WCHWXPub},
|
||||
'bitbox_not_initialized': {'gui': WCHWUninitialized},
|
||||
'bitbox_unlock': {'gui': WCHWUnlock}
|
||||
}
|
||||
wizard.navmap_merge(views)
|
||||
@@ -128,83 +125,3 @@ class BitBox02_Handler(QtHandlerBase):
|
||||
dialog.setLayout(vbox)
|
||||
dialog.exec_()
|
||||
return name.text().strip()
|
||||
|
||||
|
||||
# TODO: almost verbatim copy of trezor WCTrezorXPub, generalize!
|
||||
# problem: client.get_xpub is not uniform
|
||||
class WCBitboxXPub(WizardComponent, Logger):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information'))
|
||||
Logger.__init__(self)
|
||||
self.plugins = wizard.plugins
|
||||
self.plugin = self.plugins.get_plugin('bitbox02')
|
||||
self.busy_msg = _('Unlock your Bitbox02')
|
||||
self._busy = True
|
||||
|
||||
self.xpub = None
|
||||
self.root_fingerprint = None
|
||||
self.label = None
|
||||
self.soft_device_id = None
|
||||
|
||||
self.ok_l = WWLabel(_('Hardware keystore added to wallet'))
|
||||
self.ok_l.setAlignment(Qt.AlignCenter)
|
||||
self.layout().addWidget(self.ok_l)
|
||||
|
||||
def on_ready(self):
|
||||
_name, _info = self.wizard_data['hardware_device']
|
||||
device_id = _info.device.id_
|
||||
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
|
||||
if not client.handler:
|
||||
client.handler = self.plugin.create_handler(self.wizard)
|
||||
|
||||
cosigner = self.wizard.current_cosigner(self.wizard_data)
|
||||
xtype = cosigner['script_type']
|
||||
derivation = cosigner['derivation_path']
|
||||
|
||||
def get_xpub_task(client, derivation, xtype):
|
||||
try:
|
||||
self.xpub = client.get_xpub(derivation, xtype)
|
||||
self.root_fingerprint = client.request_root_fingerprint_from_device()
|
||||
self.label = client.label()
|
||||
self.soft_device_id = client.get_soft_device_id()
|
||||
except Exception as e:
|
||||
# TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
|
||||
self.error = repr(e)
|
||||
self.logger.error(repr(e))
|
||||
self.xpub_done()
|
||||
|
||||
t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True)
|
||||
t.start()
|
||||
|
||||
def xpub_done(self):
|
||||
self.logger.debug(f'Done retrieve xpub: {self.xpub}')
|
||||
self.busy = False
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
if self.xpub and not self.error:
|
||||
self.apply()
|
||||
valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
|
||||
if not valid:
|
||||
self.error = '\n'.join([
|
||||
_('Could not add hardware keystore to wallet'),
|
||||
error
|
||||
])
|
||||
self.valid = valid
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def apply(self):
|
||||
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
|
||||
cosigner_data['hw_type'] = 'bitbox02'
|
||||
cosigner_data['master_key'] = self.xpub
|
||||
cosigner_data['root_fingerprint'] = self.root_fingerprint
|
||||
cosigner_data['label'] = self.label
|
||||
cosigner_data['soft_device_id'] = self.soft_device_id
|
||||
|
||||
|
||||
class WCBitboxNope(WizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Bitbox02 not initialized'))
|
||||
self.layout().addWidget(WWLabel(_('This Bitbox02 is not initialized. Cannot continue')))
|
||||
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import threading
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import hook
|
||||
from electrum.wallet import Standard_Wallet
|
||||
from electrum.logging import Logger
|
||||
|
||||
from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase
|
||||
from electrum.plugins.hw_wallet import plugin
|
||||
from electrum.gui.qt.util import WWLabel
|
||||
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock
|
||||
from electrum.gui.qt.wizard.wizard import WizardComponent
|
||||
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub, WCHWUninitialized
|
||||
|
||||
from .jade import JadePlugin
|
||||
|
||||
@@ -47,9 +43,9 @@ class Plugin(JadePlugin, QtPluginBase):
|
||||
def extend_wizard(self, wizard: 'QENewWalletWizard'):
|
||||
super().extend_wizard(wizard)
|
||||
views = {
|
||||
'jade_start': { 'gui': WCScriptAndDerivation },
|
||||
'jade_xpub': { 'gui': WCJadeXPub },
|
||||
'jade_not_initialized': {'gui': WCJadeNope},
|
||||
'jade_start': {'gui': WCScriptAndDerivation},
|
||||
'jade_xpub': {'gui': WCHWXPub},
|
||||
'jade_not_initialized': {'gui': WCHWUninitialized},
|
||||
'jade_unlock': {'gui': WCHWUnlock}
|
||||
}
|
||||
wizard.navmap_merge(views)
|
||||
@@ -64,82 +60,3 @@ class Jade_Handler(QtHandlerBase):
|
||||
def __init__(self, win):
|
||||
super(Jade_Handler, self).__init__(win, 'Jade')
|
||||
|
||||
|
||||
# TODO: almost verbatim copy of trezor WCTrezorXPub, generalize!
|
||||
# problem: client.get_xpub is not uniform
|
||||
class WCJadeXPub(WizardComponent, Logger):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information'))
|
||||
Logger.__init__(self)
|
||||
self.plugins = wizard.plugins
|
||||
self.plugin = self.plugins.get_plugin('jade')
|
||||
self.busy_msg = _('Unlock your Jade')
|
||||
self._busy = True
|
||||
|
||||
self.xpub = None
|
||||
self.root_fingerprint = None
|
||||
self.label = None
|
||||
self.soft_device_id = None
|
||||
|
||||
self.ok_l = WWLabel(_('Hardware keystore added to wallet'))
|
||||
self.ok_l.setAlignment(Qt.AlignCenter)
|
||||
self.layout().addWidget(self.ok_l)
|
||||
|
||||
def on_ready(self):
|
||||
_name, _info = self.wizard_data['hardware_device']
|
||||
device_id = _info.device.id_
|
||||
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
|
||||
if not client.handler:
|
||||
client.handler = self.plugin.create_handler(self.wizard)
|
||||
|
||||
cosigner = self.wizard.current_cosigner(self.wizard_data)
|
||||
xtype = cosigner['script_type']
|
||||
derivation = cosigner['derivation_path']
|
||||
|
||||
def get_xpub_task(client, derivation, xtype):
|
||||
try:
|
||||
self.xpub = client.get_xpub(derivation, xtype)
|
||||
self.root_fingerprint = client.request_root_fingerprint_from_device()
|
||||
self.label = client.label()
|
||||
self.soft_device_id = client.get_soft_device_id()
|
||||
except Exception as e:
|
||||
# TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
|
||||
self.error = repr(e)
|
||||
self.logger.error(repr(e))
|
||||
self.xpub_done()
|
||||
|
||||
t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True)
|
||||
t.start()
|
||||
|
||||
def xpub_done(self):
|
||||
self.logger.debug(f'Done retrieve xpub: {self.xpub}')
|
||||
self.busy = False
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
if self.xpub and not self.error:
|
||||
self.apply()
|
||||
valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
|
||||
if not valid:
|
||||
self.error = '\n'.join([
|
||||
_('Could not add hardware keystore to wallet'),
|
||||
error
|
||||
])
|
||||
self.valid = valid
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def apply(self):
|
||||
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
|
||||
cosigner_data['hw_type'] = 'jade'
|
||||
cosigner_data['master_key'] = self.xpub
|
||||
cosigner_data['root_fingerprint'] = self.root_fingerprint
|
||||
cosigner_data['label'] = self.label
|
||||
cosigner_data['soft_device_id'] = self.soft_device_id
|
||||
|
||||
|
||||
class WCJadeNope(WizardComponent):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Jade not initialized'))
|
||||
self.layout().addWidget(WWLabel(_('This Jade is not initialized. Cannot continue')))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from electrum.plugins.hw_wallet.plugin import only_hook_if_libraries_available
|
||||
|
||||
from electrum.gui.qt.util import (WindowModalDialog, WWLabel, Buttons, CancelButton,
|
||||
OkButton, CloseButton, PasswordLineEdit, getOpenFileName, ChoiceWidget)
|
||||
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock
|
||||
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWUnlock, WCHWXPub
|
||||
from electrum.gui.qt.wizard.wizard import WizardComponent
|
||||
|
||||
from .trezor import (TrezorPlugin, TIM_NEW, TIM_RECOVER, TrezorInitSettings,
|
||||
@@ -473,11 +473,11 @@ class Plugin(TrezorPlugin, QtPlugin):
|
||||
def extend_wizard(self, wizard: 'QENewWalletWizard'):
|
||||
super().extend_wizard(wizard)
|
||||
views = {
|
||||
'trezor_start': { 'gui': WCScriptAndDerivation },
|
||||
'trezor_xpub': { 'gui': WCTrezorXPub },
|
||||
'trezor_not_initialized': { 'gui': WCTrezorInitMethod },
|
||||
'trezor_choose_new_recover': { 'gui': WCTrezorInitParams },
|
||||
'trezor_do_init': { 'gui': WCTrezorInit },
|
||||
'trezor_start': {'gui': WCScriptAndDerivation},
|
||||
'trezor_xpub': {'gui': WCTrezorXPub},
|
||||
'trezor_not_initialized': {'gui': WCTrezorInitMethod},
|
||||
'trezor_choose_new_recover': {'gui': WCTrezorInitParams},
|
||||
'trezor_do_init': {'gui': WCTrezorInit},
|
||||
'trezor_unlock': {'gui': WCHWUnlock},
|
||||
}
|
||||
wizard.navmap_merge(views)
|
||||
@@ -801,73 +801,12 @@ class SettingsDialog(WindowModalDialog):
|
||||
invoke_client(None)
|
||||
|
||||
|
||||
class WCTrezorXPub(WizardComponent, Logger):
|
||||
class WCTrezorXPub(WCHWXPub):
|
||||
def __init__(self, parent, wizard):
|
||||
WizardComponent.__init__(self, parent, wizard, title=_('Hardware wallet information'))
|
||||
Logger.__init__(self)
|
||||
self.plugins = wizard.plugins
|
||||
self.plugin = self.plugins.get_plugin('trezor')
|
||||
self._busy = True
|
||||
WCHWXPub.__init__(self, parent, wizard)
|
||||
|
||||
self.xpub = None
|
||||
self.root_fingerprint = None
|
||||
self.label = None
|
||||
self.soft_device_id = None
|
||||
|
||||
self.ok_l = WWLabel(_('Hardware keystore added to wallet'))
|
||||
self.ok_l.setAlignment(Qt.AlignCenter)
|
||||
self.layout().addWidget(self.ok_l)
|
||||
|
||||
def on_ready(self):
|
||||
_name, _info = self.wizard_data['hardware_device']
|
||||
device_id = _info.device.id_
|
||||
client = self.plugins.device_manager.client_by_id(device_id, scan_now=False)
|
||||
client.handler = self.plugin.create_handler(self.wizard)
|
||||
|
||||
cosigner = self.wizard.current_cosigner(self.wizard_data)
|
||||
xtype = cosigner['script_type']
|
||||
derivation = cosigner['derivation_path']
|
||||
|
||||
def get_xpub_task(client, derivation, xtype):
|
||||
try:
|
||||
self.xpub = client.get_xpub(derivation, xtype, True)
|
||||
self.root_fingerprint = client.request_root_fingerprint_from_device()
|
||||
self.label = client.label()
|
||||
self.soft_device_id = client.get_soft_device_id()
|
||||
except Exception as e:
|
||||
# TODO: handle user interaction exceptions (e.g. invalid pin) more gracefully
|
||||
self.error = repr(e)
|
||||
self.logger.error(repr(e))
|
||||
self.xpub_done()
|
||||
|
||||
t = threading.Thread(target=get_xpub_task, args=(client, derivation, xtype), daemon=True)
|
||||
t.start()
|
||||
|
||||
def xpub_done(self):
|
||||
self.logger.debug(f'Done retrieve xpub: {self.xpub}')
|
||||
self.busy = False
|
||||
self.validate()
|
||||
|
||||
def validate(self):
|
||||
if self.xpub and not self.error:
|
||||
self.apply()
|
||||
valid, error = self.wizard.check_multisig_constraints(self.wizard_data)
|
||||
if not valid:
|
||||
self.error = '\n'.join([
|
||||
_('Could not add hardware keystore to wallet'),
|
||||
error
|
||||
])
|
||||
self.valid = valid
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def apply(self):
|
||||
cosigner_data = self.wizard.current_cosigner(self.wizard_data)
|
||||
cosigner_data['hw_type'] = 'trezor'
|
||||
cosigner_data['master_key'] = self.xpub
|
||||
cosigner_data['root_fingerprint'] = self.root_fingerprint
|
||||
cosigner_data['label'] = self.label
|
||||
cosigner_data['soft_device_id'] = self.soft_device_id
|
||||
def get_xpub_from_client(self, client, derivation, xtype):
|
||||
return client.get_xpub(derivation, xtype, True)
|
||||
|
||||
|
||||
class WCTrezorInitMethod(WizardComponent, Logger):
|
||||
|
||||
Reference in New Issue
Block a user