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:
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: