URI: 
       tMerge pull request #4765 from SomberNight/cli_restore_cmd - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit cd5152a02d0cc3e544d2c0143baf4d3b7405c226
   DIR parent 1ef804c652991337dedfa3d8749eff2820549c52
  HTML 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:
         M electrum/commands.py                |      89 +++++++++++++++++++++++++++----
         M electrum/daemon.py                  |       3 ++-
         M electrum/keystore.py                |      17 ++++++++++-------
         M electrum/mnemonic.py                |       6 ++++--
         M electrum/tests/test_commands.py     |      10 +++++++++-
         M run_electrum                        |      94 ++-----------------------------
       
       6 files changed, 111 insertions(+), 108 deletions(-)
       ---
   DIR diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -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 = {}
        
       t@@ -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):
       t@@ -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)
       t@@ -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',
       t@@ -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"),
       t@@ -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"),
       t@@ -746,6 +818,7 @@ arg_types = {
            'locktime': int,
            'fee_method': str,
            'fee_level': json_loads,
       +    'encrypt_file': eval_bool,
        }
        
        config_variables = {
       t@@ -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)
   DIR diff --git a/electrum/daemon.py b/electrum/daemon.py
       t@@ -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']
       t@@ -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)
   DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
       t@@ -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)
       t@@ -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({})
   DIR diff --git a/electrum/mnemonic.py b/electrum/mnemonic.py
       t@@ -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):
       t@@ -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)
        
   DIR diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py
       t@@ -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):
       t@@ -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"))
   DIR diff --git a/run_electrum b/run_electrum
       t@@ -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):
       t@@ -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())
       t@@ -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()
       t@@ -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)
       t@@ -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)
       t@@ -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: