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 .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)

3
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)

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)
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({})

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):
# 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):
@ -129,6 +130,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)

10
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"))

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

Loading…
Cancel
Save