Browse Source

cli/rpc: 'restore' and 'create' commands are now available via RPC

3.3.3.1
SomberNight 6 years ago
parent
commit
1233309ebd
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 89
      electrum/commands.py
  2. 3
      electrum/daemon.py
  3. 17
      electrum/keystore.py
  4. 6
      electrum/mnemonic.py
  5. 10
      electrum/tests/test_commands.py
  6. 94
      run_electrum

89
electrum/commands.py

@ -41,6 +41,10 @@ from .i18n import _
from .transaction import Transaction, multisig_script, TxOutput from .transaction import Transaction, multisig_script, TxOutput
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier from .synchronizer import Notifier
from .storage import WalletStorage
from . import keystore
from .wallet import Wallet, Imported_Wallet
from .mnemonic import Mnemonic
known_commands = {} known_commands = {}
@ -123,17 +127,73 @@ class Commands:
return ' '.join(sorted(known_commands.keys())) return ' '.join(sorted(known_commands.keys()))
@command('') @command('')
def create(self, segwit=False): def create(self, passphrase=None, password=None, encrypt_file=True, segwit=False):
"""Create a new wallet""" """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') @command('')
def restore(self, text): def restore(self, text, passphrase=None, password=None, encrypt_file=True):
"""Restore a wallet from text. Text can be a seed phrase, a master """Restore a wallet from text. Text can be a seed phrase, a master
public key, a master private key, a list of bitcoin addresses public key, a master private key, a list of bitcoin addresses
or bitcoin private keys. If you want to be prompted for your or bitcoin private keys. If you want to be prompted for your
seed, type '?' or ':' (concealed) """ 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') @command('wp')
def password(self, password=None, new_password=None): def password(self, password=None, new_password=None):
@ -419,7 +479,7 @@ class Commands:
coins = self.wallet.get_spendable_coins(domain, self.config) coins = self.wallet.get_spendable_coins(domain, self.config)
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
if locktime != None: if locktime != None:
tx.locktime = locktime tx.locktime = locktime
if rbf is None: if rbf is None:
rbf = self.config.get('use_rbf', True) rbf = self.config.get('use_rbf', True)
@ -671,6 +731,16 @@ class Commands:
# for the python console # for the python console
return sorted(known_commands.keys()) 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 = { param_descriptions = {
'privkey': 'Private key. Type \'?\' to get a prompt.', 'privkey': 'Private key. Type \'?\' to get a prompt.',
'destination': 'Bitcoin address, contact or alias', 'destination': 'Bitcoin address, contact or alias',
@ -693,6 +763,7 @@ param_descriptions = {
command_options = { command_options = {
'password': ("-W", "Password"), 'password': ("-W", "Password"),
'new_password':(None, "New 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"), 'receiving': (None, "Show only receiving addresses"),
'change': (None, "Show only change addresses"), 'change': (None, "Show only change addresses"),
'frozen': (None, "Show only frozen addresses"), 'frozen': (None, "Show only frozen addresses"),
@ -708,6 +779,7 @@ command_options = {
'nbits': (None, "Number of bits of entropy"), 'nbits': (None, "Number of bits of entropy"),
'segwit': (None, "Create segwit seed"), 'segwit': (None, "Create segwit seed"),
'language': ("-L", "Default language for wordlist"), 'language': ("-L", "Default language for wordlist"),
'passphrase': (None, "Seed extension"),
'privkey': (None, "Private key. Set to '?' to get a prompt."), 'privkey': (None, "Private key. Set to '?' to get a prompt."),
'unsigned': ("-u", "Do not sign transaction"), 'unsigned': ("-u", "Do not sign transaction"),
'rbf': (None, "Replace-by-fee transaction"), 'rbf': (None, "Replace-by-fee transaction"),
@ -746,6 +818,7 @@ arg_types = {
'locktime': int, 'locktime': int,
'fee_method': str, 'fee_method': str,
'fee_level': json_loads, 'fee_level': json_loads,
'encrypt_file': eval_bool,
} }
config_variables = { config_variables = {
@ -858,12 +931,10 @@ def get_parser():
cmd = known_commands[cmdname] cmd = known_commands[cmdname]
p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description) p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
add_global_options(p) 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): for optname, default in zip(cmd.options, cmd.defaults):
a, help = command_options[optname] a, help = command_options[optname]
b = '--' + 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,) args = (a, b) if a else (b,)
if action == 'store': if action == 'store':
_type = arg_types.get(optname, str) _type = arg_types.get(optname, str)

3
electrum/daemon.py

@ -170,7 +170,7 @@ class Daemon(DaemonThread):
return True return True
def run_daemon(self, config_options): 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) config = SimpleConfig(config_options)
sub = config.get('subcommand') sub = config.get('subcommand')
assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet'] assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet']
@ -264,6 +264,7 @@ class Daemon(DaemonThread):
wallet.stop_threads() wallet.stop_threads()
def run_cmdline(self, config_options): 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') password = config_options.get('password')
new_password = config_options.get('new_password') new_password = config_options.get('new_password')
config = SimpleConfig(config_options) config = SimpleConfig(config_options)

17
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) return bool(parts) and all(bitcoin.is_address(x) for x in parts)
def get_private_keys(text): def get_private_keys(text, *, allow_spaces_inside_key=True):
parts = text.split('\n') if allow_spaces_inside_key: # see #1612
parts = map(lambda x: ''.join(x.split()), parts) parts = text.split('\n')
parts = list(filter(bool, parts)) 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): if bool(parts) and all(bitcoin.is_private_key(x) for x in parts):
return parts return parts
def is_private_key_list(text): def is_private_key_list(text, *, allow_spaces_inside_key=True):
return bool(get_private_keys(text)) 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) 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) 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) t = seed_type(seed)
if t == 'old': if t == 'old':
keystore = Old_KeyStore({}) keystore = Old_KeyStore({})

