diff --git a/lbry/cli.py b/lbry/cli.py index c55ea3098..c1ea76ca0 100644 --- a/lbry/cli.py +++ b/lbry/cli.py @@ -262,7 +262,11 @@ def main(argv=None): if args.help: print(doc) else: - parsed = docopt(doc, command_args) + parsed = docopt( + # TODO: ugly hack because docopt doesn't support commands with spaces in them + doc.replace(api_method_name.replace('_', ' '), api_method_name, 1), + command_args + ) params = set_kwargs(parsed) asyncio.get_event_loop().run_until_complete(execute_command(conf, api_method_name, params)) elif args.group is not None: diff --git a/lbry/constants.py b/lbry/constants.py index c1497ac19..513feabb0 100644 --- a/lbry/constants.py +++ b/lbry/constants.py @@ -1,3 +1,5 @@ +DEFAULT_PAGE_SIZE = 20 + NULL_HASH32 = b'\x00'*32 CENT = 1000000 diff --git a/lbry/service/api.py b/lbry/service/api.py index 16e8eb820..59c5113b6 100644 --- a/lbry/service/api.py +++ b/lbry/service/api.py @@ -19,40 +19,12 @@ from lbry.blockchain import Transaction, Output, dewies_to_lbc, dict_values_to_l from lbry.stream.managed_stream import ManagedStream from lbry.event import EventController, EventStream from lbry.crypto.hash import hex_str_to_hash +from lbry.constants import DEFAULT_PAGE_SIZE from .base import Service from .json_encoder import Paginated -DEFAULT_PAGE_SIZE = 20 - - -async def paginate_rows(get_records: Callable, page: Optional[int], page_size: Optional[int], **constraints): - page = max(1, page or 1) - page_size = max(1, page_size or DEFAULT_PAGE_SIZE) - constraints.update({ - "offset": page_size * (page - 1), - "limit": page_size - }) - return Paginated(await get_records(**constraints), page, page_size) - - -def paginate_list(items: List, page: Optional[int], page_size: Optional[int]): - page = max(1, page or 1) - page_size = max(1, page_size or DEFAULT_PAGE_SIZE) - total_items = len(items) - offset = page_size * (page - 1) - subitems = [] - if offset <= total_items: - subitems = items[offset:offset+page_size] - return { - "items": subitems, - "total_pages": int((total_items + (page_size - 1)) / page_size), - "total_items": total_items, - "page": page, "page_size": page_size - } - - StrOrList = Union[str, list] Address = Dict @@ -723,8 +695,8 @@ class API: """ if wallet_id: - return paginate_list([self.wallets.get_wallet_or_error(wallet_id)], 1, 1) - return paginate_list(self.wallets.wallets, **pagination_kwargs) + return Paginated.from_list([self.wallets[wallet_id]], 1, 1) + return Paginated.from_list(list(self.wallets.wallets.values()), **pagination_kwargs) async def wallet_reconnect(self): """ Reconnects ledger network client, applying new configurations. """ @@ -733,6 +705,7 @@ class API: async def wallet_create( self, wallet_id: str, # wallet file name + name: str = "", # skip_on_startup=False, # don't add wallet to daemon_settings.yml create_account=False, # generates the default account single_key=False # used with --create_account, creates single-key account @@ -741,12 +714,12 @@ class API: Create a new wallet. Usage: - wallet create ( | --wallet_id=) [--skip_on_startup] - [--create_account] [--single_key] + wallet create ( | --wallet_id=) [--name=] + [--skip_on_startup] [--create_account] [--single_key] """ wallet = await self.wallets.create( - wallet_id, create_account=create_account, single_key=single_key + wallet_id, name=name, create_account=create_account, single_key=single_key ) if not skip_on_startup: with self.service.conf.update_config() as c: @@ -764,17 +737,7 @@ class API: wallet add ( | --wallet_id=) """ - wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id) - for wallet in self.wallets.wallets: - if wallet.id == wallet_id: - raise Exception(f"Wallet at path '{wallet_path}' is already loaded.") - if not os.path.exists(wallet_path): - raise Exception(f"Wallet at path '{wallet_path}' was not found.") - wallet = self.wallets.import_wallet(wallet_path) - if self.ledger.sync.network.is_connected: - for account in wallet.accounts: - await self.ledger.subscribe_account(account) - return wallet + return await self.wallets.load(wallet_id) async def wallet_remove( self, @@ -787,11 +750,7 @@ class API: wallet remove ( | --wallet_id=) """ - wallet = self.wallets.get_wallet_or_error(wallet_id) - self.wallets.wallets.remove(wallet) - for account in wallet.accounts: - await self.ledger.unsubscribe_account(account) - return wallet + return self.wallets.remove(wallet_id) async def wallet_balance( self, @@ -945,9 +904,8 @@ class API: kwargs = {'confirmations': confirmations, 'show_seed': include_seed} wallet = self.wallets.get_or_default(wallet_id) if account_id: - return paginate_list([await wallet.get_account_or_error(account_id).get_details(**kwargs)], 1, 1) - else: - return paginate_list(await wallet.get_detailed_accounts(**kwargs), **pagination_kwargs) + return Paginated.from_list([wallet.accounts[account_id]], 1, 1) + return Paginated.from_list(list(wallet.accounts), **pagination_kwargs) async def account_balance( self, @@ -1018,14 +976,9 @@ class API: """ wallet = self.wallets.get_or_default(wallet_id) - account = await wallet.accounts.generate(account_name, language, { + return await wallet.accounts.generate(account_name, language, { 'name': SingleKey.name if single_key else HierarchicalDeterministic.name }) - await wallet.save() - # TODO: fix - #if self.ledger.sync.network.is_connected: - # await self.ledger.sync.subscribe_account(account) - return account async def account_remove( self, @@ -1064,7 +1017,7 @@ class API: """ wallet = self.wallets.get_or_default(wallet_id) - account = wallet.get_account_or_error(account_id) + account = wallet.accounts[account_id] change_made = False if account.receiving.name == HierarchicalDeterministic.name: @@ -1083,14 +1036,13 @@ class API: account.name = new_name change_made = True - if default and wallet.default_account != account: - wallet.accounts.remove(account) - wallet.accounts.insert(0, account) + if default and wallet.accounts.default != account: + wallet.accounts.set_default(account) change_made = True if change_made: account.modified_on = time.time() - await wallet.save() + await wallet.notify_change('account.updated') return account diff --git a/lbry/service/json_encoder.py b/lbry/service/json_encoder.py index cbef04fb9..ebccd2f49 100644 --- a/lbry/service/json_encoder.py +++ b/lbry/service/json_encoder.py @@ -4,7 +4,7 @@ from decimal import Decimal from binascii import hexlify, unhexlify from datetime import datetime, date from json import JSONEncoder -from typing import Iterator, Generic +from typing import Iterator, Generic, Callable, Awaitable from google.protobuf.message import DecodeError @@ -15,6 +15,7 @@ from lbry.crypto.bip32 import PubKey from lbry.blockchain.dewies import dewies_to_lbc from lbry.stream.managed_stream import ManagedStream from lbry.db.database import Result, ResultType +from lbry.constants import DEFAULT_PAGE_SIZE log = logging.getLogger(__name__) @@ -135,6 +136,30 @@ class Paginated(Generic[ResultType]): self.page = page self.page_size = page_size + @classmethod + def from_list(cls, items: list, page: int = None, page_size: int = None): + page = max(1, page or 1) + page_size = max(1, page_size or DEFAULT_PAGE_SIZE) + total_items = len(items) + offset = page_size * (page - 1) + subitems = [] + if offset <= total_items: + subitems = items[offset:offset + page_size] + return cls(Result(subitems, total_items), page, page_size) + + @classmethod + async def from_getter( + cls, get_records: Callable[..., Awaitable[Result]], + page: int = None, page_size: int = None, **constraints + ): + page = max(1, page or 1) + page_size = max(1, page_size or DEFAULT_PAGE_SIZE) + constraints.update({ + "offset": page_size * (page - 1), + "limit": page_size + }) + return cls(await get_records(**constraints), page, page_size) + def __getitem__(self, item: int) -> ResultType: return self.result[item] diff --git a/lbry/testcase.py b/lbry/testcase.py index 8ec498ad8..07d76b54e 100644 --- a/lbry/testcase.py +++ b/lbry/testcase.py @@ -582,12 +582,12 @@ class CommandTestCase(IntegrationTestCase): self.wallet = self.service.wallets.default self.account = self.wallet.accounts.default - addresses = await self.account.ensure_address_gap() + address = await self.account.receiving.get_or_create_usable_address() self.ledger.conf.upload_dir = os.path.join(self.ledger.conf.data_dir, 'uploads') os.mkdir(self.ledger.conf.upload_dir) - await self.chain.send_to_address(addresses[0], '10.0') + await self.chain.send_to_address(address, '10.0') await self.generate(5) async def asyncTearDown(self): @@ -642,6 +642,33 @@ class CommandTestCase(IntegrationTestCase): await self.service.wait(tx) return self.sout(tx) + async def wallet_list(self, *args, **kwargs): + return (await self.out(self.api.wallet_list(*args, **kwargs)))['items'] + + async def wallet_create(self, *args, **kwargs): + return await self.out(self.api.wallet_create(*args, **kwargs)) + + async def wallet_add(self, *args, **kwargs): + return await self.out(self.api.wallet_add(*args, **kwargs)) + + async def wallet_remove(self, *args, **kwargs): + return await self.out(self.api.wallet_remove(*args, **kwargs)) + + async def account_list(self, *args, **kwargs): + return (await self.out(self.api.account_list(*args, **kwargs)))['items'] + + async def account_create(self, *args, **kwargs): + return await self.out(self.api.account_create(*args, **kwargs)) + + async def account_add(self, *args, **kwargs): + return await self.out(self.api.account_add(*args, **kwargs)) + + async def account_set(self, *args, **kwargs): + return await self.out(self.api.account_set(*args, **kwargs)) + + async def account_remove(self, *args, **kwargs): + return await self.out(self.api.account_remove(*args, **kwargs)) + def create_upload_file(self, data, prefix=None, suffix=None): file_path = tempfile.mktemp( prefix=prefix or "tmp", suffix=suffix or "", dir=self.ledger.conf.upload_dir diff --git a/lbry/wallet/manager.py b/lbry/wallet/manager.py index a8554e494..0a8317f1a 100644 --- a/lbry/wallet/manager.py +++ b/lbry/wallet/manager.py @@ -101,6 +101,9 @@ class WalletManager: wallet.on_change.listen(lambda _: self.storage.save(wallet)) return wallet + def remove(self, wallet_id: str) -> Wallet: + return self.wallets.pop(wallet_id) + async def _report_state(self): try: for account in self.accounts: diff --git a/lbry/wallet/wallet.py b/lbry/wallet/wallet.py index 52a1b9760..0ba899b7b 100644 --- a/lbry/wallet/wallet.py +++ b/lbry/wallet/wallet.py @@ -438,6 +438,10 @@ class AccountListManager: await self.wallet.notify_change('account.removed') return account + def set_default(self, account): + self._accounts.remove(account) + self._accounts.insert(0, account) + def get_or_none(self, account_id: str) -> Optional[Account]: if account_id is not None: return self[account_id] diff --git a/tests/integration/blockchain/test_account_commands.py b/tests/integration/commands/test_account.py similarity index 100% rename from tests/integration/blockchain/test_account_commands.py rename to tests/integration/commands/test_account.py diff --git a/tests/integration/blockchain/test_wallet_commands.py b/tests/integration/commands/test_wallet.py similarity index 100% rename from tests/integration/blockchain/test_wallet_commands.py rename to tests/integration/commands/test_wallet.py