From 6a5d88a0d5f3be90d7d27d94e377e9a4a8419ed8 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Mon, 6 Aug 2018 00:28:11 -0400 Subject: [PATCH] new fund command and automatic account creation --- lbrynet/cli.py | 11 +-- lbrynet/daemon/Daemon.py | 181 ++++++++++++++++++++------------------ lbrynet/wallet/account.py | 6 ++ lbrynet/wallet/manager.py | 11 ++- tests/test_cli.py | 2 +- 5 files changed, 115 insertions(+), 96 deletions(-) diff --git a/lbrynet/cli.py b/lbrynet/cli.py index fa5805568..d167766ea 100644 --- a/lbrynet/cli.py +++ b/lbrynet/cli.py @@ -67,16 +67,9 @@ def normalize_value(x, key=None): return True if x.lower() == 'false': return False - if '.' in x: - try: - return float(x) - except ValueError: - # not a float - pass - try: + if x.isdigit(): return int(x) - except ValueError: - return x + return x def remove_brackets(key): diff --git a/lbrynet/daemon/Daemon.py b/lbrynet/daemon/Daemon.py index 5514d328a..776b4a472 100644 --- a/lbrynet/daemon/Daemon.py +++ b/lbrynet/daemon/Daemon.py @@ -12,6 +12,7 @@ from twisted.web import server from twisted.internet import defer, reactor from twisted.internet.task import LoopingCall from twisted.python.failure import Failure +from typing import Union from torba.constants import COIN @@ -42,7 +43,7 @@ from lbrynet.dht.error import TimeoutError from lbrynet.core.Peer import Peer from lbrynet.core.SinglePeerDownloader import SinglePeerDownloader from lbrynet.core.client.StandaloneBlobDownloader import StandaloneBlobDownloader -from lbrynet.wallet.account import Account as LBRYAccount +from lbrynet.wallet.account import Account as LBCAccount log = logging.getLogger(__name__) requires = AuthJSONRPCServer.requires @@ -805,7 +806,6 @@ class Daemon(AuthJSONRPCServer): log.info("Get version info: " + json.dumps(platform_info)) return self._render_response(platform_info) - # @AuthJSONRPCServer.deprecated() # deprecated actually disables the call def jsonrpc_report_bug(self, message=None): """ Report a bug to slack @@ -2369,36 +2369,6 @@ class Daemon(AuthJSONRPCServer): d.addCallback(lambda address: self._render_response(address)) return d - @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - @AuthJSONRPCServer.deprecated("wallet_send") - @defer.inlineCallbacks - def jsonrpc_send_amount_to_address(self, amount, address): - """ - Queue a payment of credits to an address - - Usage: - send_amount_to_address ( | --amount=) (
| --address=
) - - Options: - --amount= : (float) amount to send - --address=
: (str) address to send credits to - - Returns: - (bool) true if payment successfully scheduled - """ - - if amount < 0: - raise NegativeFundsError() - elif not amount: - raise NullFundsError() - - reserved_points = self.wallet.reserve_points(address, amount) - if reserved_points is None: - raise InsufficientFundsError() - yield self.wallet.send_points_to_address(reserved_points, amount) - self.analytics_manager.send_credits_sent() - defer.returnValue(True) - @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) @defer.inlineCallbacks def jsonrpc_wallet_send(self, amount, address=None, claim_id=None): @@ -2978,27 +2948,6 @@ class Daemon(AuthJSONRPCServer): return self._blob_availability(blob_hash, search_timeout, blob_timeout) - @requires(UPNP_COMPONENT, WALLET_COMPONENT, DHT_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - @AuthJSONRPCServer.deprecated("stream_availability") - def jsonrpc_get_availability(self, uri, sd_timeout=None, peer_timeout=None): - """ - Get stream availability for lbry uri - - Usage: - get_availability ( | --uri=) [ | --sd_timeout=] - [ | --peer_timeout=] - - Options: - --uri= : (str) check availability for this uri - --sd_timeout= : (int) sd blob download timeout - --peer_timeout= : (int) how long to look for peers - - Returns: - (float) Peers per blob / total blobs - """ - - return self.jsonrpc_stream_availability(uri, peer_timeout, sd_timeout) - @requires(UPNP_COMPONENT, WALLET_COMPONENT, DHT_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) @defer.inlineCallbacks def jsonrpc_stream_availability(self, uri, search_timeout=None, blob_timeout=None): @@ -3102,39 +3051,22 @@ class Daemon(AuthJSONRPCServer): response['head_blob_availability'].get('is_available') defer.returnValue(response) - @defer.inlineCallbacks - def jsonrpc_cli_test_command(self, pos_arg, pos_args=[], pos_arg2=None, pos_arg3=None, - a_arg=False, b_arg=False): - """ - This command is only for testing the CLI argument parsing - Usage: - cli_test_command [--a_arg] [--b_arg] ( | --pos_arg=) - [...] [--pos_arg2=] - [--pos_arg3=] - - Options: - --a_arg : (bool) a arg - --b_arg : (bool) b arg - --pos_arg= : (int) pos arg - --pos_args= : (int) pos args - --pos_arg2= : (int) pos arg 2 - --pos_arg3= : (int) pos arg 3 - Returns: - pos args - """ - out = (pos_arg, pos_args, pos_arg2, pos_arg3, a_arg, b_arg) - response = yield self._render_response(out) - defer.returnValue(response) + ####################### + # New Wallet Commands # + ####################### + # TODO: + # Delete this after all commands have been migrated + # and refactored. @requires("wallet") - def jsonrpc_account_balance(self, account_name=None, confirmations=6, - include_reserved=False, include_claims=False): + def jsonrpc_balance(self, account_name=None, confirmations=6, include_reserved=False, + include_claims=False): """ Return the balance of an individual account or all of the accounts. Usage: - account_balance [] [--confirmations=] - [--include_reserved] [--include_claims] + balance [] [--confirmations=] + [--include_reserved] [--include_claims] Options: --account= : (str) If provided only the balance for this @@ -3150,7 +3082,7 @@ class Daemon(AuthJSONRPCServer): if account_name: for account in self.wallet.accounts: if account.name == account_name: - if include_claims and not isinstance(account, LBRYAccount): + if include_claims and not isinstance(account, LBCAccount): raise Exception( "'--include-claims' requires specifying an LBC ledger account. " "Found '{}', but it's an {} ledger account." @@ -3170,7 +3102,7 @@ class Daemon(AuthJSONRPCServer): return self.wallet.get_balances(confirmations) @requires("wallet") - def jsonrpc_account_max_gap(self, account_name): + def jsonrpc_max_address_gap(self, account_name): """ Finds ranges of consecutive addresses that are unused and returns the length of the longest such range: for change and receiving address chains. This is @@ -3178,7 +3110,7 @@ class Daemon(AuthJSONRPCServer): account settings. Usage: - account_max_gap + max_address_gap Options: --account= : (str) account for which to get max gaps @@ -3186,10 +3118,89 @@ class Daemon(AuthJSONRPCServer): Returns: (map) maximum gap for change and receiving addresses """ + return self.get_account_or_error('account', account_name).get_max_gap() + + @requires("wallet") + def jsonrpc_fund(self, to_account, from_account, amount=0, + everything=False, outputs=1, broadcast=False): + """ + Transfer some amount (or --everything) to an account from another + account (can be the same account). Decimal amounts are interpreted + as LBC and non-decimal amounts are interpreted as dewies. You can + also spread the transfer across a number of --outputs (cannot be + used together with --everything). + + Usage: + transfer ( | --to_account=) + ( | --from_account=) + ( | --amount= | --everything) + [ | --outputs=] + [--broadcast] + + Options: + --to_account= : (str) send to this account + --from_account= : (str) spend from this account + --amount= : (str) the amount to transfer (lbc or dewies) + --everything : (bool) transfer everything (excluding claims), default: false. + --outputs= : (int) split payment across many outputs, default: 1. + --broadcast : (bool) actually broadcast the transaction, default: false. + + Returns: + (map) maximum gap for change and receiving addresses + + """ + to_account = self.get_account_or_error('to_account', to_account) + from_account = self.get_account_or_error('from_account', from_account) + amount = self.get_dewies_or_error('amount', amount) if amount else None + if not isinstance(outputs, int): + raise ValueError("--outputs must be an integer.") + if everything and outputs > 1: + raise ValueError("Using --everything along with --outputs is not supported.") + return from_account.fund( + to_account=to_account, amount=amount, everything=everything, + outputs=outputs, broadcast=broadcast + ).addCallback(lambda tx: self.tx_to_json(tx, from_account.ledger)) + + @staticmethod + def tx_to_json(tx, ledger): + return { + 'txid': tx.id, + 'inputs': [ + {'amount': txi.amount, 'address': txi.txo_ref.txo.get_address(ledger)} + for txi in tx.inputs + ], + 'outputs': [ + {'amount': txo.amount, 'address': txo.get_address(ledger)} + for txo in tx.outputs + ], + 'total_input': tx.input_sum, + 'total_output': tx.input_sum, + 'total_fee': tx.fee, + 'xhex': hexlify(tx.raw).decode(), + } + + def get_account_or_error(self, argument: str, account_name: str, lbc_only=False): for account in self.wallet.accounts: if account.name == account_name: - return account.get_max_gap() - raise Exception("Couldn't find an account named: '{}'.".format(account_name)) + if lbc_only and not isinstance(account, LBCAccount): + raise ValueError( + "Found '{}', but it's an {} ledger account. " + "'{}' requires specifying an LBC ledger account." + .format(account_name, account.ledger.symbol, argument) + ) + return account + raise ValueError("Couldn't find an account named: '{}'.".format(account_name)) + + @staticmethod + def get_dewies_or_error(argument: str, amount: Union[str, int]): + if isinstance(amount, str): + if '.' in amount: + return int(Decimal(amount) * COIN) + elif amount.isdigit(): + return int(amount) + elif isinstance(amount, int): + return amount + raise ValueError("Invalid value for '{}' argument: {}".format(argument, amount)) def loggly_time_string(dt): diff --git a/lbrynet/wallet/account.py b/lbrynet/wallet/account.py index 1038339a7..a2661b92f 100644 --- a/lbrynet/wallet/account.py +++ b/lbrynet/wallet/account.py @@ -101,6 +101,12 @@ class Account(BaseAccount): }) defer.returnValue(channels) + @classmethod + def get_private_key_from_seed(cls, ledger: 'baseledger.BaseLedger', seed: str, password: str): + return super().get_private_key_from_seed( + ledger, seed, password or 'lbryum' + ) + @classmethod def from_dict(cls, ledger, d: dict) -> 'Account': account = super().from_dict(ledger, d) diff --git a/lbrynet/wallet/manager.py b/lbrynet/wallet/manager.py index 8381347f7..f1028d867 100644 --- a/lbrynet/wallet/manager.py +++ b/lbrynet/wallet/manager.py @@ -1,5 +1,6 @@ import os import json +import logging from twisted.internet import defer from torba.basemanager import BaseWalletManager @@ -13,6 +14,8 @@ from .account import generate_certificate from .transaction import Transaction from .database import WalletDatabase +log = logging.getLogger(__name__) + class BackwardsCompatibleNetwork: def __init__(self, manager): @@ -107,10 +110,16 @@ class LbryWalletManager(BaseWalletManager): with open(wallet_file_path, 'w') as f: f.write(json_data) - return cls.from_config({ + manager = cls.from_config({ 'ledgers': {ledger_id: ledger_config}, 'wallets': [wallet_file_path] }) + if manager.default_account is None: + ledger = manager.get_or_create_ledger('lbc_mainnet') + log.info('Wallet at %s is empty, generating a default account.', wallet_file_path) + manager.default_wallet.generate_account(ledger) + manager.default_wallet.save() + return manager def get_best_blockhash(self): return defer.succeed('') diff --git a/tests/test_cli.py b/tests/test_cli.py index 617ba3452..f4749f99a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,7 +11,7 @@ class CLITest(unittest.TestCase): def test_guess_type(self): self.assertEqual('0.3.8', normalize_value('0.3.8')) - self.assertEqual(0.3, normalize_value('0.3')) + self.assertEqual('0.3', normalize_value('0.3')) self.assertEqual(3, normalize_value('3')) self.assertEqual(3, normalize_value(3))