From 2ddf1a08f669d98d4d77e3d78862174a883b4423 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 17 Oct 2018 19:07:17 -0400 Subject: [PATCH] publish command supports custom account list to lookup channels --- lbrynet/daemon/Daemon.py | 62 ++++++++++++++------- lbrynet/daemon/Publisher.py | 8 ++- lbrynet/wallet/manager.py | 6 +- tests/integration/wallet/test_commands.py | 68 +++++++++++++++++++++++ tox.ini | 1 + 5 files changed, 117 insertions(+), 28 deletions(-) diff --git a/lbrynet/daemon/Daemon.py b/lbrynet/daemon/Daemon.py index 973b22498..f2e633e30 100644 --- a/lbrynet/daemon/Daemon.py +++ b/lbrynet/daemon/Daemon.py @@ -6,7 +6,7 @@ import urllib import json import textwrap -from typing import Callable, Optional +from typing import Callable, Optional, List from operator import itemgetter from binascii import hexlify, unhexlify from copy import deepcopy @@ -400,10 +400,10 @@ class Daemon(AuthJSONRPCServer): del self.streams[sd_hash] defer.returnValue(result) - async def _publish_stream(self, name, bid, claim_dict, file_path=None, certificate=None, + async def _publish_stream(self, account, name, bid, claim_dict, file_path=None, certificate=None, claim_address=None, change_address=None): publisher = Publisher( - self.blob_manager, self.payment_rate_manager, self.storage, + account, self.blob_manager, self.payment_rate_manager, self.storage, self.file_manager, self.wallet_manager, certificate ) parse_lbry_uri(name) @@ -1975,17 +1975,19 @@ class Daemon(AuthJSONRPCServer): return self.get_est_cost(uri, size) @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - async def jsonrpc_channel_new(self, channel_name, amount): + async def jsonrpc_channel_new(self, channel_name, amount, account_id=None): """ Generate a publisher key and create a new '@' prefixed certificate claim Usage: channel_new ( | --channel_name=) ( | --amount=) + [--account_id=] Options: --channel_name= : (str) name of the channel prefixed with '@' --amount= : (decimal) bid amount on the channel + --account_id= : (str) id of the account to store channel Returns: (dict) Dictionary containing result of the claim @@ -2007,10 +2009,12 @@ class Daemon(AuthJSONRPCServer): raise Exception("Invalid channel name") amount = self.get_dewies_or_error("amount", amount) - if amount <= 0: raise Exception("Invalid amount") - tx = await self.wallet_manager.claim_new_channel(channel_name, amount) + + tx = await self.wallet_manager.claim_new_channel( + channel_name, amount, self.get_account_or_default(account_id) + ) self.default_wallet.save() self.analytics_manager.send_new_channel() nout = 0 @@ -2030,7 +2034,7 @@ class Daemon(AuthJSONRPCServer): Get certificate claim infos for channels that can be published to Usage: - channel_list [ | --account_id= ] + channel_list [ | --account_id=] [--page=] [--page_size=] Options: @@ -2089,11 +2093,12 @@ class Daemon(AuthJSONRPCServer): @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, PAYMENT_RATE_COMPONENT, DATABASE_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - async def jsonrpc_publish(self, name, bid, metadata=None, file_path=None, fee=None, title=None, - description=None, author=None, language=None, license=None, - license_url=None, thumbnail=None, preview=None, nsfw=None, sources=None, - channel_name=None, channel_id=None, - claim_address=None, change_address=None): + async def jsonrpc_publish( + self, name, bid, metadata=None, file_path=None, fee=None, title=None, + description=None, author=None, language=None, license=None, + license_url=None, thumbnail=None, preview=None, nsfw=None, sources=None, + channel_name=None, channel_id=None, channel_account_id=None, account_id=None, + claim_address=None, change_address=None): """ Make a new name claim and publish associated data to lbrynet, update over existing claim if user already has a claim for name. @@ -2117,6 +2122,7 @@ class Daemon(AuthJSONRPCServer): [--license=] [--license_url=] [--thumbnail=] [--preview=] [--nsfw=] [--sources=] [--channel_name=] [--channel_id=] + [--channel_account_id=...] [--account_id=] [--claim_address=] [--change_address=] Options: @@ -2156,8 +2162,11 @@ class Daemon(AuthJSONRPCServer): for channel claim being in the wallet. This allows publishing to a channel where only the certificate private key is in the wallet. + --channel_account_id=: (str) one or more account ids for accounts to look in + for channel certificates, defaults to all accounts. + --account_id= : (str) account to use for funding the transaction --claim_address= : (str) address where the claim is sent to, if not specified - new address wil automatically be created + new address will automatically be created Returns: (dict) Dictionary containing result of the claim @@ -2184,7 +2193,9 @@ class Daemon(AuthJSONRPCServer): # raises an error if the address is invalid decode_address(address) - available = await self.default_account.get_balance() + account = self.get_account_or_default(account_id) + + available = await account.get_balance() if amount >= available: # TODO: add check for existing claim balance #balance = yield self.wallet.get_max_usable_balance_for_claim(name) @@ -2236,7 +2247,7 @@ class Daemon(AuthJSONRPCServer): log.warning("Stripping empty fee from published metadata") del metadata['fee'] elif 'address' not in metadata['fee']: - address = await self.default_account.receiving.get_or_create_usable_address() + address = await account.receiving.get_or_create_usable_address() metadata['fee']['address'] = address if 'fee' in metadata and 'version' not in metadata['fee']: metadata['fee']['version'] = '_0_0_1' @@ -2279,7 +2290,9 @@ class Daemon(AuthJSONRPCServer): certificate = None if channel_id or channel_name: - certificate = await self.get_channel_or_error(channel_id, channel_name) + certificate = await self.get_channel_or_error( + self.get_accounts_or_all(channel_account_id), channel_id, channel_name + ) log.info("Publish: %s", { 'name': name, @@ -2293,8 +2306,8 @@ class Daemon(AuthJSONRPCServer): }) return await self._publish_stream( - name, amount, claim_dict, file_path, certificate, - claim_address, change_address + account, name, amount, claim_dict, file_path, + certificate, claim_address, change_address ) @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) @@ -3248,16 +3261,17 @@ class Daemon(AuthJSONRPCServer): response['head_blob_availability'].get('is_available') defer.returnValue(response) - async def get_channel_or_error(self, channel_id: str = None, channel_name: str = None): + async def get_channel_or_error( + self, accounts: List[LBCAccount], channel_id: str = None, channel_name: str = None): if channel_id is not None: certificates = await self.wallet_manager.get_certificates( - private_key_accounts=[self.default_account], claim_id=channel_id) + private_key_accounts=accounts, claim_id=channel_id) if not certificates: raise ValueError("Couldn't find channel with claim_id '{}'." .format(channel_id)) return certificates[0] if channel_name is not None: certificates = await self.wallet_manager.get_certificates( - private_key_accounts=[self.default_account], claim_name=channel_name) + private_key_accounts=accounts, claim_name=channel_name) if not certificates: raise ValueError("Couldn't find channel with name '{}'.".format(channel_name)) return certificates[0] @@ -3268,6 +3282,12 @@ class Daemon(AuthJSONRPCServer): return self.default_account return self.get_account_or_error(account_id, argument_name, lbc_only) + def get_accounts_or_all(self, account_ids: List[str]): + return [ + self.get_account_or_error(account_id) + for account_id in account_ids + ] if account_ids else self.default_wallet.accounts + def get_account_or_error(self, account_id: str, argument_name: str = "account", lbc_only=True): for account in self.default_wallet.accounts: if account.id == account_id: diff --git a/lbrynet/daemon/Publisher.py b/lbrynet/daemon/Publisher.py index d49c77ec8..6f2fd5280 100644 --- a/lbrynet/daemon/Publisher.py +++ b/lbrynet/daemon/Publisher.py @@ -13,7 +13,9 @@ def d2f(d): class Publisher: - def __init__(self, blob_manager, payment_rate_manager, storage, lbry_file_manager, wallet, certificate): + def __init__(self, account, blob_manager, payment_rate_manager, storage, + lbry_file_manager, wallet, certificate): + self.account = account self.blob_manager = blob_manager self.payment_rate_manager = payment_rate_manager self.storage = storage @@ -44,7 +46,7 @@ class Publisher: claim_dict['stream']['source']['contentType'] = get_content_type(file_path) claim_dict['stream']['source']['version'] = "_0_0_1" # need current version here tx = await self.wallet.claim_name( - name, bid, claim_dict, self.certificate, holding_address + self.account, name, bid, claim_dict, self.certificate, holding_address ) # check if we have a file already for this claim (if this is a publish update with a new stream) @@ -65,7 +67,7 @@ class Publisher: async def publish_stream(self, name, bid, claim_dict, stream_hash, holding_address=None): """Make a claim without creating a lbry file""" tx = await self.wallet.claim_name( - name, bid, claim_dict, self.certificate, holding_address + self.account, name, bid, claim_dict, self.certificate, holding_address ) if stream_hash: # the stream_hash returned from the db will be None if this isn't a stream we have await d2f(self.storage.save_content_claim( diff --git a/lbrynet/wallet/manager.py b/lbrynet/wallet/manager.py index c7c8df0fb..1be212f08 100644 --- a/lbrynet/wallet/manager.py +++ b/lbrynet/wallet/manager.py @@ -341,8 +341,7 @@ class LbryWalletManager(BaseWalletManager): def get_utxos(account: BaseAccount): return account.get_utxos() - async def claim_name(self, name, amount, claim_dict, certificate=None, claim_address=None): - account = self.default_account + async def claim_name(self, account, name, amount, claim_dict, certificate=None, claim_address=None): claim = ClaimDict.load_dict(claim_dict) if not claim_address: claim_address = await account.receiving.get_or_create_usable_address() @@ -405,8 +404,7 @@ class LbryWalletManager(BaseWalletManager): # TODO: release reserved tx outputs in case anything fails by this point return tx - async def claim_new_channel(self, channel_name, amount): - account = self.default_account + async def claim_new_channel(self, channel_name, amount, account): address = await account.receiving.get_or_create_usable_address() cert, key = generate_certificate() tx = await Transaction.claim(channel_name, cert, amount, address, [account], account) diff --git a/tests/integration/wallet/test_commands.py b/tests/integration/wallet/test_commands.py index f44adcd51..276cf2d34 100644 --- a/tests/integration/wallet/test_commands.py +++ b/tests/integration/wallet/test_commands.py @@ -462,3 +462,71 @@ class AccountManagement(CommandTestCase): # list specific account response = await self.daemon.jsonrpc_account_list(account_id, include_claims=True) self.assertEqual(response['name'], 'recreated account') + + +class PublishCommand(CommandTestCase): + + VERBOSE = False + + async def test_publishing_checks_all_accounts_for_certificate(self): + account1_id, account1 = self.account.id, self.account + new_account = await self.daemon.jsonrpc_account_create('second account') + account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id']) + + spam_channel = await self.out(self.daemon.jsonrpc_channel_new('@spam', '1.0')) + self.assertTrue(spam_channel['success']) + await self.confirm_tx(spam_channel['tx']['txid']) + + self.assertEqual('8.989893', await self.daemon.jsonrpc_account_balance()) + + result = await self.out(self.daemon.jsonrpc_wallet_send( + '5.0', await self.daemon.jsonrpc_address_unused(account2_id) + )) + await self.confirm_tx(result['txid']) + + self.assertEqual('3.989769', await self.daemon.jsonrpc_account_balance()) + self.assertEqual('5.0', await self.daemon.jsonrpc_account_balance(account2_id)) + + baz_channel = await self.out(self.daemon.jsonrpc_channel_new('@baz', '1.0', account2_id)) + self.assertTrue(baz_channel['success']) + await self.confirm_tx(baz_channel['tx']['txid']) + + channels = await self.out(self.daemon.jsonrpc_channel_list(account1_id)) + self.assertEqual(len(channels), 1) + self.assertEqual(channels[0]['name'], '@spam') + self.assertEqual(channels, await self.out(self.daemon.jsonrpc_channel_list())) + + channels = await self.out(self.daemon.jsonrpc_channel_list(account2_id)) + self.assertEqual(len(channels), 1) + self.assertEqual(channels[0]['name'], '@baz') + + # defaults to using all accounts to lookup channel + with tempfile.NamedTemporaryFile() as file: + file.write(b'hi!') + file.flush() + claim1 = await self.out(self.daemon.jsonrpc_publish( + 'hovercraft', '1.0', file_path=file.name, channel_name='@baz' + )) + self.assertTrue(claim1['success']) + await self.confirm_tx(claim1['tx']['txid']) + + # uses only the specific accounts which contains the channel + with tempfile.NamedTemporaryFile() as file: + file.write(b'hi!') + file.flush() + claim1 = await self.out(self.daemon.jsonrpc_publish( + 'hovercraft', '1.0', file_path=file.name, + channel_name='@baz', channel_account_id=[account2_id] + )) + self.assertTrue(claim1['success']) + await self.confirm_tx(claim1['tx']['txid']) + + # fails when specifying account which does not contain channel + with tempfile.NamedTemporaryFile() as file: + file.write(b'hi!') + file.flush() + with self.assertRaisesRegex(ValueError, "Couldn't find channel with name '@baz'."): + await self.out(self.daemon.jsonrpc_publish( + 'hovercraft', '1.0', file_path=file.name, + channel_name='@baz', channel_account_id=[account1_id] + )) diff --git a/tox.ini b/tox.ini index 7190eaff2..9f6ad535b 100644 --- a/tox.ini +++ b/tox.ini @@ -19,4 +19,5 @@ commands = coverage run -p --source={envsitepackagesdir}/lbrynet -m unittest integration.wallet.test_transactions.BasicTransactionTest coverage run -p --source={envsitepackagesdir}/lbrynet -m twisted.trial --reactor=asyncio integration.cli coverage run -p --source={envsitepackagesdir}/lbrynet -m twisted.trial --reactor=asyncio integration.wallet.test_commands.AccountManagement + coverage run -p --source={envsitepackagesdir}/lbrynet -m twisted.trial --reactor=asyncio integration.wallet.test_commands.PublishCommand coverage run -p --source={envsitepackagesdir}/lbrynet -m twisted.trial --reactor=asyncio integration.wallet.test_commands.EpicAdventuresOfChris45