electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit cd5152a02d0cc3e544d2c0143baf4d3b7405c226
parent 1ef804c652991337dedfa3d8749eff2820549c52
Author: ThomasV <thomasv@electrum.org>
Date:   Fri, 12 Oct 2018 10:48:09 +0200

Merge pull request #4765 from SomberNight/cli_restore_cmd

cli/rpc: 'restore' and 'create' commands are now available via RPC
Diffstat:
Melectrum/commands.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Melectrum/daemon.py | 3++-
Melectrum/keystore.py | 17++++++++++-------
Melectrum/mnemonic.py | 6++++--
Melectrum/tests/test_commands.py | 10+++++++++-
Mrun_electrum | 94+++++--------------------------------------------------------------------------
6 files changed, 111 insertions(+), 108 deletions(-)

diff --git a/electrum/commands.py b/electrum/commands.py @@ -41,6 +41,10 @@ from .i18n import _ from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .synchronizer import Notifier +from .storage import WalletStorage +from . import keystore +from .wallet import Wallet, Imported_Wallet +from .mnemonic import Mnemonic known_commands = {} @@ -123,17 +127,73 @@ class Commands: return ' '.join(sorted(known_commands.keys())) @command('') - def create(self, segwit=False): + def create(self, passphrase=None, password=None, encrypt_file=True, segwit=False): """Create a new wallet""" - raise Exception('Not a JSON-RPC command') + storage = WalletStorage(self.config.get_wallet_path()) + if storage.file_exists(): + raise Exception("Remove the existing wallet first!") + + seed_type = 'segwit' if segwit else 'standard' + seed = Mnemonic('en').make_seed(seed_type) + k = keystore.from_seed(seed, passphrase) + storage.put('keystore', k.dump()) + storage.put('wallet_type', 'standard') + wallet = Wallet(storage) + wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) + wallet.synchronize() + msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet." + + wallet.storage.write() + return {'seed': seed, 'path': wallet.storage.path, 'msg': msg} - @command('wn') - def restore(self, text): + @command('') + def restore(self, text, passphrase=None, password=None, encrypt_file=True): """Restore a wallet from text. Text can be a seed phrase, a master public key, a master private key, a list of bitcoin addresses or bitcoin private keys. If you want to be prompted for your seed, type '?' or ':' (concealed) """ - raise Exception('Not a JSON-RPC command') + storage = WalletStorage(self.config.get_wallet_path()) + if storage.file_exists(): + raise Exception("Remove the existing wallet first!") + + text = text.strip() + if keystore.is_address_list(text): + wallet = Imported_Wallet(storage) + for x in text.split(): + wallet.import_address(x) + elif keystore.is_private_key_list(text, allow_spaces_inside_key=False): + k = keystore.Imported_KeyStore({}) + storage.put('keystore', k.dump()) + wallet = Imported_Wallet(storage) + for x in text.split(): + wallet.import_private_key(x, password) + else: + if keystore.is_seed(text): + k = keystore.from_seed(text, passphrase) + elif keystore.is_master_key(text): + k = keystore.from_master_key(text) + else: + raise Exception("Seed or key not recognized") + storage.put('keystore', k.dump()) + storage.put('wallet_type', 'standard') + wallet = Wallet(storage) + + wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file) + wallet.synchronize() + + if self.network: + wallet.start_network(self.network) + print_error("Recovering wallet...") + wallet.wait_until_synchronized() + wallet.stop_threads() + # note: we don't wait for SPV + msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet" + else: + msg = ("This wallet was restored offline. It may contain more addresses than displayed. " + "Start a daemon (not offline) to sync history.") + + wallet.storage.write() + return {'path': wallet.storage.path, 'msg': msg} @command('wp') def password(self, password=None, new_password=None): @@ -419,7 +479,7 @@ class Commands: coins = self.wallet.get_spendable_coins(domain, self.config) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) - if locktime != None: + if locktime != None: tx.locktime = locktime if rbf is None: rbf = self.config.get('use_rbf', True) @@ -671,6 +731,16 @@ class Commands: # for the python console return sorted(known_commands.keys()) + +def eval_bool(x: str) -> bool: + if x == 'false': return False + if x == 'true': return True + try: + return bool(ast.literal_eval(x)) + except: + return bool(x) + + param_descriptions = { 'privkey': 'Private key. Type \'?\' to get a prompt.', 'destination': 'Bitcoin address, contact or alias', @@ -693,6 +763,7 @@ param_descriptions = { command_options = { 'password': ("-W", "Password"), 'new_password':(None, "New Password"), + 'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"), 'receiving': (None, "Show only receiving addresses"), 'change': (None, "Show only change addresses"), 'frozen': (None, "Show only frozen addresses"), @@ -708,6 +779,7 @@ command_options = { 'nbits': (None, "Number of bits of entropy"), 'segwit': (None, "Create segwit seed"), 'language': ("-L", "Default language for wordlist"), + 'passphrase': (None, "Seed extension"), 'privkey': (None, "Private key. Set to '?' to get a prompt."), 'unsigned': ("-u", "Do not sign transaction"), 'rbf': (None, "Replace-by-fee transaction"), @@ -746,6 +818,7 @@ arg_types = { 'locktime': int, 'fee_method': str, 'fee_level': json_loads, + 'encrypt_file': eval_bool, } config_variables = { @@ -858,12 +931,10 @@ def get_parser(): cmd = known_commands[cmdname] p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description) add_global_options(p) - if cmdname == 'restore': - p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") for optname, default in zip(cmd.options, cmd.defaults): a, help = command_options[optname] b = '--' + optname - action = "store_true" if type(default) is bool else 'store' + action = "store_true" if default is False else 'store' args = (a, b) if a else (b,) if action == 'store': _type = arg_types.get(optname, str) diff --git a/electrum/daemon.py b/electrum/daemon.py @@ -170,7 +170,7 @@ class Daemon(DaemonThread): return True def run_daemon(self, config_options): - asyncio.set_event_loop(self.network.asyncio_loop) + asyncio.set_event_loop(self.network.asyncio_loop) # FIXME what if self.network is None? config = SimpleConfig(config_options) sub = config.get('subcommand') assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] @@ -264,6 +264,7 @@ class Daemon(DaemonThread): wallet.stop_threads() def run_cmdline(self, config_options): + asyncio.set_event_loop(self.network.asyncio_loop) # FIXME what if self.network is None? password = config_options.get('password') new_password = config_options.get('new_password') config = SimpleConfig(config_options) diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -711,16 +711,19 @@ def is_address_list(text): return bool(parts) and all(bitcoin.is_address(x) for x in parts) -def get_private_keys(text): - parts = text.split('\n') - parts = map(lambda x: ''.join(x.split()), parts) - parts = list(filter(bool, parts)) +def get_private_keys(text, *, allow_spaces_inside_key=True): + if allow_spaces_inside_key: # see #1612 + parts = text.split('\n') + parts = map(lambda x: ''.join(x.split()), parts) + parts = list(filter(bool, parts)) + else: + parts = text.split() if bool(parts) and all(bitcoin.is_private_key(x) for x in parts): return parts -def is_private_key_list(text): - return bool(get_private_keys(text)) +def is_private_key_list(text, *, allow_spaces_inside_key=True): + return bool(get_private_keys(text, allow_spaces_inside_key=allow_spaces_inside_key)) is_mpk = lambda x: is_old_mpk(x) or is_xpub(x) @@ -746,7 +749,7 @@ def purpose48_derivation(account_id: int, xtype: str) -> str: return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) -def from_seed(seed, passphrase, is_p2sh): +def from_seed(seed, passphrase, is_p2sh=False): t = seed_type(seed) if t == 'old': keystore = Old_KeyStore({}) diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py @@ -112,9 +112,10 @@ filenames = { } - +# FIXME every time we instantiate this class, we read the wordlist from disk +# and store a new copy of it in memory class Mnemonic(object): - # Seed derivation no longer follows BIP39 + # Seed derivation does not follow BIP39 # Mnemonic phrase uses a hash based checksum, instead of a wordlist-dependent checksum def __init__(self, lang=None): @@ -128,6 +129,7 @@ class Mnemonic(object): def mnemonic_to_seed(self, mnemonic, passphrase): PBKDF2_ROUNDS = 2048 mnemonic = normalize_text(mnemonic) + passphrase = passphrase or '' passphrase = normalize_text(passphrase) return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'electrum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS) diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py @@ -1,7 +1,7 @@ import unittest from decimal import Decimal -from electrum.commands import Commands +from electrum.commands import Commands, eval_bool class TestCommands(unittest.TestCase): @@ -31,3 +31,11 @@ class TestCommands(unittest.TestCase): self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd')) self.assertEqual("['file:///var/www/','https://electrum.org']", Commands._setconfig_normalize_value('rpcpassword', "['file:///var/www/','https://electrum.org']")) + + def test_eval_bool(self): + self.assertFalse(eval_bool("False")) + self.assertFalse(eval_bool("false")) + self.assertFalse(eval_bool("0")) + self.assertTrue(eval_bool("True")) + self.assertTrue(eval_bool("true")) + self.assertTrue(eval_bool("1")) diff --git a/run_electrum b/run_electrum @@ -65,18 +65,16 @@ if not is_android: check_imports() -from electrum import bitcoin, util +from electrum import util from electrum import constants -from electrum import SimpleConfig, Network -from electrum.wallet import Wallet, Imported_Wallet -from electrum import bitcoin, util, constants +from electrum import SimpleConfig +from electrum.wallet import Wallet from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled from electrum.util import set_verbosity, InvalidPassword from electrum.commands import get_parser, known_commands, Commands, config_variables from electrum import daemon from electrum import keystore -from electrum.mnemonic import Mnemonic # get password routine def prompt_password(prompt, confirm=True): @@ -91,80 +89,6 @@ def prompt_password(prompt, confirm=True): return password - -def run_non_RPC(config): - cmdname = config.get('cmd') - - storage = WalletStorage(config.get_wallet_path()) - if storage.file_exists(): - sys.exit("Error: Remove the existing wallet first!") - - def password_dialog(): - return prompt_password("Password (hit return if you do not wish to encrypt your wallet):") - - if cmdname == 'restore': - text = config.get('text').strip() - passphrase = config.get('passphrase', '') - password = password_dialog() if keystore.is_private(text) else None - if keystore.is_address_list(text): - wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_address(x) - elif keystore.is_private_key_list(text): - k = keystore.Imported_KeyStore({}) - storage.put('keystore', k.dump()) - storage.put('use_encryption', bool(password)) - wallet = Imported_Wallet(storage) - for x in text.split(): - wallet.import_private_key(x, password) - storage.write() - else: - if keystore.is_seed(text): - k = keystore.from_seed(text, passphrase, False) - elif keystore.is_master_key(text): - k = keystore.from_master_key(text) - else: - sys.exit("Error: Seed or key not recognized") - if password: - k.update_password(None, password) - storage.put('keystore', k.dump()) - storage.put('wallet_type', 'standard') - storage.put('use_encryption', bool(password)) - storage.write() - wallet = Wallet(storage) - if not config.get('offline'): - network = Network(config) - network.start() - wallet.start_network(network) - print_msg("Recovering wallet...") - wallet.synchronize() - wallet.wait_until_synchronized() - wallet.stop_threads() - # note: we don't wait for SPV - msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet" - else: - msg = "This wallet was restored offline. It may contain more addresses than displayed." - print_msg(msg) - - elif cmdname == 'create': - password = password_dialog() - passphrase = config.get('passphrase', '') - seed_type = 'segwit' if config.get('segwit') else 'standard' - seed = Mnemonic('en').make_seed(seed_type) - k = keystore.from_seed(seed, passphrase, False) - storage.put('keystore', k.dump()) - storage.put('wallet_type', 'standard') - wallet = Wallet(storage) - wallet.update_password(None, password, True) - wallet.synchronize() - print_msg("Your wallet generation seed is:\n\"%s\"" % seed) - print_msg("Please keep it in a safe place; if you lose it, you will not be able to restore your wallet.") - - wallet.storage.write() - print_msg("Wallet saved in '%s'" % wallet.storage.path) - sys.exit(0) - - def init_daemon(config_options): config = SimpleConfig(config_options) storage = WalletStorage(config.get_wallet_path()) @@ -233,14 +157,12 @@ def init_cmdline(config_options, server): else: password = None - config_options['password'] = password + config_options['password'] = config_options.get('password') or password if cmd.name == 'password': new_password = prompt_password('New password:') config_options['new_password'] = new_password - return cmd, password - def get_connected_hw_devices(plugins): support = plugins.get_hardware_support() @@ -297,7 +219,7 @@ def run_offline_command(config, config_options, plugins): # check password if cmd.requires_password and wallet.has_password(): try: - seed = wallet.check_password(password) + wallet.check_password(password) except InvalidPassword: print_msg("Error: This password does not decode this wallet.") sys.exit(1) @@ -320,6 +242,7 @@ def run_offline_command(config, config_options, plugins): wallet.storage.write() return result + def init_plugins(config, gui_name): from electrum.plugin import Plugins return Plugins(config, is_local or is_android, gui_name) @@ -406,11 +329,6 @@ if __name__ == '__main__': elif config.get('simnet'): constants.set_simnet() - # run non-RPC commands separately - if cmdname in ['create', 'restore']: - run_non_RPC(config) - sys.exit(0) - if cmdname == 'gui': fd, server = daemon.get_fd_or_server(config) if fd is not None: