mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
cli/rpc: 'restore' and 'create' commands are now available via RPC
This commit is contained in:
parent
cd5453e477
commit
1233309ebd
6 changed files with 111 additions and 108 deletions
|
@ -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!")
|
||||
|
||||
@command('wn')
|
||||
def restore(self, text):
|
||||
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('')
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
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…
Add table
Reference in a new issue