6
electrum/mnemonic.py

@ -113,9 +113,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): 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 # Mnemonic phrase uses a hash based checksum, instead of a wordlist-dependent checksum
def __init__(self, lang=None): def __init__(self, lang=None):
@ -129,6 +130,7 @@ class Mnemonic(object):
def mnemonic_to_seed(self, mnemonic, passphrase): def mnemonic_to_seed(self, mnemonic, passphrase):
PBKDF2_ROUNDS = 2048 PBKDF2_ROUNDS = 2048
mnemonic = normalize_text(mnemonic) mnemonic = normalize_text(mnemonic)
passphrase = passphrase or ''
passphrase = normalize_text(passphrase) passphrase = normalize_text(passphrase)
return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'electrum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS) return hashlib.pbkdf2_hmac('sha512', mnemonic.encode('utf-8'), b'electrum' + passphrase.encode('utf-8'), iterations = PBKDF2_ROUNDS)

10
electrum/tests/test_commands.py

@ -1,7 +1,7 @@
import unittest import unittest
from decimal import Decimal from decimal import Decimal
from electrum.commands import Commands from electrum.commands import Commands, eval_bool
class TestCommands(unittest.TestCase): class TestCommands(unittest.TestCase):
@ -31,3 +31,11 @@ class TestCommands(unittest.TestCase):
self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd')) self.assertEqual("2asd", Commands._setconfig_normalize_value('rpcpassword', '2asd'))
self.assertEqual("['file:///var/www/','https://electrum.org']", self.assertEqual("['file:///var/www/','https://electrum.org']",
Commands._setconfig_normalize_value('rpcpassword', "['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"))

94
run_electrum

@ -65,18 +65,16 @@ if not is_android:
check_imports() check_imports()
from electrum import bitcoin, util from electrum import util
from electrum import constants from electrum import constants
from electrum import SimpleConfig, Network from electrum import SimpleConfig
from electrum.wallet import Wallet, Imported_Wallet from electrum.wallet import Wallet
from electrum import bitcoin, util, constants
from electrum.storage import WalletStorage, get_derivation_used_for_hw_device_encryption 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 print_msg, print_stderr, json_encode, json_decode, UserCancelled
from electrum.util import set_verbosity, InvalidPassword from electrum.util import set_verbosity, 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.mnemonic import Mnemonic
# get password routine # get password routine
def prompt_password(prompt, confirm=True): def prompt_password(prompt, confirm=True):
@ -91,80 +89,6 @@ def prompt_password(prompt, confirm=True):
return password 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): def init_daemon(config_options):
config = SimpleConfig(config_options) config = SimpleConfig(config_options)
storage = WalletStorage(config.get_wallet_path()) storage = WalletStorage(config.get_wallet_path())
@ -233,14 +157,12 @@ def init_cmdline(config_options, server):
else: else:
password = None password = None
config_options['password'] = password config_options['password'] = config_options.get('password') or password
if cmd.name == 'password': if cmd.name == 'password':
new_password = prompt_password('New password:') new_password = prompt_password('New password:')
config_options['new_password'] = new_password config_options['new_password'] = new_password
return cmd, password
def get_connected_hw_devices(plugins): def get_connected_hw_devices(plugins):
support = plugins.get_hardware_support() support = plugins.get_hardware_support()
@ -297,7 +219,7 @@ def run_offline_command(config, config_options, plugins):
# check password # check password
if cmd.requires_password and wallet.has_password(): if cmd.requires_password and wallet.has_password():
try: try:
seed = wallet.check_password(password) wallet.check_password(password)
except InvalidPassword: except InvalidPassword:
print_msg("Error: This password does not decode this wallet.") print_msg("Error: This password does not decode this wallet.")
sys.exit(1) sys.exit(1)
@ -320,6 +242,7 @@ def run_offline_command(config, config_options, plugins):
wallet.storage.write() wallet.storage.write()
return result return result
def init_plugins(config, gui_name): def init_plugins(config, gui_name):
from electrum.plugin import Plugins from electrum.plugin import Plugins
return Plugins(config, is_local or is_android, gui_name) return Plugins(config, is_local or is_android, gui_name)
@ -406,11 +329,6 @@ if __name__ == '__main__':
elif config.get('simnet'): elif config.get('simnet'):
constants.set_simnet() constants.set_simnet()
# run non-RPC commands separately
if cmdname in ['create', 'restore']:
run_non_RPC(config)
sys.exit(0)
if cmdname == 'gui': if cmdname == 'gui':
fd, server = daemon.get_fd_or_server(config) fd, server = daemon.get_fd_or_server(config)
if fd is not None: if fd is not None:

Loading…
Cancel
Save