Browse Source

Merge pull request #8888 from SomberNight/202402_jsonrpc_errors

cli/rpc: nicer error messages and error-passing
master
ThomasV 2 years ago committed by GitHub
parent
commit
9799603779
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 94
      electrum/commands.py
  2. 30
      electrum/daemon.py
  3. 21
      electrum/util.py
  4. 24
      electrum/wallet.py
  5. 34
      run_electrum

94
electrum/commands.py

@ -42,7 +42,8 @@ import os
from .import util, ecc from .import util, ecc
from .util import (bfh, format_satoshis, json_decode, json_normalize, from .util import (bfh, format_satoshis, json_decode, json_normalize,
is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal) is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal,
UserFacingException)
from . import bitcoin from . import bitcoin
from .bitcoin import is_address, hash_160, COIN from .bitcoin import is_address, hash_160, COIN
from .bip32 import BIP32Node from .bip32 import BIP32Node
@ -77,7 +78,7 @@ if TYPE_CHECKING:
known_commands = {} # type: Dict[str, Command] known_commands = {} # type: Dict[str, Command]
class NotSynchronizedException(Exception): class NotSynchronizedException(UserFacingException):
pass pass
@ -144,21 +145,21 @@ def command(s):
if isinstance(wallet, str): if isinstance(wallet, str):
wallet = daemon.get_wallet(wallet) wallet = daemon.get_wallet(wallet)
if wallet is None: if wallet is None:
raise Exception('wallet not loaded') raise UserFacingException('wallet not loaded')
kwargs['wallet'] = wallet kwargs['wallet'] = wallet
if cmd.requires_password and password is None and wallet.has_password(): if cmd.requires_password and password is None and wallet.has_password():
password = wallet.get_unlocked_password() password = wallet.get_unlocked_password()
if password: if password:
kwargs['password'] = password kwargs['password'] = password
else: else:
raise Exception('Password required. Unlock the wallet, or add a --password option to your command') raise UserFacingException('Password required. Unlock the wallet, or add a --password option to your command')
wallet = kwargs.get('wallet') # type: Optional[Abstract_Wallet] wallet = kwargs.get('wallet') # type: Optional[Abstract_Wallet]
if cmd.requires_wallet and not wallet: if cmd.requires_wallet and not wallet:
raise Exception('wallet not loaded') raise UserFacingException('wallet not loaded')
if cmd.requires_password and password is None and wallet.has_password(): if cmd.requires_password and password is None and wallet.has_password():
raise Exception('Password required') raise UserFacingException('Password required')
if cmd.requires_lightning and (not wallet or not wallet.has_lightning()): if cmd.requires_lightning and (not wallet or not wallet.has_lightning()):
raise Exception('Lightning not enabled in this wallet') raise UserFacingException('Lightning not enabled in this wallet')
return await func(*args, **kwargs) return await func(*args, **kwargs)
return func_wrapper return func_wrapper
return decorator return decorator
@ -251,7 +252,7 @@ class Commands:
""" """
wallet = self.daemon.load_wallet(wallet_path, password, upgrade=True) wallet = self.daemon.load_wallet(wallet_path, password, upgrade=True)
if wallet is None: if wallet is None:
raise Exception('could not load wallet') raise UserFacingException('could not load wallet')
if unlock: if unlock:
wallet.unlock(password) wallet.unlock(password)
run_hook('load_wallet', wallet, None) run_hook('load_wallet', wallet, None)
@ -301,7 +302,7 @@ class Commands:
async def password(self, password=None, new_password=None, encrypt_file=None, wallet: Abstract_Wallet = None): async def password(self, password=None, new_password=None, encrypt_file=None, wallet: Abstract_Wallet = None):
"""Change wallet password. """ """Change wallet password. """
if wallet.storage.is_encrypted_with_hw_device() and new_password: if wallet.storage.is_encrypted_with_hw_device() and new_password:
raise Exception("Can't change the password of a wallet encrypted with a hw device.") raise UserFacingException("Can't change the password of a wallet encrypted with a hw device.")
if encrypt_file is None: if encrypt_file is None:
if not password and new_password: if not password and new_password:
# currently no password, setting one now: we encrypt by default # currently no password, setting one now: we encrypt by default
@ -403,18 +404,18 @@ class Commands:
keypairs = {} keypairs = {}
inputs = [] # type: List[PartialTxInput] inputs = [] # type: List[PartialTxInput]
locktime = jsontx.get('locktime', 0) locktime = jsontx.get('locktime', 0)
for txin_dict in jsontx.get('inputs'): for txin_idx, txin_dict in enumerate(jsontx.get('inputs')):
if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None: if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n'])) prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
elif txin_dict.get('output'): elif txin_dict.get('output'):
prevout = TxOutpoint.from_str(txin_dict['output']) prevout = TxOutpoint.from_str(txin_dict['output'])
else: else:
raise Exception("missing prevout for txin") raise UserFacingException(f"missing prevout for txin {txin_idx}")
txin = PartialTxInput(prevout=prevout) txin = PartialTxInput(prevout=prevout)
try: try:
txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats']) txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats'])
except KeyError: except KeyError:
raise Exception("missing 'value_sats' field for txin") raise UserFacingException(f"missing 'value_sats' field for txin {txin_idx}")
nsequence = txin_dict.get('nsequence', None) nsequence = txin_dict.get('nsequence', None)
if nsequence is not None: if nsequence is not None:
txin.nsequence = nsequence txin.nsequence = nsequence
@ -428,15 +429,15 @@ class Commands:
inputs.append(txin) inputs.append(txin)
outputs = [] # type: List[PartialTxOutput] outputs = [] # type: List[PartialTxOutput]
for txout_dict in jsontx.get('outputs'): for txout_idx, txout_dict in enumerate(jsontx.get('outputs')):
try: try:
txout_addr = txout_dict['address'] txout_addr = txout_dict['address']
except KeyError: except KeyError:
raise Exception("missing 'address' field for txout") raise UserFacingException(f"missing 'address' field for txout {txout_idx}")
try: try:
txout_val = int(txout_dict.get('value') or txout_dict['value_sats']) txout_val = int(txout_dict.get('value') or txout_dict['value_sats'])
except KeyError: except KeyError:
raise Exception("missing 'value_sats' field for txout") raise UserFacingException(f"missing 'value_sats' field for txout {txout_idx}")
txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val) txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val)
outputs.append(txout) outputs.append(txout)
@ -651,7 +652,7 @@ class Commands:
try: try:
node = BIP32Node.from_xkey(xkey) node = BIP32Node.from_xkey(xkey)
except Exception: except Exception:
raise Exception('xkey should be a master public/private key') raise UserFacingException('xkey should be a master public/private key')
return node._replace(xtype=xtype).to_xkey() return node._replace(xtype=xtype).to_xkey()
@command('wp') @command('wp')
@ -672,12 +673,12 @@ class Commands:
out = "Error: " + repr(e) out = "Error: " + repr(e)
return out return out
def _resolver(self, x, wallet): def _resolver(self, x, wallet: Abstract_Wallet):
if x is None: if x is None:
return None return None
out = wallet.contacts.resolve(x) out = wallet.contacts.resolve(x)
if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False: if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
raise Exception('cannot verify alias', x) raise UserFacingException(f"cannot verify alias: {x}")
return out['address'] return out['address']
@command('n') @command('n')
@ -802,13 +803,13 @@ class Commands:
if is_hash256_str(tx): # txid if is_hash256_str(tx): # txid
tx = wallet.db.get_transaction(tx) tx = wallet.db.get_transaction(tx)
if tx is None: if tx is None:
raise Exception("Transaction not in wallet.") raise UserFacingException("Transaction not in wallet.")
else: # raw tx else: # raw tx
try: try:
tx = Transaction(tx) tx = Transaction(tx)
tx.deserialize() tx.deserialize()
except transaction.SerializationError as e: except transaction.SerializationError as e:
raise Exception(f"Failed to deserialize transaction: {e}") from e raise UserFacingException(f"Failed to deserialize transaction: {e}") from e
domain_coins = from_coins.split(',') if from_coins else None domain_coins = from_coins.split(',') if from_coins else None
coins = wallet.get_spendable_coins(None) coins = wallet.get_spendable_coins(None)
if domain_coins is not None: if domain_coins is not None:
@ -891,20 +892,20 @@ class Commands:
if raw: if raw:
tx = Transaction(raw) tx = Transaction(raw)
else: else:
raise Exception("Unknown transaction") raise UserFacingException("Unknown transaction")
if tx.txid() != txid: if tx.txid() != txid:
raise Exception("Mismatching txid") raise UserFacingException("Mismatching txid")
return tx.serialize() return tx.serialize()
@command('') @command('')
async def encrypt(self, pubkey, message) -> str: async def encrypt(self, pubkey, message) -> str:
"""Encrypt a message with a public key. Use quotes if the message contains whitespaces.""" """Encrypt a message with a public key. Use quotes if the message contains whitespaces."""
if not is_hex_str(pubkey): if not is_hex_str(pubkey):
raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}") raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
try: try:
message = to_bytes(message) message = to_bytes(message)
except TypeError: except TypeError:
raise Exception(f"message must be a string-like object instead of {repr(message)}") raise UserFacingException(f"message must be a string-like object instead of {repr(message)}")
public_key = ecc.ECPubkey(bfh(pubkey)) public_key = ecc.ECPubkey(bfh(pubkey))
encrypted = public_key.encrypt_message(message) encrypted = public_key.encrypt_message(message)
return encrypted.decode('utf-8') return encrypted.decode('utf-8')
@ -913,9 +914,9 @@ class Commands:
async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str: async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str:
"""Decrypt a message encrypted with a public key.""" """Decrypt a message encrypted with a public key."""
if not is_hex_str(pubkey): if not is_hex_str(pubkey):
raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}") raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
if not isinstance(encrypted, (str, bytes, bytearray)): if not isinstance(encrypted, (str, bytes, bytearray)):
raise Exception(f"encrypted must be a string-like object instead of {repr(encrypted)}") raise UserFacingException(f"encrypted must be a string-like object instead of {repr(encrypted)}")
decrypted = wallet.decrypt_message(pubkey, encrypted, password) decrypted = wallet.decrypt_message(pubkey, encrypted, password)
return decrypted.decode('utf-8') return decrypted.decode('utf-8')
@ -924,7 +925,7 @@ class Commands:
"""Returns a payment request""" """Returns a payment request"""
r = wallet.get_request(request_id) r = wallet.get_request(request_id)
if not r: if not r:
raise Exception("Request not found") raise UserFacingException("Request not found")
return wallet.export_request(r) return wallet.export_request(r)
@command('w') @command('w')
@ -932,7 +933,7 @@ class Commands:
"""Returns an invoice (request for outgoing payment)""" """Returns an invoice (request for outgoing payment)"""
r = wallet.get_invoice(invoice_id) r = wallet.get_invoice(invoice_id)
if not r: if not r:
raise Exception("Request not found") raise UserFacingException("Request not found")
return wallet.export_invoice(r) return wallet.export_invoice(r)
#@command('w') #@command('w')
@ -976,13 +977,14 @@ class Commands:
async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None): async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
"""Change the gap limit of the wallet.""" """Change the gap limit of the wallet."""
if not iknowwhatimdoing: if not iknowwhatimdoing:
raise Exception("WARNING: Are you SURE you want to change the gap limit?\n" raise UserFacingException(
"It makes recovering your wallet from seed difficult!\n" "WARNING: Are you SURE you want to change the gap limit?\n"
"Please do your research and make sure you understand the implications.\n" "It makes recovering your wallet from seed difficult!\n"
"Typically only merchants and power users might want to do this.\n" "Please do your research and make sure you understand the implications.\n"
"To proceed, try again, with the --iknowwhatimdoing option.") "Typically only merchants and power users might want to do this.\n"
"To proceed, try again, with the --iknowwhatimdoing option.")
if not isinstance(wallet, Deterministic_Wallet): if not isinstance(wallet, Deterministic_Wallet):
raise Exception("This wallet is not deterministic.") raise UserFacingException("This wallet is not deterministic.")
return wallet.change_gap_limit(new_limit) return wallet.change_gap_limit(new_limit)
@command('wn') @command('wn')
@ -991,7 +993,7 @@ class Commands:
known addresses in the wallet. known addresses in the wallet.
""" """
if not isinstance(wallet, Deterministic_Wallet): if not isinstance(wallet, Deterministic_Wallet):
raise Exception("This wallet is not deterministic.") raise UserFacingException("This wallet is not deterministic.")
if not wallet.is_up_to_date(): if not wallet.is_up_to_date():
raise NotSynchronizedException("Wallet not fully synchronized.") raise NotSynchronizedException("Wallet not fully synchronized.")
return wallet.min_acceptable_gap() return wallet.min_acceptable_gap()
@ -1091,11 +1093,12 @@ class Commands:
transactions. transactions.
""" """
if not is_hash256_str(txid): if not is_hash256_str(txid):
raise Exception(f"{repr(txid)} is not a txid") raise UserFacingException(f"{repr(txid)} is not a txid")
height = wallet.adb.get_tx_height(txid).height height = wallet.adb.get_tx_height(txid).height
if height != TX_HEIGHT_LOCAL: if height != TX_HEIGHT_LOCAL:
raise Exception(f'Only local transactions can be removed. ' raise UserFacingException(
f'This tx has height: {height} != {TX_HEIGHT_LOCAL}') f'Only local transactions can be removed. '
f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
wallet.adb.remove_transaction(txid) wallet.adb.remove_transaction(txid)
wallet.save_db() wallet.save_db()
@ -1105,9 +1108,9 @@ class Commands:
The transaction must be related to the wallet. The transaction must be related to the wallet.
""" """
if not is_hash256_str(txid): if not is_hash256_str(txid):
raise Exception(f"{repr(txid)} is not a txid") raise UserFacingException(f"{repr(txid)} is not a txid")
if not wallet.db.get_transaction(txid): if not wallet.db.get_transaction(txid):
raise Exception("Transaction not in wallet.") raise UserFacingException("Transaction not in wallet.")
return { return {
"confirmations": wallet.adb.get_tx_height(txid).conf, "confirmations": wallet.adb.get_tx_height(txid).conf,
} }
@ -1253,8 +1256,9 @@ class Commands:
async def get_channel_ctx(self, channel_point, iknowwhatimdoing=False, wallet: Abstract_Wallet = None): async def get_channel_ctx(self, channel_point, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
""" return the current commitment transaction of a channel """ """ return the current commitment transaction of a channel """
if not iknowwhatimdoing: if not iknowwhatimdoing:
raise Exception("WARNING: this command is potentially unsafe.\n" raise UserFacingException(
"To proceed, try again, with the --iknowwhatimdoing option.") "WARNING: this command is potentially unsafe.\n"
"To proceed, try again, with the --iknowwhatimdoing option.")
txid, index = channel_point.split(':') txid, index = channel_point.split(':')
chan_id, _ = channel_id_from_funding_tx(txid, int(index)) chan_id, _ = channel_id_from_funding_tx(txid, int(index))
chan = wallet.lnworker.channels[chan_id] chan = wallet.lnworker.channels[chan_id]
@ -1354,7 +1358,7 @@ class Commands:
configured exchange rate source. configured exchange rate source.
""" """
if not self.daemon.fx.is_enabled(): if not self.daemon.fx.is_enabled():
raise Exception("FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'") raise UserFacingException("FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'")
# Currency codes are uppercase # Currency codes are uppercase
from_ccy = from_ccy.upper() from_ccy = from_ccy.upper()
to_ccy = to_ccy.upper() to_ccy = to_ccy.upper()
@ -1368,9 +1372,9 @@ class Commands:
rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy) rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy)
# Test if currencies exist # Test if currencies exist
if rate_from.is_nan(): if rate_from.is_nan():
raise Exception(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable') raise UserFacingException(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable')
if rate_to.is_nan(): if rate_to.is_nan():
raise Exception(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable') raise UserFacingException(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable')
# Conversion # Conversion
try: try:
from_amount = to_decimal(from_amount) from_amount = to_decimal(from_amount)

30
electrum/daemon.py

@ -35,6 +35,7 @@ from base64 import b64decode, b64encode
from collections import defaultdict from collections import defaultdict
import json import json
import socket import socket
from enum import IntEnum
import aiohttp import aiohttp
from aiohttp import web, client_exceptions from aiohttp import web, client_exceptions
@ -44,7 +45,7 @@ from . import util
from .network import Network from .network import Network
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword) from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword)
from .invoices import PR_PAID, PR_EXPIRED from .invoices import PR_PAID, PR_EXPIRED
from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError
from .util import EventListener, event_listener from .util import EventListener, event_listener
from .wallet import Wallet, Abstract_Wallet from .wallet import Wallet, Abstract_Wallet
from .storage import WalletStorage from .storage import WalletStorage
@ -251,11 +252,20 @@ class AuthenticatedServer(Logger):
response['result'] = await f(**params) response['result'] = await f(**params)
else: else:
response['result'] = await f(*params) response['result'] = await f(*params)
except UserFacingException as e:
response['error'] = {
'code': JsonRPCError.Codes.USERFACING,
'message': str(e),
}
except BaseException as e: except BaseException as e:
self.logger.exception("internal error while executing RPC") self.logger.exception("internal error while executing RPC")
response['error'] = { response['error'] = {
'code': 1, 'code': JsonRPCError.Codes.INTERNAL,
'message': str(e), 'message': "internal error while executing RPC",
'data': {
"exception": repr(e),
"traceback": "".join(traceback.format_exception(e)),
},
} }
return web.json_response(response) return web.json_response(response)
@ -325,12 +335,11 @@ class CommandsServer(AuthenticatedServer):
if hasattr(self.daemon.gui_object, 'new_window'): if hasattr(self.daemon.gui_object, 'new_window'):
path = config_options.get('wallet_path') or self.config.get_wallet_path(use_gui_last_wallet=True) path = config_options.get('wallet_path') or self.config.get_wallet_path(use_gui_last_wallet=True)
self.daemon.gui_object.new_window(path, config_options.get('url')) self.daemon.gui_object.new_window(path, config_options.get('url'))
response = "ok" return True
else: else:
response = "error: current GUI does not support multiple windows" raise UserFacingException("error: current GUI does not support multiple windows")
else: else:
response = "Error: Electrum is running in daemon mode. Please stop the daemon first." raise UserFacingException("error: Electrum is running in daemon mode. Please stop the daemon first.")
return response
async def run_cmdline(self, config_options): async def run_cmdline(self, config_options):
cmdname = config_options['cmd'] cmdname = config_options['cmd']
@ -348,11 +357,8 @@ class CommandsServer(AuthenticatedServer):
elif 'wallet' in cmd.options: elif 'wallet' in cmd.options:
kwargs['wallet'] = config_options.get('wallet_path') kwargs['wallet'] = config_options.get('wallet_path')
func = getattr(self.cmd_runner, cmd.name) func = getattr(self.cmd_runner, cmd.name)
# fixme: not sure how to retrieve message in jsonrpcclient # execute requested command now. note: cmd can raise, the caller (self.handle) will wrap it.
try: result = await func(*args, **kwargs)
result = await func(*args, **kwargs)
except Exception as e:
result = {'error':str(e)}
return result return result

21
electrum/util.py

@ -50,6 +50,7 @@ import functools
from functools import partial from functools import partial
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
import socket import socket
import enum
import attr import attr
import aiohttp import aiohttp
@ -1932,6 +1933,20 @@ class MySocksProxy(aiorpcx.SOCKSProxy):
return ret return ret
class JsonRPCError(Exception):
class Codes(enum.IntEnum):
# application-specific error codes
USERFACING = 1
INTERNAL = 2
def __init__(self, *, code: int, message: str, data: Optional[dict] = None):
Exception.__init__(self)
self.code = code
self.message = message
self.data = data
class JsonRPCClient: class JsonRPCClient:
def __init__(self, session: aiohttp.ClientSession, url: str): def __init__(self, session: aiohttp.ClientSession, url: str):
@ -1940,6 +1955,10 @@ class JsonRPCClient:
self._id = 0 self._id = 0
async def request(self, endpoint, *args): async def request(self, endpoint, *args):
"""Send request to server, parse and return result.
note: parsing code is naive, the server is assumed to be well-behaved.
Up to the caller to handle exceptions, including those arising from parsing errors.
"""
self._id += 1 self._id += 1
data = ('{"jsonrpc": "2.0", "id":"%d", "method": "%s", "params": %s }' data = ('{"jsonrpc": "2.0", "id":"%d", "method": "%s", "params": %s }'
% (self._id, endpoint, json.dumps(args))) % (self._id, endpoint, json.dumps(args)))
@ -1949,7 +1968,7 @@ class JsonRPCClient:
result = r.get('result') result = r.get('result')
error = r.get('error') error = r.get('error')
if error: if error:
return 'Error: ' + str(error) raise JsonRPCError(code=error["code"], message=error["message"], data=error.get("data"))
else: else:
return result return result
else: else:

24
electrum/wallet.py

@ -817,7 +817,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return serialized_privkey return serialized_privkey
def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str: def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
raise Exception("this wallet is not deterministic") raise UserFacingException("this wallet is not deterministic")
@abstractmethod @abstractmethod
def get_public_keys(self, address: str) -> Sequence[str]: def get_public_keys(self, address: str) -> Sequence[str]:
@ -1440,7 +1440,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# FIXME: Lightning capital gains would requires FIFO # FIXME: Lightning capital gains would requires FIFO
if (from_timestamp is not None or to_timestamp is not None) \ if (from_timestamp is not None or to_timestamp is not None) \
and (from_height is not None or to_height is not None): and (from_height is not None or to_height is not None):
raise Exception('timestamp and block height based filtering cannot be used together') raise UserFacingException('timestamp and block height based filtering cannot be used together')
show_fiat = fx and fx.is_enabled() and fx.has_history() show_fiat = fx and fx.is_enabled() and fx.has_history()
out = [] out = []
@ -2597,17 +2597,17 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return choice return choice
def create_new_address(self, for_change: bool = False): def create_new_address(self, for_change: bool = False):
raise Exception("this wallet cannot generate new addresses") raise UserFacingException("this wallet cannot generate new addresses")
def import_address(self, address: str) -> str: def import_address(self, address: str) -> str:
raise Exception("this wallet cannot import addresses") raise UserFacingException("this wallet cannot import addresses")
def import_addresses(self, addresses: List[str], *, def import_addresses(self, addresses: List[str], *,
write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]: write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
raise Exception("this wallet cannot import addresses") raise UserFacingException("this wallet cannot import addresses")
def delete_address(self, address: str) -> None: def delete_address(self, address: str) -> None:
raise Exception("this wallet cannot delete addresses") raise UserFacingException("this wallet cannot delete addresses")
def get_request_URI(self, req: Request) -> Optional[str]: def get_request_URI(self, req: Request) -> Optional[str]:
lightning_invoice = None lightning_invoice = None
@ -3056,7 +3056,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
) -> PartialTransaction: ) -> PartialTransaction:
"""Helper function for make_unsigned_transaction.""" """Helper function for make_unsigned_transaction."""
if fee is not None and feerate is not None: if fee is not None and feerate is not None:
raise Exception("Cannot specify both 'fee' and 'feerate' at the same time!") raise UserFacingException("Cannot specify both 'fee' and 'feerate' at the same time!")
coins = self.get_spendable_coins(domain_addr, nonlocal_only=nonlocal_only) coins = self.get_spendable_coins(domain_addr, nonlocal_only=nonlocal_only)
if domain_coins is not None: if domain_coins is not None:
coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
@ -3905,7 +3905,7 @@ def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=N
"""Create a new wallet""" """Create a new wallet"""
storage = WalletStorage(path) storage = WalletStorage(path)
if storage.file_exists(): if storage.file_exists():
raise Exception("Remove the existing wallet first!") raise UserFacingException("Remove the existing wallet first!")
db = WalletDB('', storage=storage, upgrade=True) db = WalletDB('', storage=storage, upgrade=True)
seed = Mnemonic('en').make_seed(seed_type=seed_type) seed = Mnemonic('en').make_seed(seed_type=seed_type)
@ -3942,7 +3942,7 @@ def restore_wallet_from_text(
else: else:
storage = WalletStorage(path) storage = WalletStorage(path)
if storage.file_exists(): if storage.file_exists():
raise Exception("Remove the existing wallet first!") raise UserFacingException("Remove the existing wallet first!")
if encrypt_file is None: if encrypt_file is None:
encrypt_file = True encrypt_file = True
db = WalletDB('', storage=storage, upgrade=True) db = WalletDB('', storage=storage, upgrade=True)
@ -3953,7 +3953,7 @@ def restore_wallet_from_text(
good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False) good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
# FIXME tell user about bad_inputs # FIXME tell user about bad_inputs
if not good_inputs: if not good_inputs:
raise Exception("None of the given addresses can be imported") raise UserFacingException("None of the given addresses can be imported")
elif keystore.is_private_key_list(text, allow_spaces_inside_key=False): elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
k = keystore.Imported_KeyStore({}) k = keystore.Imported_KeyStore({})
db.put('keystore', k.dump()) db.put('keystore', k.dump())
@ -3962,7 +3962,7 @@ def restore_wallet_from_text(
good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False) good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
# FIXME tell user about bad_inputs # FIXME tell user about bad_inputs
if not good_inputs: if not good_inputs:
raise Exception("None of the given privkeys can be imported") raise UserFacingException("None of the given privkeys can be imported")
else: else:
if keystore.is_master_key(text): if keystore.is_master_key(text):
k = keystore.from_master_key(text) k = keystore.from_master_key(text)
@ -3971,7 +3971,7 @@ def restore_wallet_from_text(
if k.can_have_deterministic_lightning_xprv(): if k.can_have_deterministic_lightning_xprv():
db.put('lightning_xprv', k.get_lightning_xprv(None)) db.put('lightning_xprv', k.get_lightning_xprv(None))
else: else:
raise Exception("Seed or key not recognized") raise UserFacingException("Seed or key not recognized")
db.put('keystore', k.dump()) db.put('keystore', k.dump())
db.put('wallet_type', 'standard') db.put('wallet_type', 'standard')
if gap_limit is not None: if gap_limit is not None:

34
run_electrum

@ -104,7 +104,7 @@ from electrum.util import InvalidPassword
from electrum.commands import get_parser, known_commands, Commands, config_variables from electrum.commands import get_parser, known_commands, Commands, config_variables
from electrum import daemon from electrum import daemon
from electrum import keystore from electrum import keystore
from electrum.util import create_and_start_event_loop from electrum.util import create_and_start_event_loop, UserFacingException, JsonRPCError
from electrum.i18n import set_language from electrum.i18n import set_language
if TYPE_CHECKING: if TYPE_CHECKING:
@ -462,7 +462,17 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
else: else:
sys_exit(0) sys_exit(0)
else: else:
result = daemon.request(config, 'gui', (config_options,)) try:
result = daemon.request(config, 'gui', (config_options,))
except JsonRPCError as e:
if e.code == JsonRPCError.Codes.USERFACING:
print_stderr(e.message)
elif e.code == JsonRPCError.Codes.INTERNAL:
print_stderr("(inside daemon): " + e.data["traceback"])
print_stderr(e.message)
else:
raise Exception(f"unknown error code {e.code}")
sys_exit(1)
elif cmdname == 'daemon': elif cmdname == 'daemon':
@ -497,8 +507,17 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
print_msg("Found lingering lockfile for daemon. Removing.") print_msg("Found lingering lockfile for daemon. Removing.")
daemon.remove_lockfile(lockfile) daemon.remove_lockfile(lockfile)
sys_exit(1) sys_exit(1)
except JsonRPCError as e:
if e.code == JsonRPCError.Codes.USERFACING:
print_stderr(e.message)
elif e.code == JsonRPCError.Codes.INTERNAL:
print_stderr("(inside daemon): " + e.data["traceback"])
print_stderr(e.message)
else:
raise Exception(f"unknown error code {e.code}")
sys_exit(1)
except Exception as e: except Exception as e:
print_stderr(str(e) or repr(e)) _logger.exception("error running command (with daemon)")
sys_exit(1) sys_exit(1)
else: else:
if cmd.requires_network: if cmd.requires_network:
@ -520,14 +539,15 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict):
finally: finally:
plugins.stop() plugins.stop()
plugins.stopped_event.wait(1) plugins.stopped_event.wait(1)
except UserFacingException as e:
print_stderr(str(e))
sys_exit(1)
except Exception as e: except Exception as e:
print_stderr(str(e) or repr(e)) _logger.exception("error running command (without daemon)")
sys_exit(1) sys_exit(1)
# print result
if isinstance(result, str): if isinstance(result, str):
print_msg(result) print_msg(result)
elif type(result) is dict and result.get('error'):
print_stderr(result.get('error'))
sys_exit(1)
elif result is not None: elif result is not None:
print_msg(json_encode(result)) print_msg(json_encode(result))
sys_exit(0) sys_exit(0)

Loading…
Cancel
Save