From d47575e8e0cf5b2464bababc0f551b68084b63da Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Sun, 24 Mar 2019 16:55:04 -0400 Subject: [PATCH] progress --- lbrynet/conf.py | 4 - lbrynet/extras/daemon/Components.py | 5 +- lbrynet/extras/daemon/Daemon.py | 1359 +++++++++-------- lbrynet/schema/claim.py | 265 ++-- .../{extras/daemon => schema}/mime_types.py | 0 lbrynet/stream/managed_stream.py | 2 +- lbrynet/wallet/account.py | 57 +- lbrynet/wallet/database.py | 43 +- lbrynet/wallet/ledger.py | 4 +- lbrynet/wallet/manager.py | 98 +- lbrynet/wallet/transaction.py | 65 +- setup.py | 1 + tests/integration/test_chris45.py | 72 +- tests/integration/test_claim_commands.py | 457 ++++-- tests/integration/test_file_commands.py | 38 +- tests/integration/test_resolve_command.py | 7 +- tests/integration/test_sync.py | 6 +- tests/integration/testcase.py | 52 +- tests/unit/lbrynet_daemon/test_mime_types.py | 2 +- 19 files changed, 1360 insertions(+), 1177 deletions(-) rename lbrynet/{extras/daemon => schema}/mime_types.py (100%) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index f8a78f605..7030d502e 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -21,10 +21,6 @@ CURRENCIES = { 'LBC': {'type': 'crypto'}, 'USD': {'type': 'fiat'}, } -SLACK_WEBHOOK = ( - 'nUE0pUZ6Yl9bo29epl5moTSwnl5wo20ip2IlqzywMKZiIQSFZR5' - 'AHx4mY0VmF0WQZ1ESEP9kMHZlp1WzJwWOoKN3ImR1M2yUAaMyqGZ=' -) HEADERS_FILE_SHA256_CHECKSUM = ( 366295, 'b0c8197153a33ccbc52fb81a279588b6015b68b7726f73f6a2b81f7e25bfe4b9' ) diff --git a/lbrynet/extras/daemon/Components.py b/lbrynet/extras/daemon/Components.py index bf8ffbdda..74bc30b17 100644 --- a/lbrynet/extras/daemon/Components.py +++ b/lbrynet/extras/daemon/Components.py @@ -265,10 +265,7 @@ class WalletComponent(Component): async def start(self): log.info("Starting torba wallet") - storage = self.component_manager.get_component(DATABASE_COMPONENT) - lbrynet.schema.BLOCKCHAIN_NAME = self.conf.blockchain_name - self.wallet_manager = await LbryWalletManager.from_lbrynet_config(self.conf, storage) - self.wallet_manager.old_db = storage + self.wallet_manager = await LbryWalletManager.from_lbrynet_config(self.conf) await self.wallet_manager.start() async def stop(self): diff --git a/lbrynet/extras/daemon/Daemon.py b/lbrynet/extras/daemon/Daemon.py index e395dabd5..ce338d3ed 100644 --- a/lbrynet/extras/daemon/Daemon.py +++ b/lbrynet/extras/daemon/Daemon.py @@ -8,7 +8,6 @@ import typing import aiohttp import base58 import random -from decimal import Decimal from urllib.parse import urlencode, quote from typing import Callable, Optional, List from binascii import hexlify, unhexlify @@ -18,8 +17,8 @@ from functools import wraps from torba.client.wallet import Wallet from torba.client.baseaccount import SingleKey, HierarchicalDeterministic -from lbrynet import __version__, utils -from lbrynet.conf import Config, Setting, SLACK_WEBHOOK +from lbrynet import utils +from lbrynet.conf import Config, Setting from lbrynet.blob.blob_file import is_valid_blobhash from lbrynet.blob_exchange.downloader import download_blob from lbrynet.error import InsufficientFundsError, DownloadSDTimeout, ComponentsNotStarted @@ -32,8 +31,8 @@ from lbrynet.extras.daemon.Components import EXCHANGE_RATE_MANAGER_COMPONENT, UP from lbrynet.extras.daemon.ComponentManager import RequiredCondition from lbrynet.extras.daemon.ComponentManager import ComponentManager from lbrynet.extras.daemon.json_response_encoder import JSONResponseEncoder -from lbrynet.extras.daemon.mime_types import guess_media_type from lbrynet.extras.daemon.undecorated import undecorated +from lbrynet.wallet.transaction import Transaction, Output from lbrynet.wallet.account import Account as LBCAccount, validate_claim_id from lbrynet.wallet.dewies import dewies_to_lbc, lbc_to_dewies from lbrynet.schema.claim import Claim @@ -728,31 +727,151 @@ class Daemon(metaclass=JSONRPCServerType): log.info("Get version info: " + json.dumps(platform_info)) return platform_info - async def jsonrpc_report_bug(self, message=None): + @requires(WALLET_COMPONENT) + async def jsonrpc_resolve(self, urls: typing.Union[str, list]): """ - Report a bug to slack + Get the claim that a URL refers to. Usage: - report_bug ( | --message=) + resolve ... Options: - --message= : (str) Description of the bug + --urls= : (str, list) one or more urls to resolve Returns: - (bool) true if successful + Dictionary of results, keyed by url + '': { + If a resolution error occurs: + 'error': Error message + + If the url resolves to a channel or a claim in a channel: + 'certificate': { + 'address': (str) claim address, + 'amount': (float) claim amount, + 'effective_amount': (float) claim amount including supports, + 'claim_id': (str) claim id, + 'claim_sequence': (int) claim sequence number (or -1 if unknown), + 'decoded_claim': (bool) whether or not the claim value was decoded, + 'height': (int) claim height, + 'depth': (int) claim depth, + 'has_signature': (bool) included if decoded_claim + 'name': (str) claim name, + 'permanent_url': (str) permanent url of the certificate claim, + 'supports: (list) list of supports [{'txid': (str) txid, + 'nout': (int) nout, + 'amount': (float) amount}], + 'txid': (str) claim txid, + 'nout': (str) claim nout, + 'signature_is_valid': (bool), included if has_signature, + 'value': ClaimDict if decoded, otherwise hex string + } + + If the url resolves to a channel: + 'claims_in_channel': (int) number of claims in the channel, + + If the url resolves to a claim: + 'claim': { + 'address': (str) claim address, + 'amount': (float) claim amount, + 'effective_amount': (float) claim amount including supports, + 'claim_id': (str) claim id, + 'claim_sequence': (int) claim sequence number (or -1 if unknown), + 'decoded_claim': (bool) whether or not the claim value was decoded, + 'height': (int) claim height, + 'depth': (int) claim depth, + 'has_signature': (bool) included if decoded_claim + 'name': (str) claim name, + 'permanent_url': (str) permanent url of the claim, + 'channel_name': (str) channel name if claim is in a channel + 'supports: (list) list of supports [{'txid': (str) txid, + 'nout': (int) nout, + 'amount': (float) amount}] + 'txid': (str) claim txid, + 'nout': (str) claim nout, + 'signature_is_valid': (bool), included if has_signature, + 'value': ClaimDict if decoded, otherwise hex string + } + } """ - platform_name = system_info.get_platform()['platform'] - webhook = utils.deobfuscate(SLACK_WEBHOOK) - payload = json.dumps({ - "text": f"os: {platform_name}\n" - f" version: {__version__}\n" - f"<{get_loggly_query_string(self.installation_id)}|loggly>\n" - f"{message}" - }) - async with aiohttp.request('post', webhook, data=payload): - pass - return True + if isinstance(urls, str): + urls = [urls] + + results = {} + + valid_urls = set() + for u in urls: + try: + parse_lbry_uri(u) + valid_urls.add(u) + except URIParseError: + results[u] = {"error": "%s is not a valid url" % u} + + resolved = await self.wallet_manager.resolve(*tuple(valid_urls)) + + for resolved_uri in resolved: + results[resolved_uri] = resolved[resolved_uri] + + return results + + @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT, + STREAM_MANAGER_COMPONENT, + conditions=[WALLET_IS_UNLOCKED]) + async def jsonrpc_get(self, uri, file_name=None, timeout=None): + """ + Download stream from a LBRY name. + + Usage: + get [ | --file_name=] [ | --timeout=] + + + Options: + --uri= : (str) uri of the content to download + --file_name= : (str) specified name for the downloaded file + --timeout= : (int) download timeout in number of seconds + + Returns: + (dict) Dictionary containing information about the stream + { + 'completed': (bool) true if download is completed, + 'file_name': (str) name of file, + 'download_directory': (str) download directory, + 'points_paid': (float) credit paid to download file, + 'stopped': (bool) true if download is stopped, + 'stream_hash': (str) stream hash of file, + 'stream_name': (str) stream name , + 'suggested_file_name': (str) suggested file name, + 'sd_hash': (str) sd hash of file, + 'download_path': (str) download path of file, + 'mime_type': (str) mime type of file, + 'key': (str) key attached to file, + 'total_bytes': (int) file size in bytes, + 'written_bytes': (int) written size in bytes, + 'blobs_completed': (int) number of fully downloaded blobs, + 'blobs_in_stream': (int) total blobs on stream, + 'status': (str) downloader status, + 'claim_id': (str) claim id, + 'outpoint': (str) claim outpoint string, + 'txid': (str) claim txid, + 'nout': (int) claim nout, + 'metadata': (dict) claim metadata, + 'channel_claim_id': (str) None if claim is not signed + 'channel_name': (str) None if claim is not signed + 'claim_name': (str) claim name + } + """ + + try: + stream = await self.stream_manager.download_stream_from_uri( + uri, self.exchange_rate_manager, file_name, timeout + ) + if not stream: + raise DownloadSDTimeout(uri) + except Exception as e: + log.warning("Error downloading %s: %s", uri, str(e)) + return {"error": str(e)} + else: + return stream.as_dict() SETTINGS_DOC = """ Settings management. @@ -1457,200 +1576,6 @@ class Daemon(metaclass=JSONRPCServerType): ) ] - CLAIM_DOC = """ - Claim management. - """ - - @requires(WALLET_COMPONENT) - async def jsonrpc_claim_show(self, txid=None, nout=None, claim_id=None): - """ - Resolve claim info from txid/nout or with claim ID - - Usage: - claim_show [ | --txid=] [ | --nout=] - [ | --claim_id=] - - Options: - --txid= : (str) look for claim with this txid, nout must - also be specified - --nout= : (int) look for claim with this nout, txid must - also be specified - --claim_id= : (str) look for claim with this claim id - - Returns: - (dict) Dictionary containing claim info as below, - - { - 'txid': (str) txid of claim - 'nout': (int) nout of claim - 'amount': (float) amount of claim - 'value': (str) value of claim - 'height' : (int) height of claim takeover - 'claim_id': (str) claim ID of claim - 'supports': (list) list of supports associated with claim - } - - if claim cannot be resolved, dictionary as below will be returned - - { - 'error': (str) reason for error - } - - """ - if claim_id is not None and txid is None and nout is None: - claim_results = await self.wallet_manager.get_claim_by_claim_id(claim_id) - elif txid is not None and nout is not None and claim_id is None: - claim_results = await self.wallet_manager.get_claim_by_outpoint(txid, int(nout)) - else: - raise Exception("Must specify either txid/nout, or claim_id") - return claim_results - - @requires(WALLET_COMPONENT) - async def jsonrpc_resolve(self, urls: typing.Union[str, list]): - """ - Get the claim that a URL refers to. - - Usage: - resolve ... - - Options: - --urls= : (str, list) one or more urls to resolve - - Returns: - Dictionary of results, keyed by url - '': { - If a resolution error occurs: - 'error': Error message - - If the url resolves to a channel or a claim in a channel: - 'certificate': { - 'address': (str) claim address, - 'amount': (float) claim amount, - 'effective_amount': (float) claim amount including supports, - 'claim_id': (str) claim id, - 'claim_sequence': (int) claim sequence number (or -1 if unknown), - 'decoded_claim': (bool) whether or not the claim value was decoded, - 'height': (int) claim height, - 'depth': (int) claim depth, - 'has_signature': (bool) included if decoded_claim - 'name': (str) claim name, - 'permanent_url': (str) permanent url of the certificate claim, - 'supports: (list) list of supports [{'txid': (str) txid, - 'nout': (int) nout, - 'amount': (float) amount}], - 'txid': (str) claim txid, - 'nout': (str) claim nout, - 'signature_is_valid': (bool), included if has_signature, - 'value': ClaimDict if decoded, otherwise hex string - } - - If the url resolves to a channel: - 'claims_in_channel': (int) number of claims in the channel, - - If the url resolves to a claim: - 'claim': { - 'address': (str) claim address, - 'amount': (float) claim amount, - 'effective_amount': (float) claim amount including supports, - 'claim_id': (str) claim id, - 'claim_sequence': (int) claim sequence number (or -1 if unknown), - 'decoded_claim': (bool) whether or not the claim value was decoded, - 'height': (int) claim height, - 'depth': (int) claim depth, - 'has_signature': (bool) included if decoded_claim - 'name': (str) claim name, - 'permanent_url': (str) permanent url of the claim, - 'channel_name': (str) channel name if claim is in a channel - 'supports: (list) list of supports [{'txid': (str) txid, - 'nout': (int) nout, - 'amount': (float) amount}] - 'txid': (str) claim txid, - 'nout': (str) claim nout, - 'signature_is_valid': (bool), included if has_signature, - 'value': ClaimDict if decoded, otherwise hex string - } - } - """ - - if isinstance(urls, str): - urls = [urls] - - results = {} - - valid_urls = set() - for u in urls: - try: - parse_lbry_uri(u) - valid_urls.add(u) - except URIParseError: - results[u] = {"error": "%s is not a valid url" % u} - - resolved = await self.wallet_manager.resolve(*tuple(valid_urls)) - - for resolved_uri in resolved: - results[resolved_uri] = resolved[resolved_uri] - - return results - - @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT, - STREAM_MANAGER_COMPONENT, - conditions=[WALLET_IS_UNLOCKED]) - async def jsonrpc_get(self, uri, file_name=None, timeout=None): - """ - Download stream from a LBRY name. - - Usage: - get [ | --file_name=] [ | --timeout=] - - - Options: - --uri= : (str) uri of the content to download - --file_name= : (str) specified name for the downloaded file - --timeout= : (int) download timeout in number of seconds - - Returns: - (dict) Dictionary containing information about the stream - { - 'completed': (bool) true if download is completed, - 'file_name': (str) name of file, - 'download_directory': (str) download directory, - 'points_paid': (float) credit paid to download file, - 'stopped': (bool) true if download is stopped, - 'stream_hash': (str) stream hash of file, - 'stream_name': (str) stream name , - 'suggested_file_name': (str) suggested file name, - 'sd_hash': (str) sd hash of file, - 'download_path': (str) download path of file, - 'mime_type': (str) mime type of file, - 'key': (str) key attached to file, - 'total_bytes': (int) file size in bytes, - 'written_bytes': (int) written size in bytes, - 'blobs_completed': (int) number of fully downloaded blobs, - 'blobs_in_stream': (int) total blobs on stream, - 'status': (str) downloader status, - 'claim_id': (str) claim id, - 'outpoint': (str) claim outpoint string, - 'txid': (str) claim txid, - 'nout': (int) claim nout, - 'metadata': (dict) claim metadata, - 'channel_claim_id': (str) None if claim is not signed - 'channel_name': (str) None if claim is not signed - 'claim_name': (str) claim name - } - """ - - try: - stream = await self.stream_manager.download_stream_from_uri( - uri, self.exchange_rate_manager, file_name, timeout - ) - if not stream: - raise DownloadSDTimeout(uri) - except Exception as e: - log.warning("Error downloading %s: %s", uri, str(e)) - return {"error": str(e)} - else: - return stream.as_dict() - @requires(STREAM_MANAGER_COMPONENT) async def jsonrpc_file_set_status(self, status, **kwargs): """ @@ -1774,58 +1699,150 @@ class Daemon(metaclass=JSONRPCServerType): """ @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - async def jsonrpc_channel_new(self, channel_name, amount, account_id=None): + async def jsonrpc_channel_create( + self, name, bid, allow_duplicate_name=False, account_id=None, claim_address=None, preview=False, **kwargs): """ - Generate a publisher key and create a new '@' prefixed certificate claim + Generate a publisher key and create a new '@' prefixed channel claim. Usage: - channel_new ( | --channel_name=) - ( | --amount=) - [--account_id=] + channel create ( | --name=) ( | --bid=) + [--tags=...] [--allow_duplicate_name=] + [--title=] [--description=<description>] [--language=<language>] + [--contact_email=<contact_email>] + [--homepage_url=<homepage_url>] [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>] + [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] Options: - --channel_name=<channel_name> : (str) name of the channel prefixed with '@' - --amount=<amount> : (decimal) bid amount on the channel - --account_id=<account_id> : (str) id of the account to store channel - - Returns: - (dict) Dictionary containing result of the claim - { - 'tx' : (str) hex encoded transaction - 'txid' : (str) txid of resulting claim - 'nout' : (int) nout of the resulting claim - 'fee' : (float) fee paid for the claim transaction - 'claim_id' : (str) claim ID of the resulting claim - } + --name=<name> : (str) name of the channel prefixed with '@' + --allow_duplicate_name=<allow_duplicate_name> : (bool) create new channel even if one already exists with + given name. default: false. + --bid=<bid> : (decimal) amount to back the claim + --tags=<tags> : (list) content tags + --title=<title> : (str) title of the publication + --description=<description> : (str) description of the publication + --language=<language> : (str) primary language of the channel + --contact_email=<contact_email>: (str) email of channel owner + --homepage_url=<homepage_url> : (str) homepage url + --thumbnail_url=<thumbnail_url>: (str) thumbnail url + --cover_url=<cover_url> : (str) url of cover image + --account_id=<account_id> : (str) id of the account to store channel + --claim_address=<claim_address>: (str) address where the channel is sent to, if not specified + it will be determined automatically from the account + --preview : (bool) do not broadcast the transaction """ - try: - parsed = parse_lbry_uri(channel_name) - if not parsed.contains_channel: - raise Exception("Cannot make a new channel for a non channel name") - if parsed.path: - raise Exception("Invalid channel uri") - except (TypeError, URIParseError): - raise Exception("Invalid channel name") + account = self.get_account_or_default(account_id) + name = self.get_channel_name_or_error(name) + amount = self.get_dewies_or_error('bid', bid, positive_value=True) + claim_address = await self.get_receiving_address(claim_address, account) - amount = self.get_dewies_or_error("amount", amount) - if amount <= 0: - raise Exception("Invalid amount") + existing_channels = await account.get_channels(claim_name=name) + if len(existing_channels) > 0: + if not allow_duplicate_name: + raise Exception( + f"You already have a channel under the name '{name}'. " + f"Use --allow-duplicate-name flag to override." + ) - tx = await self.wallet_manager.claim_new_channel( - channel_name, amount, self.get_account_or_default(account_id) + claim = Claim() + claim.channel.update(**kwargs) + tx = await Transaction.claim_create( + name, claim, amount, claim_address, [account], account ) - self.default_wallet.save() - await self.analytics_manager.send_new_channel() - nout = 0 - txo = tx.outputs[nout] - log.info("Claimed a new channel! lbry://%s txid: %s nout: %d", channel_name, tx.id, nout) - return { - "success": True, - "tx": tx, - "claim_id": txo.claim_id, - "claim_address": txo.get_address(self.ledger), - "output": txo - } + txo = tx.outputs[0] + txo.generate_channel_private_key() + + if not preview: + await tx.sign([account]) + await account.ledger.broadcast(tx) + account.add_channel_private_key(txo.ref, txo.private_key) + self.default_wallet.save() + await self.storage.save_claims([self._old_get_temp_claim_info( + tx, txo, claim_address, claim, name, dewies_to_lbc(amount) + )]) + await self.analytics_manager.send_new_channel() + else: + await account.ledger.release_tx(tx) + + return tx + + @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) + async def jsonrpc_channel_update( + self, claim_id, bid=None, account_id=None, claim_address=None, + new_signing_key=False, preview=False, **kwargs): + """ + Update attributes of a channel. + + Usage: + channel update (<claim_id> | --claim_id=<claim_id>) [<bid> | --bid=<bid>] + [--tags=<tags>...] [--clear-tags] [--title=<title>] [--description=<description>] + [--language=<language>] [--contact_email=<contact_email>] + [--homepage_url=<homepage_url>] [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>] + [--account_id=<account_id>] [--claim_address=<claim_address>] [--new-signing-key] [--preview] + + Options: + --claim_id=<claim_id> : (str) claim_id of the channel to update + --bid=<bid> : (decimal) amount to back the claim + --tags=<tags> : (list) add content tags + --clear-tags : (bool) clear existing tags (prior to adding new ones) + --title=<title> : (str) title of the publication + --description=<description> : (str) description of the publication + --language=<language> : (str) primary language of the channel + --contact_email=<contact_email>: (str) email of channel owner + --homepage_url=<homepage_url> : (str) homepage url + --thumbnail_url=<thumbnail_url>: (str) thumbnail url + --cover_url=<cover_url> : (str) url of cover image + --account_id=<account_id> : (str) id of the account to store channel + --claim_address=<claim_address>: (str) address where the channel is sent + --new-signing-key : (bool) generate a new signing key, will invalidate all previous publishes + --preview : (bool) do not broadcast the transaction + """ + account = self.get_account_or_default(account_id) + + existing_channels = await account.get_claims(claim_id=claim_id) + if len(existing_channels) != 1: + raise Exception( + f"Can't find the channel '{claim_id}' in account '{account_id}'." + ) + old_txo = existing_channels[0] + if not old_txo.claim.is_channel: + raise Exception( + f"A claim with id '{claim_id}' was found but it is not a channel." + ) + + if bid is not None: + amount = self.get_dewies_or_error('bid', bid, positive_value=True) + else: + amount = old_txo.amount + + if claim_address is not None: + self.ledger.is_valid_address(claim_address) + else: + claim_address = old_txo.get_address(account.ledger) + + old_txo.claim.channel.update(**kwargs) + tx = await Transaction.claim_update( + old_txo, amount, claim_address, [account], account + ) + new_txo = tx.outputs[0] + + if new_signing_key: + new_txo.generate_channel_private_key() + else: + new_txo.private_key = old_txo.private_key + + if not preview: + await tx.sign([account]) + await account.ledger.broadcast(tx) + account.add_channel_private_key(new_txo.ref, new_txo.private_key) + self.default_wallet.save() + await self.storage.save_claims([self._old_get_temp_claim_info( + tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name, dewies_to_lbc(amount) + )]) + await self.analytics_manager.send_new_channel() + else: + await account.ledger.release_tx(tx) + + return tx @requires(WALLET_COMPONENT) def jsonrpc_channel_list(self, account_id=None, page=None, page_size=None): @@ -1886,58 +1903,41 @@ class Daemon(metaclass=JSONRPCServerType): return await self.wallet_manager.import_certificate_info(serialized_certificate_info) + CLAIM_DOC = """ + Claim management. + """ + @requires(WALLET_COMPONENT, STREAM_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_publish( - self, name, bid, file_path=None, fee=None, title=None, - description=None, author=None, language=None, license=None, - license_url=None, thumbnail=None, preview=None, nsfw=None, - channel_name=None, channel_id=None, channel_account_id=None, account_id=None, - claim_address=None, change_address=None): + self, name, bid, file_path, allow_duplicate_name=False, + channel_id=None, channel_account_id=None, + account_id=None, claim_address=None, preview=False, **kwargs): """ - Make a new name claim and publish associated data to lbrynet, - update over existing claim if user already has a claim for name. - - Fields required in the final Metadata are: - 'title' - 'description' - 'author' - 'language' - 'license' - 'nsfw' - - Metadata can be set by either using the metadata argument or by setting individual arguments - fee, title, description, author, language, license, license_url, thumbnail, preview, nsfw, - or sources. Individual arguments will overwrite the fields specified in metadata argument. + Make a new name claim and publish associated data to lbrynet. Usage: - publish (<name> | --name=<name>) (<bid> | --bid=<bid>) [--metadata=<metadata>] - [--file_path=<file_path>] [--fee=<fee>] [--title=<title>] - [--description=<description>] [--author=<author>] [--language=<language>] - [--license=<license>] [--license_url=<license_url>] [--thumbnail=<thumbnail>] - [--preview=<preview>] [--nsfw=<nsfw>] - [--channel_name=<channel_name>] [--channel_id=<channel_id>] - [--channel_account_id=<channel_account_id>...] [--account_id=<account_id>] - [--claim_address=<claim_address>] [--change_address=<change_address>] + publish (<name> | --name=<name>) (<bid> | --bid=<bid>) (<file_path> | --file_path=<file_path>) + [--tags=<tags>...] [--allow_duplicate_name=<allow_duplicate_name>] + [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>] + [--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>] + [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>] + [--release_time=<release_time>] [--duration=<duration>] + [--video_width=<video_width>] [--video_height=<video_height>] + [--channel_id=<channel_id>] [--channel_account_id=<channel_account_id>...] + [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] Options: --name=<name> : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash)) + --allow_duplicate_name=<allow_duplicate_name> : (bool) create new claim even if one already exists with + given name. default: false. --bid=<bid> : (decimal) amount to back the claim - --metadata=<metadata> : (dict) ClaimDict to associate with the claim. - --file_path=<file_path> : (str) path to file to be associated with name. If provided, - a lbry stream of this file will be used in 'sources'. - If no path is given but a sources dict is provided, - it will be used. If neither are provided, an - error is raised. - --fee=<fee> : (dict) Dictionary representing key fee to download content: - { - 'currency': currency_symbol, - 'amount': decimal, - 'address': str, optional - } - supported currencies: LBC, USD, BTC - If an address is not provided a new one will be - automatically generated. Default fee is zero. + --file_path=<file_path> : (str) path to file to be associated with name. + --tags=<tags> : (list) content tags + --fee_currency=<fee_currency> : (string) specify fee currency + --fee_amount=<fee_amount> : (decimal) content download fee + --fee_address=<fee_address> : (str) address where to send fee payments, will use + value from --claim_address if not provided --title=<title> : (str) title of the publication --description=<description> : (str) description of the publication --author=<author> : (str) author of the publication. The usage for this field is not @@ -1948,164 +1948,216 @@ class Daemon(metaclass=JSONRPCServerType): --language=<language> : (str) language of the publication --license=<license> : (str) publication license --license_url=<license_url> : (str) publication license url - --thumbnail=<thumbnail> : (str) thumbnail url - --preview=<preview> : (str) preview url - --nsfw=<nsfw> : (bool) whether the content is nsfw - --channel_name=<channel_name> : (str) name of the publisher channel name in the wallet - --channel_id=<channel_id> : (str) claim id of the publisher channel, does not check - for channel claim being in the wallet. This allows - publishing to a channel where only the certificate - private key is in the wallet. + --thumbnail_url=<thumbnail_url>: (str) thumbnail url + --release_time=<duration> : (int) original public release of content, seconds since UNIX epoch + --duration=<duration> : (int) audio/video duration in seconds, an attempt will be made to + calculate this automatically if not provided + --video_width=<video_width> : (int) video width + --video_height=<video_height> : (int) video height + --channel_id=<channel_id> : (str) claim id of the publisher channel --channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in - for channel certificates, defaults to all accounts. - --account_id=<account_id> : (str) account to use for funding the transaction - --claim_address=<claim_address> : (str) address where the claim is sent to, if not specified - new address will automatically be created - - Returns: - (dict) Dictionary containing result of the claim - { - 'tx' : (str) hex encoded transaction - 'txid' : (str) txid of resulting claim - 'nout' : (int) nout of the resulting claim - 'fee' : (decimal) fee paid for the claim transaction - 'claim_id' : (str) claim ID of the resulting claim - } + for channel certificates, defaults to all accounts. + --account_id=<account_id> : (str) account to use for funding the transaction + --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified + it will be determined automatically from the account + --preview : (bool) do not broadcast the transaction """ - - try: - parsed = parse_lbry_uri(name) - if parsed.name != name: - raise Exception("Name given to publish has invalid characters") - except (TypeError, URIParseError): - raise Exception("Invalid name given to publish") - - amount = self.get_dewies_or_error('bid', bid) - if amount <= 0: - raise ValueError("Bid value must be greater than 0.0") - - for address in [claim_address, change_address]: - if address is not None: - # raises an error if the address is invalid - self.ledger.is_valid_address(address) - account = self.get_account_or_default(account_id) + channel = await self.get_channel_or_none(channel_account_id, channel_id, for_signing=True) + name = self.get_claim_name_or_error(name) + amount = self.get_dewies_or_error('bid', bid, positive_value=True) + claim_address = await self.get_receiving_address(claim_address, account) + kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address) - available = await account.get_balance() existing_claims = await account.get_claims(claim_name=name) - if len(existing_claims) > 1: - log.warning("found %i claims for the name %s", len(existing_claims), name) - if amount >= available: - if len(existing_claims) == 1: - available += existing_claims[0].get_estimator(self.ledger).effective_amount - if amount >= available: - raise InsufficientFundsError( - f"Please lower the bid value, the maximum amount " - f"you can specify for this claim is {dewies_to_lbc(available)}." + if len(existing_claims) > 0: + if not allow_duplicate_name: + raise Exception( + f"You already have a claim published under the name '{name}'. " + f"Use --allow-duplicate-name flag to override." ) claim = Claim() - stream = claim.stream - if title is not None: - stream.title = title - if description is not None: - stream.description = description - if author is not None: - stream.author = author - if language is not None: - stream.language = language - if license is not None: - stream.license = license - if license_url is not None: - stream.license_url = license_url - if thumbnail is not None: - stream.thumbnail_url = thumbnail - - # check for original deprecated format {'currency':{'address','amount'}} - # add address, version to fee if unspecified - if fee is not None: - if fee['currency'] == 'LBC': - stream.fee.lbc = Decimal(fee['amount']) - elif fee['currency'] == 'USD': - stream.fee.usd = Decimal(fee['amount']) - if 'address' in fee: - stream.fee.address = fee['address'] - else: - stream.fee.address = await account.receiving.get_or_create_usable_address() - - sd_to_delete = None - - if file_path: - if not os.path.isfile(file_path): - raise Exception("invalid file path to publish") - if os.path.getsize(file_path) == 0: - raise Exception(f"Cannot publish empty file {file_path}") - if existing_claims: - sd_to_delete = existing_claims[-1].claim.stream.hash - - # since the file hasn't yet been made into a stream, we don't have - # a valid Source for the claim when validating the format, we'll use a fake one - stream.hash = '0' * 96 - elif not existing_claims: - raise Exception("no previous stream to update") - else: - stream.hash = '0' * 96 - stream.hash_bytes = existing_claims[-1].claim.stream.hash_bytes - certificate = None - if channel_id or channel_name: - certificate = await self.get_channel_or_error( - self.get_accounts_or_all(channel_account_id), channel_id, channel_name - ) - - if file_path: - stream = await self.stream_manager.create_stream(file_path) - claim.stream.hash = stream.sd_hash - claim.stream.media_type = guess_media_type(file_path) - - if sd_to_delete: - stream_hash_to_delete = await self.storage.get_stream_hash_for_sd_hash(sd_to_delete) - stream_to_delete = self.stream_manager.get_stream_by_stream_hash(stream_hash_to_delete) - if stream_to_delete: - await self.stream_manager.delete_stream(stream_to_delete, delete_file=False) - log.info("updating claim to stream generated from %s, deleted previous stream %s", - sd_to_delete[:8], file_path) - else: - log.info("previous stream %s from claim was not saved locally, nothing to delete", - sd_to_delete[:8]) - else: - log.info("generated stream from %s for claim", file_path) - else: - log.info("updating claim with stream %s from previous", claim.stream.hash[:8]) - - sd_hash = claim.stream.hash - log.info("Publish: %s", { - 'name': name, - 'file_path': file_path, - 'bid': dewies_to_lbc(amount), - 'claim_address': claim_address, - 'change_address': change_address, - 'channel_id': channel_id, - 'channel_name': channel_name - }) - tx = await self.wallet_manager.claim_name( - account, name, amount, claim, certificate, claim_address + claim.stream.update(file_path=file_path, hash='0'*96, **kwargs) + tx = await Transaction.claim_create( + name, claim, amount, claim_address, [account], account, channel ) - stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash) - if stream_hash: - await self.storage.save_content_claim( - stream_hash, tx.outputs[0].id + new_txo = tx.outputs[0] + + if not preview: + file_stream = await self.stream_manager.create_stream(file_path) + claim.stream.hash = file_stream.sd_hash + if channel: + new_txo.sign(channel) + await tx.sign([account]) + await account.ledger.broadcast(tx) + await self.storage.save_claims([self._old_get_temp_claim_info( + tx, new_txo, claim_address, claim, name, dewies_to_lbc(amount) + )]) + stream_hash = await self.storage.get_stream_hash_for_sd_hash(claim.stream.hash) + if stream_hash: + await self.storage.save_content_claim(stream_hash, new_txo.id) + await self.analytics_manager.send_claim_action('publish') + else: + await account.ledger.release_tx(tx) + + return tx + + @requires(WALLET_COMPONENT, STREAM_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT, + conditions=[WALLET_IS_UNLOCKED]) + async def jsonrpc_claim_update( + self, claim_id, bid=None, file_path=None, + channel_id=None, channel_account_id=None, clear_channel=False, + account_id=None, claim_address=None, + preview=False, **kwargs): + """ + Modify an existing claim. + + Usage: + claim update (<claim_id> | --claim_id=<claim_id>) + [--bid=<bid>] [--file_path=<file_path>] [--tags=<tags>...] [--clear-tags] + [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>] + [--title=<title>] [--description=<description>] [--author=<author>] [--language=<language>] + [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>] + [--release_time=<release_time>] [--duration=<duration>] + [--video_width=<video_width>] [--video_height=<video_height>] + [--channel_id=<channel_id>] [--channel_account_id=<channel_account_id>...] [--clear-channel] + [--account_id=<account_id>] [--claim_address=<claim_address>] [--preview] + + Options: + --claim_id=<claim_id> : (str) id of the claim to update + --bid=<bid> : (decimal) amount to back the claim + --file_path=<file_path> : (str) path to file to be associated with name. + --tags=<tags> : (list) content tags + --clear-tags : (bool) clear existing tags (prior to adding new ones) + --fee_currency=<fee_currency> : (string) specify fee currency + --fee_amount=<fee_amount> : (decimal) content download fee + --fee_address=<fee_address> : (str) address where to send fee payments, will use + value from --claim_address if not provided + --title=<title> : (str) title of the publication + --description=<description> : (str) description of the publication + --author=<author> : (str) author of the publication. The usage for this field is not + the same as for channels. The author field is used to credit an author + who is not the publisher and is not represented by the channel. For + example, a pdf file of 'The Odyssey' has an author of 'Homer' but may + by published to a channel such as '@classics', or to no channel at all + --language=<language> : (str) language of the publication + --license=<license> : (str) publication license + --license_url=<license_url> : (str) publication license url + --thumbnail_url=<thumbnail_url>: (str) thumbnail url + --release_time=<duration> : (int) original public release of content, seconds since UNIX epoch + --duration=<duration> : (int) audio/video duration in seconds, an attempt will be made to + calculate this automatically if not provided + --video_width=<video_width> : (int) video width + --video_height=<video_height> : (int) video height + --channel_id=<channel_id> : (str) claim id of the publisher channel + --clear-channel : (bool) remove channel signature + --channel_account_id=<channel_id>: (str) one or more account ids for accounts to look in + for channel certificates, defaults to all accounts. + --account_id=<account_id> : (str) account to use for funding the transaction + --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified + it will be determined automatically from the account + --preview : (bool) do not broadcast the transaction + """ + account = self.get_account_or_default(account_id) + + existing_claims = await account.get_claims(claim_id=claim_id) + if len(existing_claims) != 1: + raise Exception( + f"Can't find the claim '{claim_id}' in account '{account_id}'." ) - await self.analytics_manager.send_claim_action('publish') - nout = 0 - txo = tx.outputs[nout] - log.info("Success! Published to lbry://%s txid: %s nout: %d", name, tx.id, nout) - return { - "success": True, - "tx": tx, - "claim_id": txo.claim_id, - "claim_address": self.ledger.hash160_to_address(txo.script.values['pubkey_hash']), - "output": tx.outputs[nout] - } + old_txo = existing_claims[0] + if not old_txo.claim.is_stream: + raise Exception( + f"A claim with id '{claim_id}' was found but it is not a stream claim." + ) + + if bid is not None: + amount = self.get_dewies_or_error('bid', bid, positive_value=True) + else: + amount = old_txo.amount + + if claim_address is not None: + self.ledger.is_valid_address(claim_address) + else: + claim_address = old_txo.get_address(account.ledger) + + channel = None + if channel_id: + channel = await self.get_channel_or_error(channel_account_id, channel_id, for_signing=True) + elif old_txo.claim.is_signed and not clear_channel: + channel = old_txo.channel + + kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address) + old_txo.claim.stream.update(**kwargs) + tx = await Transaction.claim_update( + old_txo, amount, claim_address, [account], account, channel + ) + new_txo = tx.outputs[0] + + if not preview: + if file_path is not None: + file_stream = await self.stream_manager.create_stream(file_path) + new_txo.claim.stream.hash = file_stream.sd_hash + if channel: + new_txo.sign(channel) + await tx.sign([account]) + await account.ledger.broadcast(tx) + await self.storage.save_claims([self._old_get_temp_claim_info( + tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name, dewies_to_lbc(amount) + )]) + stream_hash = await self.storage.get_stream_hash_for_sd_hash(new_txo.claim.stream.hash) + if stream_hash: + await self.storage.save_content_claim(stream_hash, new_txo.id) + await self.analytics_manager.send_claim_action('publish') + else: + await account.ledger.release_tx(tx) + + return tx + + @requires(WALLET_COMPONENT) + async def jsonrpc_claim_show(self, txid=None, nout=None, claim_id=None): + """ + Resolve claim info from txid/nout or with claim ID + + Usage: + claim_show [<txid> | --txid=<txid>] [<nout> | --nout=<nout>] + [<claim_id> | --claim_id=<claim_id>] + + Options: + --txid=<txid> : (str) look for claim with this txid, nout must + also be specified + --nout=<nout> : (int) look for claim with this nout, txid must + also be specified + --claim_id=<claim_id> : (str) look for claim with this claim id + + Returns: + (dict) Dictionary containing claim info as below, + + { + 'txid': (str) txid of claim + 'nout': (int) nout of claim + 'amount': (float) amount of claim + 'value': (str) value of claim + 'height' : (int) height of claim takeover + 'claim_id': (str) claim ID of claim + 'supports': (list) list of supports associated with claim + } + + if claim cannot be resolved, dictionary as below will be returned + + { + 'error': (str) reason for error + } + + """ + if claim_id is not None and txid is None and nout is None: + claim_results = await self.wallet_manager.get_claim_by_claim_id(claim_id) + elif txid is not None and nout is not None and claim_id is None: + claim_results = await self.wallet_manager.get_claim_by_outpoint(txid, int(nout)) + else: + raise Exception("Must specify either txid/nout, or claim_id") + return claim_results @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) async def jsonrpc_claim_abandon(self, claim_id=None, txid=None, nout=None, account_id=None, blocking=True): @@ -2147,106 +2199,8 @@ class Daemon(metaclass=JSONRPCServerType): await self.ledger.wait(tx) return {"success": True, "tx": tx} - @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - async def jsonrpc_claim_new_support(self, name, claim_id, amount, account_id=None): - """ - Support a name claim - - Usage: - claim_new_support (<name> | --name=<name>) (<claim_id> | --claim_id=<claim_id>) - (<amount> | --amount=<amount>) [--account_id=<account_id>] - - Options: - --name=<name> : (str) name of the claim to support - --claim_id=<claim_id> : (str) claim_id of the claim to support - --amount=<amount> : (decimal) amount of support - --account_id=<account_id> : (str) id of the account to use - - Returns: - (dict) Dictionary containing the transaction information - { - "hex": (str) raw transaction, - "inputs": (list) inputs(dict) used for the transaction, - "outputs": (list) outputs(dict) for the transaction, - "total_fee": (int) fee in dewies, - "total_input": (int) total of inputs in dewies, - "total_output": (int) total of outputs in dewies(input - fees), - "txid": (str) txid of the transaction, - } - """ - account = self.get_account_or_default(account_id) - amount = self.get_dewies_or_error("amount", amount) - result = await self.wallet_manager.support_claim(name, claim_id, amount, account) - await self.analytics_manager.send_claim_action('new_support') - return result - - @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - async def jsonrpc_claim_tip(self, claim_id, amount, account_id=None): - """ - Tip the owner of the claim - - Usage: - claim_tip (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>) - [--account_id=<account_id>] - - Options: - --claim_id=<claim_id> : (str) claim_id of the claim to support - --amount=<amount> : (decimal) amount of support - --account_id=<account_id> : (str) id of the account to use - - Returns: - (dict) Dictionary containing the transaction information - { - "hex": (str) raw transaction, - "inputs": (list) inputs(dict) used for the transaction, - "outputs": (list) outputs(dict) for the transaction, - "total_fee": (int) fee in dewies, - "total_input": (int) total of inputs in dewies, - "total_output": (int) total of outputs in dewies(input - fees), - "txid": (str) txid of the transaction, - } - """ - account = self.get_account_or_default(account_id) - amount = self.get_dewies_or_error("amount", amount) - validate_claim_id(claim_id) - result = await self.wallet_manager.tip_claim(amount, claim_id, account) - await self.analytics_manager.send_claim_action('new_support') - return result - - @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) - def jsonrpc_claim_send_to_address(self, claim_id, address, amount=None): - """ - Send a name claim to an address - - Usage: - claim_send_to_address (<claim_id> | --claim_id=<claim_id>) - (<address> | --address=<address>) - [<amount> | --amount=<amount>] - - Options: - --claim_id=<claim_id> : (str) claim_id to send - --address=<address> : (str) address to send the claim to - --amount=<amount> : (int) Amount of credits to claim name for, - defaults to the current amount on the claim - - Returns: - (dict) Dictionary containing result of the claim - { - 'tx' : (str) hex encoded transaction - 'txid' : (str) txid of resulting claim - 'nout' : (int) nout of the resulting claim - 'fee' : (float) fee paid for the claim transaction - 'claim_id' : (str) claim ID of the resulting claim - } - - """ - self.ledger.is_valid_address(address) - return self.wallet_manager.send_claim_to_address( - claim_id, address, self.get_dewies_or_error("amount", amount) if amount else None - ) - @requires(WALLET_COMPONENT) - def jsonrpc_claim_list_mine(self, account_id=None, page=None, page_size=None): + def jsonrpc_claim_list(self, account_id=None, page=None, page_size=None): """ List my name claims @@ -2258,28 +2212,6 @@ class Daemon(metaclass=JSONRPCServerType): --account_id=<account_id> : (str) id of the account to query --page=<page> : (int) page to return during paginating --page_size=<page_size> : (int) number of items on page during pagination - - Returns: - (list) List of name claims owned by user - [ - { - 'address': (str) address that owns the claim - 'amount': (float) amount assigned to the claim - 'blocks_to_expiration': (int) number of blocks until it expires - 'category': (str) "claim", "update" , or "support" - 'claim_id': (str) claim ID of the claim - 'confirmations': (int) number of blocks of confirmations for the claim - 'expiration_height': (int) the block height which the claim will expire - 'expired': (bool) true if expired, false otherwise - 'height': (int) height of the block containing the claim - 'is_spent': (bool) true if claim is abandoned, false otherwise - 'name': (str) name of the claim - 'permanent_url': (str) permanent url of the claim, - 'txid': (str) txid of the claim - 'nout': (int) nout of the claim - 'value': (str) value of the claim - }, - ] """ account = self.get_account_or_default(account_id) return maybe_paginate( @@ -2289,94 +2221,23 @@ class Daemon(metaclass=JSONRPCServerType): ) @requires(WALLET_COMPONENT) - async def jsonrpc_claim_list(self, name): + async def jsonrpc_claim_search(self, name, channel_id=None, winning=False): """ - List current claims and information about them for a given name + Search for claims on the blockchain. Usage: - claim_list (<name> | --name=<name>) + claim search (<name> | --name=<name>) [--channel_id=<channel_id>] [--winning] Options: - --name=<name> : (str) name of the claim to list info about - - Returns: - (dict) State of claims assigned for the name - { - 'claims': (list) list of claims for the name - [ - { - 'amount': (float) amount assigned to the claim - 'effective_amount': (float) total amount assigned to the claim, - including supports - 'claim_id': (str) claim ID of the claim - 'height': (int) height of block containing the claim - 'txid': (str) txid of the claim - 'nout': (int) nout of the claim - 'permanent_url': (str) permanent url of the claim, - 'supports': (list) a list of supports attached to the claim - 'value': (str) the value of the claim - }, - ] - 'supports_without_claims': (list) supports without any claims attached to them - 'last_takeover_height': (int) the height of last takeover for the name - } - """ - claims = await self.wallet_manager.get_claims_for_name(name) # type: dict - claims['claims'] = sort_claim_results(claims['claims']) - return claims - - @requires(WALLET_COMPONENT) - async def jsonrpc_claim_list_by_channel(self, page=0, page_size=10, uri=None, uris=[]): - """ - Get paginated claims in a channel specified by a channel uri - - Usage: - claim_list_by_channel (<uri> | --uri=<uri>) [<uris>...] [--page=<page>] - [--page_size=<page_size>] - - Options: - --uri=<uri> : (str) uri of the channel - --uris=<uris> : (list) uris of the channel - --page=<page> : (int) which page of results to return where page 1 is the first - page, defaults to no pages - --page_size=<page_size> : (int) number of results in a page, default of 10 - - Returns: - { - resolved channel uri: { - If there was an error: - 'error': (str) error message - - 'claims_in_channel': the total number of results for the channel, - - If a page of results was requested: - 'returned_page': page number returned, - 'claims_in_channel': [ - { - 'absolute_channel_position': (int) claim index number in sorted list of - claims which assert to be part of the - channel - 'address': (str) claim address, - 'amount': (float) claim amount, - 'effective_amount': (float) claim amount including supports, - 'claim_id': (str) claim id, - 'decoded_claim': (bool) whether or not the claim value was decoded, - 'height': (int) claim height, - 'depth': (int) claim depth, - 'has_signature': (bool) included if decoded_claim - 'name': (str) claim name, - 'supports: (list) list of supports [{'txid': (str) txid, - 'nout': (int) nout, - 'amount': (float) amount}], - 'txid': (str) claim txid, - 'nout': (str) claim nout, - 'signature_is_valid': (bool), included if has_signature, - 'value': ClaimDict if decoded, otherwise hex string - } - ], - } - } + --name=<name> : (str) name of the claim to list info about + --channel_id=<channel_id> : (str) limit search to specific channel + --winning : (bool) limit to winning claims """ + response = await self.wallet_manager.ledger.network.get_claims_for_name(name) + resolutions = await self.wallet_manager.resolve(*(f"{claim['name']}#{claim['claim_id']}" for claim in response['claims'])) + response['claims'] = [value.get('claim', value.get('certificate')) for value in resolutions.values()] + response['claims'] = sort_claim_results(response['claims']) + return response uris = tuple(uris) page = int(page) @@ -2414,6 +2275,108 @@ class Daemon(metaclass=JSONRPCServerType): results[u]['claims_in_channel'] = resolved[u].get('claims_in_channel', []) return results + SUPPORT_DOC = """ + Support and tip management. + """ + + @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) + async def jsonrpc_support_create(self, claim_id, amount, tip=False, account_id=None): + """ + Create a support or a tip for name claim. + + Usage: + support create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>) + [--tip] [--account_id=<account_id>] + + Options: + --claim_id=<claim_id> : (str) claim_id of the claim to support + --amount=<amount> : (decimal) amount of support + --tip : (bool) send support to claim owner, default: false. + --account_id=<account_id> : (str) id of the account to use + + Returns: + (dict) Dictionary containing the transaction information + { + "hex": (str) raw transaction, + "inputs": (list) inputs(dict) used for the transaction, + "outputs": (list) outputs(dict) for the transaction, + "total_fee": (int) fee in dewies, + "total_input": (int) total of inputs in dewies, + "total_output": (int) total of outputs in dewies(input - fees), + "txid": (str) txid of the transaction, + } + """ + account = self.get_account_or_default(account_id) + amount = self.get_dewies_or_error("amount", amount) + result = await self.wallet_manager.support_claim(name, claim_id, amount, account) + await self.analytics_manager.send_claim_action('new_support') + # tip: + validate_claim_id(claim_id) + result = await self.wallet_manager.tip_claim(amount, claim_id, account) + await self.analytics_manager.send_claim_action('new_support') + return result + + @requires(WALLET_COMPONENT, conditions=[WALLET_IS_UNLOCKED]) + async def jsonrpc_support_abandon(self, claim_id=None, txid=None, nout=None, account_id=None, blocking=True): + """ + Abandon a name and reclaim credits from the claim + + Usage: + claim_abandon [<claim_id> | --claim_id=<claim_id>] + [<txid> | --txid=<txid>] [<nout> | --nout=<nout>] + [--account_id=<account_id>] + [--blocking] + + Options: + --claim_id=<claim_id> : (str) claim_id of the claim to abandon + --txid=<txid> : (str) txid of the claim to abandon + --nout=<nout> : (int) nout of the claim to abandon + --account_id=<account_id> : (str) id of the account to use + --blocking : (bool) wait until abandon is in mempool + + Returns: + (dict) Dictionary containing result of the claim + { + success: (bool) True if txn is successful + txid : (str) txid of resulting transaction + } + """ + account = self.get_account_or_default(account_id) + + if claim_id is None and txid is None and nout is None: + raise Exception('Must specify claim_id, or txid and nout') + if txid is None and nout is not None: + raise Exception('Must specify txid') + if nout is None and txid is not None: + raise Exception('Must specify nout') + + tx = await self.wallet_manager.abandon_claim(claim_id, txid, nout, account) + await self.analytics_manager.send_claim_action('abandon') + if blocking: + await self.ledger.wait(tx) + return {"success": True, "tx": tx} + + @requires(WALLET_COMPONENT) + def jsonrpc_support_list(self, account_id=None, page=None, page_size=None): + """ + List supports and tips. + + Usage: + support_list [<account_id> | --account_id=<account_id>] + [--page=<page>] [--page_size=<page_size>] + + Options: + --account_id=<account_id> : (str) id of the account to query + --page=<page> : (int) page to return during paginating + --page_size=<page_size> : (int) number of items on page during pagination + """ + account = self.get_account_or_default(account_id) + return maybe_paginate( + account.get_supports, + account.get_support_count, + page, page_size + ) + TRANSACTION_DOC = """ Transaction management. """ @@ -2908,34 +2871,72 @@ class Daemon(metaclass=JSONRPCServerType): result['node_id'] = hexlify(self.dht_node.protocol.node_id).decode() return result - 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=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=accounts, claim_name=channel_name) - if not certificates: - raise ValueError(f"Couldn't find channel with name '{channel_name}'.") - return certificates[0] - raise ValueError("Couldn't find channel because a channel name or channel_id was not provided.") + def get_fee_address(self, kwargs: dict, claim_address: str) -> str: + if 'fee_address' in kwargs: + self.ledger.is_valid_address(kwargs['fee_address']) + return kwargs['fee_address'] + return claim_address - def get_account_or_default(self, account_id: str, argument_name: str = "account", lbc_only=True): + async def get_receiving_address(self, address: str, account: LBCAccount) -> str: + if address is None: + return await account.receiving.get_or_create_usable_address() + self.ledger.is_valid_address(address) + return address + + @staticmethod + def get_claim_name_or_error(name: str) -> str: + try: + parsed = parse_lbry_uri(name) + if parsed.name != name: + raise Exception("Claim name given has invalid characters.") + if parsed.is_channel: + raise Exception("Claim names cannot start with @ symbol. This is reserved for channels.") + except (TypeError, URIParseError): + raise Exception("Invalid claim name given.") + return name + + @staticmethod + def get_channel_name_or_error(channel_name: str) -> str: + try: + parsed = parse_lbry_uri(channel_name) + if not parsed.contains_channel: + raise Exception("Cannot make a new channel for a non channel name") + if parsed.path: + raise Exception("Invalid channel uri") + except (TypeError, URIParseError): + raise Exception("Invalid channel name") + return channel_name + + async def get_channel_or_none(self, account_ids: List[str], channel_id: str = None, + for_signing: bool = False) -> Output: + if channel_id is not None: + return await self.get_channel_or_error(account_ids, channel_id, for_signing) + + async def get_channel_or_error(self, account_ids: List[str], channel_id: str = None, + for_signing: bool = False) -> Output: + if channel_id is None: + raise ValueError("Couldn't find channel because a channel_id was not provided.") + for account in self.get_accounts_or_all(account_ids): + channels = await account.get_channels(claim_id=channel_id, limit=1) + if channels: + if for_signing and channels[0].private_key is None: + raise Exception(f"Couldn't find private key for channel '{channel_id}'. ") + return channels[0] + raise ValueError(f"Couldn't find channel with channel_id '{channel_id}'.") + + def get_account_or_default(self, account_id: str, argument_name: str = "account", lbc_only=True) -> LBCAccount: if account_id is None: 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]): + def get_accounts_or_all(self, account_ids: List[str]) -> List[LBCAccount]: 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): + def get_account_or_error( + self, account_id: str, argument_name: str = "account", lbc_only=True) -> Optional[LBCAccount]: for account in self.default_wallet.accounts: if account.id == account_id: if lbc_only and not isinstance(account, LBCAccount): @@ -2948,11 +2949,27 @@ class Daemon(metaclass=JSONRPCServerType): raise ValueError(f"Couldn't find account: {account_id}.") @staticmethod - def get_dewies_or_error(argument: str, lbc: str): + def get_dewies_or_error(argument: str, lbc: str, positive_value=False): try: - return lbc_to_dewies(lbc) + dewies = lbc_to_dewies(lbc) + if positive_value and dewies <= 0: + raise ValueError(f"'{argument}' value must be greater than 0.0") + return dewies except ValueError as e: - raise ValueError("Invalid value for '{}': {}".format(argument, e.args[0])) + raise ValueError(f"Invalid value for '{argument}': {e.args[0]}") + + def _old_get_temp_claim_info(self, tx, txo, address, claim_dict, name, bid): + return { + "claim_id": txo.claim_id, + "name": name, + "amount": bid, + "address": address, + "txid": tx.id, + "nout": txo.position, + "value": claim_dict, + "height": -1, + "claim_sequence": -1, + } def loggly_time_string(dt): diff --git a/lbrynet/schema/claim.py b/lbrynet/schema/claim.py index 3cf5ca1a2..5a1fab7d8 100644 --- a/lbrynet/schema/claim.py +++ b/lbrynet/schema/claim.py @@ -1,9 +1,13 @@ +import os.path from typing import List, Tuple from decimal import Decimal from binascii import hexlify, unhexlify from google.protobuf.json_format import MessageToDict from google.protobuf.message import DecodeError +from hachoir.parser import createParser as binary_file_parser +from hachoir.metadata import extractMetadata as binary_file_metadata +from hachoir.core.log import log as hachoir_log from torba.client.hash import Base58 from torba.client.constants import COIN @@ -11,6 +15,10 @@ from torba.client.constants import COIN from lbrynet.schema.types.v2.claim_pb2 import Claim as ClaimMessage, Fee as FeeMessage from lbrynet.schema import compat from lbrynet.schema.base import Signable +from lbrynet.schema.mime_types import guess_media_type + + +hachoir_log.use_print = False class Claim(Signable): @@ -54,6 +62,9 @@ class Claim(Signable): def channel(self) -> 'Channel': return Channel(self) + def to_dict(self): + return MessageToDict(self.message, preserving_proto_field_name=True) + @classmethod def from_bytes(cls, data: bytes) -> 'Claim': try: @@ -208,225 +219,243 @@ class Fee: self._fee.currency = FeeMessage.USD -class Channel: +class BaseClaimSubType: - __slots__ = '_claim', '_channel' + __slots__ = 'claim', 'message' - def __init__(self, claim: Claim = None): - self._claim = claim or Claim() - self._channel = self._claim.channel_message - - def to_dict(self): - return MessageToDict(self._channel) + def __init__(self, claim: Claim): + self.claim = claim or Claim() @property - def claim(self) -> Claim: - return self._claim + def title(self) -> str: + return self.message.title + + @title.setter + def title(self, title: str): + self.message.title = title + + @property + def description(self) -> str: + return self.message.description + + @description.setter + def description(self, description: str): + self.message.description = description + + @property + def language(self) -> str: + return self.message.language + + @language.setter + def language(self, language: str): + self.message.language = language + + @property + def thumbnail_url(self) -> str: + return self.message.thumbnail_url + + @thumbnail_url.setter + def thumbnail_url(self, thumbnail_url: str): + self.message.thumbnail_url = thumbnail_url @property def tags(self) -> List: - return self._channel.tags + return self.message.tags + + def to_dict(self): + return MessageToDict(self.message, preserving_proto_field_name=True) + + def update(self, tags=None, clear_tags=False, **kwargs): + + if clear_tags: + self.message.ClearField('tags') + + if tags is not None: + if isinstance(tags, str): + self.tags.append(tags) + elif isinstance(tags, list): + self.tags.extend(tags) + else: + raise ValueError(f"Unknown tag type: {tags}") + + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Channel(BaseClaimSubType): + + __slots__ = () + + def __init__(self, claim: Claim = None): + super().__init__(claim) + self.message = self.claim.channel_message @property def public_key(self) -> str: - return hexlify(self._channel.public_key).decode() + return hexlify(self.message.public_key).decode() @public_key.setter def public_key(self, sd_public_key: str): - self._channel.public_key = unhexlify(sd_public_key.encode()) + self.message.public_key = unhexlify(sd_public_key.encode()) @property def public_key_bytes(self) -> bytes: - return self._channel.public_key + return self.message.public_key @public_key_bytes.setter def public_key_bytes(self, public_key: bytes): - self._channel.public_key = public_key - - @property - def language(self) -> str: - return self._channel.language - - @language.setter - def language(self, language: str): - self._channel.language = language - - @property - def title(self) -> str: - return self._channel.title - - @title.setter - def title(self, title: str): - self._channel.title = title - - @property - def description(self) -> str: - return self._channel.description - - @description.setter - def description(self, description: str): - self._channel.description = description + self.message.public_key = public_key @property def contact_email(self) -> str: - return self._channel.contact_email + return self.message.contact_email @contact_email.setter def contact_email(self, contact_email: str): - self._channel.contact_email = contact_email + self.message.contact_email = contact_email @property def homepage_url(self) -> str: - return self._channel.homepage_url + return self.message.homepage_url @homepage_url.setter def homepage_url(self, homepage_url: str): - self._channel.homepage_url = homepage_url - - @property - def thumbnail_url(self) -> str: - return self._channel.thumbnail_url - - @thumbnail_url.setter - def thumbnail_url(self, thumbnail_url: str): - self._channel.thumbnail_url = thumbnail_url + self.message.homepage_url = homepage_url @property def cover_url(self) -> str: - return self._channel.cover_url + return self.message.cover_url @cover_url.setter def cover_url(self, cover_url: str): - self._channel.cover_url = cover_url + self.message.cover_url = cover_url -class Stream: +class Stream(BaseClaimSubType): - __slots__ = '_claim', '_stream' + __slots__ = () def __init__(self, claim: Claim = None): - self._claim = claim or Claim() - self._stream = self._claim.stream_message + super().__init__(claim) + self.message = self.claim.stream_message - def to_dict(self): - return MessageToDict(self._stream) + def update( + self, file_path=None, duration=None, + fee_currency=None, fee_amount=None, fee_address=None, + video_height=None, video_width=None, + **kwargs): - @property - def claim(self) -> Claim: - return self._claim + super().update(**kwargs) + + if video_height is not None: + self.video.height = video_height + + if video_width is not None: + self.video.width = video_width + + if file_path is not None: + self.media_type = guess_media_type(file_path) + if not os.path.isfile(file_path): + raise Exception(f"File does not exist: {file_path}") + self.file.size = os.path.getsize(file_path) + if self.file.size == 0: + raise Exception(f"Cannot publish empty file: {file_path}") + + if fee_amount and fee_currency: + if fee_address: + self.fee.address = fee_address + if fee_currency.lower() == 'lbc': + self.fee.lbc = Decimal(fee_amount) + elif fee_currency.lower() == 'usd': + self.fee.usd = Decimal(fee_amount) + else: + raise Exception(f'Unknown currency type: {fee_currency}') + + if duration is not None: + self.duration = duration + elif file_path is not None: + try: + file_metadata = binary_file_metadata(binary_file_parser(file_path)) + self.duration = file_metadata.getValues('duration')[0].seconds + except: + pass @property def video(self) -> Video: - return Video(self._stream.video) + return Video(self.message.video) @property def file(self) -> File: - return File(self._stream.file) + return File(self.message.file) @property def fee(self) -> Fee: - return Fee(self._stream.fee) + return Fee(self.message.fee) @property def has_fee(self) -> bool: - return self._stream.HasField('fee') - - @property - def tags(self) -> List: - return self._stream.tags + return self.message.HasField('fee') @property def hash(self) -> str: - return hexlify(self._stream.hash).decode() + return hexlify(self.message.hash).decode() @hash.setter def hash(self, sd_hash: str): - self._stream.hash = unhexlify(sd_hash.encode()) + self.message.hash = unhexlify(sd_hash.encode()) @property def hash_bytes(self) -> bytes: - return self._stream.hash + return self.message.hash @hash_bytes.setter def hash_bytes(self, hash: bytes): - self._stream.hash = hash - - @property - def language(self) -> str: - return self._stream.language - - @language.setter - def language(self, language: str): - self._stream.language = language - - @property - def title(self) -> str: - return self._stream.title - - @title.setter - def title(self, title: str): - self._stream.title = title + self.message.hash = hash @property def author(self) -> str: - return self._stream.author + return self.message.author @author.setter def author(self, author: str): - self._stream.author = author - - @property - def description(self) -> str: - return self._stream.description - - @description.setter - def description(self, description: str): - self._stream.description = description + self.message.author = author @property def media_type(self) -> str: - return self._stream.media_type + return self.message.media_type @media_type.setter def media_type(self, media_type: str): - self._stream.media_type = media_type + self.message.media_type = media_type @property def license(self) -> str: - return self._stream.license + return self.message.license @license.setter def license(self, license: str): - self._stream.license = license + self.message.license = license @property def license_url(self) -> str: - return self._stream.license_url + return self.message.license_url @license_url.setter def license_url(self, license_url: str): - self._stream.license_url = license_url - - @property - def thumbnail_url(self) -> str: - return self._stream.thumbnail_url - - @thumbnail_url.setter - def thumbnail_url(self, thumbnail_url: str): - self._stream.thumbnail_url = thumbnail_url + self.message.license_url = license_url @property def duration(self) -> int: - return self._stream.duration + return self.message.duration @duration.setter def duration(self, duration: int): - self._stream.duration = duration + self.message.duration = duration @property def release_time(self) -> int: - return self._stream.release_time + return self.message.release_time @release_time.setter def release_time(self, release_time: int): - self._stream.release_time = release_time + self.message.release_time = release_time diff --git a/lbrynet/extras/daemon/mime_types.py b/lbrynet/schema/mime_types.py similarity index 100% rename from lbrynet/extras/daemon/mime_types.py rename to lbrynet/schema/mime_types.py diff --git a/lbrynet/stream/managed_stream.py b/lbrynet/stream/managed_stream.py index dc8740266..3a675a5ac 100644 --- a/lbrynet/stream/managed_stream.py +++ b/lbrynet/stream/managed_stream.py @@ -4,7 +4,7 @@ import typing import logging import binascii from lbrynet.utils import generate_id -from lbrynet.extras.daemon.mime_types import guess_media_type +from lbrynet.schema.mime_types import guess_media_type from lbrynet.stream.downloader import StreamDownloader from lbrynet.stream.descriptor import StreamDescriptor from lbrynet.stream.reflector.client import StreamReflectorClient diff --git a/lbrynet/wallet/account.py b/lbrynet/wallet/account.py index f2746657c..f7243e9a0 100644 --- a/lbrynet/wallet/account.py +++ b/lbrynet/wallet/account.py @@ -1,12 +1,16 @@ import json import logging import binascii +import typing from hashlib import sha256 from string import hexdigits from torba.client.baseaccount import BaseAccount from torba.client.basetransaction import TXORef +if typing.TYPE_CHECKING: + from lbrynet.wallet import ledger + log = logging.getLogger(__name__) @@ -21,31 +25,32 @@ def validate_claim_id(claim_id): class Account(BaseAccount): + ledger: 'ledger.MainNetLedger' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.certificates = {} + self.channel_keys = {} @property def hash(self) -> bytes: h = sha256(json.dumps(self.to_dict(False)).encode()) - for cert in sorted(self.certificates.keys()): + for cert in sorted(self.channel_keys.keys()): h.update(cert.encode()) return h.digest() def apply(self, d: dict): super().apply(d) - self.certificates.update(d.get('certificates', {})) + self.channel_keys.update(d.get('certificates', {})) - def add_certificate_private_key(self, ref: TXORef, private_key): - assert ref.id not in self.certificates, 'Trying to add a duplicate certificate.' - self.certificates[ref.id] = private_key + def add_channel_private_key(self, ref: TXORef, private_key): + assert ref.id not in self.channel_keys, 'Trying to add a duplicate channel private key.' + self.channel_keys[ref.id] = private_key - def get_certificate_private_key(self, ref: TXORef): - return self.certificates.get(ref.id) + def get_channel_private_key(self, ref: TXORef): + return self.channel_keys.get(ref.id) async def maybe_migrate_certificates(self): - if not self.certificates: + if not self.channel_keys: return addresses = {} @@ -59,7 +64,7 @@ class Account(BaseAccount): } double_hex_encoded_to_pop = [] - for maybe_claim_id in list(self.certificates): + for maybe_claim_id in list(self.channel_keys): if ':' not in maybe_claim_id: try: validate_claim_id(maybe_claim_id) @@ -71,7 +76,7 @@ class Account(BaseAccount): maybe_claim_id_bytes = maybe_claim_id_bytes.encode() decoded_double_hex = binascii.unhexlify(maybe_claim_id_bytes).decode() validate_claim_id(decoded_double_hex) - if decoded_double_hex in self.certificates: + if decoded_double_hex in self.channel_keys: log.warning("don't know how to migrate certificate %s", decoded_double_hex) else: log.info("claim id was double hex encoded, fixing it") @@ -80,9 +85,9 @@ class Account(BaseAccount): continue for double_encoded_claim_id, correct_claim_id in double_hex_encoded_to_pop: - self.certificates[correct_claim_id] = self.certificates.pop(double_encoded_claim_id) + self.channel_keys[correct_claim_id] = self.channel_keys.pop(double_encoded_claim_id) - for maybe_claim_id in list(self.certificates): + for maybe_claim_id in list(self.channel_keys): results['total'] += 1 if ':' not in maybe_claim_id: try: @@ -117,8 +122,8 @@ class Account(BaseAccount): .format(maybe_claim_id) ) tx_nout = '{txid}:{nout}'.format(**claim) - self.certificates[tx_nout] = self.certificates[maybe_claim_id] - del self.certificates[maybe_claim_id] + self.channel_keys[tx_nout] = self.channel_keys[maybe_claim_id] + del self.channel_keys[maybe_claim_id] log.info( "Migrated certificate with claim_id '%s' ('%s') to a new look up key %s.", maybe_claim_id, txo.script.values['claim_name'], tx_nout @@ -186,18 +191,18 @@ class Account(BaseAccount): @classmethod def from_dict(cls, ledger, wallet, d: dict) -> 'Account': account = super().from_dict(ledger, wallet, d) - account.certificates = d.get('certificates', {}) + account.channel_keys = d.get('certificates', {}) return account - def to_dict(self, include_certificates=True): + def to_dict(self, include_channel_keys=True): d = super().to_dict() - if include_certificates: - d['certificates'] = self.certificates + if include_channel_keys: + d['certificates'] = self.channel_keys return d async def get_details(self, **kwargs): details = await super().get_details(**kwargs) - details['certificates'] = len(self.certificates) + details['certificates'] = len(self.channel_keys) return details def get_claim(self, claim_id=None, txid=None, nout=None): @@ -207,15 +212,15 @@ class Account(BaseAccount): return self.ledger.db.get_claims(**{'account': self, 'txo.txid': txid, 'txo.position': nout}) @staticmethod - def constraint_utxos_sans_claims(constraints): + def constraint_spending_utxos(constraints): constraints.update({'is_claim': 0, 'is_update': 0, 'is_support': 0}) def get_utxos(self, **constraints): - self.constraint_utxos_sans_claims(constraints) + self.constraint_spending_utxos(constraints) return super().get_utxos(**constraints) def get_utxo_count(self, **constraints): - self.constraint_utxos_sans_claims(constraints) + self.constraint_spending_utxos(constraints) return super().get_utxo_count(**constraints) def get_claims(self, **constraints): @@ -230,6 +235,12 @@ class Account(BaseAccount): def get_channel_count(self, **constraints): return self.ledger.db.get_channel_count(account=self, **constraints) + def get_supports(self, **constraints): + return self.ledger.db.get_supports(account=self, **constraints) + + def get_support_count(self, **constraints): + return self.ledger.db.get_support_count(account=self, **constraints) + async def send_to_addresses(self, amount, addresses, broadcast=False): tx_class = self.ledger.transaction_class tx = await tx_class.create( diff --git a/lbrynet/wallet/database.py b/lbrynet/wallet/database.py index 7d77bcf78..9892d7556 100644 --- a/lbrynet/wallet/database.py +++ b/lbrynet/wallet/database.py @@ -1,5 +1,9 @@ +from typing import List + from torba.client.basedatabase import BaseDatabase +from lbrynet.wallet.transaction import Output + class WalletDatabase(BaseDatabase): @@ -48,7 +52,7 @@ class WalletDatabase(BaseDatabase): row['claim_name'] = txo.claim_name return row - async def get_txos(self, **constraints): + async def get_txos(self, **constraints) -> List[Output]: my_account = constraints.get('my_account', constraints.get('account', None)) txos = await super().get_txos(**constraints) @@ -58,8 +62,8 @@ class WalletDatabase(BaseDatabase): if txo.script.is_claim_name or txo.script.is_update_claim: if txo.claim.is_signed: channel_ids.add(txo.claim.signing_channel_id) - if txo.claim_name.startswith('@') and my_account is not None: - txo.private_key = my_account.get_certificate_private_key(txo.ref) + if txo.claim.is_channel and my_account is not None: + txo.private_key = my_account.get_channel_private_key(txo.ref) if channel_ids: channels = { @@ -77,11 +81,11 @@ class WalletDatabase(BaseDatabase): @staticmethod def constrain_claims(constraints): - constraints['claim_type__any'] = {'is_claim': 1, 'is_update': 1, 'is_support': 1} + constraints['claim_type__any'] = {'is_claim': 1, 'is_update': 1} - def get_claims(self, **constraints): + async def get_claims(self, **constraints) -> List[Output]: self.constrain_claims(constraints) - return self.get_utxos(**constraints) + return await self.get_utxos(**constraints) def get_claim_count(self, **constraints): self.constrain_claims(constraints) @@ -100,22 +104,17 @@ class WalletDatabase(BaseDatabase): self.constrain_channels(constraints) return self.get_claim_count(**constraints) - async def get_certificates(self, private_key_accounts, exclude_without_key=False, **constraints): - channels = await self.get_channels(**constraints) - certificates = [] - if private_key_accounts is not None: - for channel in channels: - if not channel.has_private_key: - private_key = None - for account in private_key_accounts: - private_key = account.get_certificate_private_key(channel.ref) - if private_key is not None: - break - if private_key is None and exclude_without_key: - continue - channel.private_key = private_key - certificates.append(channel) - return certificates + @staticmethod + def constrain_supports(constraints): + constraints['is_support'] = 1 + + def get_supports(self, **constraints): + self.constrain_supports(constraints) + return self.get_utxos(**constraints) + + def get_support_count(self, **constraints): + self.constrain_supports(constraints) + return self.get_utxo_count(**constraints) async def release_all_outputs(self, account): await self.db.execute( diff --git a/lbrynet/wallet/ledger.py b/lbrynet/wallet/ledger.py index a39e15233..fe6180150 100644 --- a/lbrynet/wallet/ledger.py +++ b/lbrynet/wallet/ledger.py @@ -29,6 +29,8 @@ class MainNetLedger(BaseLedger): network_class = Network transaction_class = Transaction + db: WalletDatabase + secret_prefix = bytes((0x1c,)) pubkey_address_prefix = bytes((0x55,)) script_address_prefix = bytes((0x7a,)) @@ -97,7 +99,7 @@ class MainNetLedger(BaseLedger): log.info("Loaded account %s with %s LBC, %d receiving addresses (gap: %d), " "%d change addresses (gap: %d), %d channels, %d certificates and %d claims. ", account.id, balance, total_receiving, account.receiving.gap, total_change, account.change.gap, - channel_count, len(account.certificates), claim_count) + channel_count, len(account.channel_keys), claim_count) class TestNetLedger(MainNetLedger): diff --git a/lbrynet/wallet/manager.py b/lbrynet/wallet/manager.py index 1a19f750c..a7b3bbdfe 100644 --- a/lbrynet/wallet/manager.py +++ b/lbrynet/wallet/manager.py @@ -155,7 +155,7 @@ class LbryWalletManager(BaseWalletManager): return receiving_addresses, change_addresses @classmethod - async def from_lbrynet_config(cls, settings, db): + async def from_lbrynet_config(cls, settings): ledger_id = { 'lbrycrd_main': 'lbc_mainnet', @@ -233,24 +233,6 @@ class LbryWalletManager(BaseWalletManager): await account.ledger.broadcast(tx) return tx - async def send_claim_to_address(self, claim_id: str, destination_address: str, amount: Optional[int], - account=None): - account = account or self.default_account - claims = await account.get_claims( - claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports - claim_id=claim_id - ) - if not claims: - raise NameError(f"Claim not found: {claim_id}") - if not amount: - amount = claims[0].get_estimator(self.ledger).effective_amount - tx = await Transaction.update( - claims[0], claims[0].claim, amount, - destination_address.encode(), [account], account - ) - await self.ledger.broadcast(tx) - return tx - def send_points_to_address(self, reserved: ReservedPoints, amount: int, account=None): destination_address: bytes = reserved.identifier.encode('latin1') return self.send_amount_to_address(amount, destination_address, account) @@ -392,44 +374,6 @@ class LbryWalletManager(BaseWalletManager): def get_utxos(account: BaseAccount): return account.get_utxos() - async def claim_name(self, account, name, amount, claim: Claim, certificate=None, claim_address=None): - claim_address = claim_address or await account.receiving.get_or_create_usable_address() - existing_claims = await account.get_claims( - claim_name_type__any={'is_claim': 1, 'is_update': 1}, # exclude is_supports - claim_name=name - ) - - inputs = [] - if len(existing_claims) == 0: - claim_output = Output.pay_claim_name_pubkey_hash( - amount, name, claim, account.ledger.address_to_hash160(claim_address) - ) - elif len(existing_claims) == 1: - previous_claim = existing_claims[0] - claim_output = Output.pay_update_claim_pubkey_hash( - amount, previous_claim.claim_name, previous_claim.claim_id, - claim, account.ledger.address_to_hash160(claim_address) - ) - inputs = [Input.spend(previous_claim)] - else: - raise NameError(f"More than one other claim exists with the name '{name}'.") - - if certificate: - claim_output.sign(certificate, first_input_id=b'placeholder') - - tx = await Transaction.create(inputs, [claim_output], [account], account) - - if certificate: - claim_output.sign(certificate) - await tx.sign([account]) - - await account.ledger.broadcast(tx) - await self.old_db.save_claims([self._old_get_temp_claim_info( - tx, tx.outputs[0], claim_address, claim, name, dewies_to_lbc(amount) - )]) - # TODO: release reserved tx outputs in case anything fails by this point - return tx - async def support_claim(self, claim_name, claim_id, amount, account): holding_address = await account.receiving.get_or_create_usable_address() tx = await Transaction.support(claim_name, claim_id, amount, holding_address, [account], account) @@ -468,46 +412,6 @@ 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): - address = await account.receiving.get_or_create_usable_address() - claim = Claim() - claim_output = Output.pay_claim_name_pubkey_hash( - amount, channel_name, claim, account.ledger.address_to_hash160(address) - ) - key = claim_output.generate_channel_private_key() - claim_output.script.generate() - tx = await Transaction.create([], [claim_output], [account], account) - - - await account.ledger.broadcast(tx) - account.add_certificate_private_key(tx.outputs[0].ref, key.decode()) - # TODO: release reserved tx outputs in case anything fails by this point - - await self.old_db.save_claims([self._old_get_temp_claim_info( - tx, tx.outputs[0], address, claim, channel_name, dewies_to_lbc(amount) - )]) - return tx - - def _old_get_temp_claim_info(self, tx, txo, address, claim_dict, name, bid): - return { - "claim_id": txo.claim_id, - "name": name, - "amount": bid, - "address": address, - "txid": tx.id, - "nout": txo.position, - "value": claim_dict, - "height": -1, - "claim_sequence": -1, - } - - def get_certificates(self, private_key_accounts, exclude_without_key=True, **constraints): - return self.db.get_certificates( - private_key_accounts=private_key_accounts, - exclude_without_key=exclude_without_key, - **constraints - ) - def update_peer_address(self, peer, address): pass # TODO: Data payments is disabled diff --git a/lbrynet/wallet/transaction.py b/lbrynet/wallet/transaction.py index 202fdb91a..1bf2b868a 100644 --- a/lbrynet/wallet/transaction.py +++ b/lbrynet/wallet/transaction.py @@ -11,7 +11,7 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from ecdsa.util import sigencode_der -from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput +from torba.client.basetransaction import BaseTransaction, BaseInput, BaseOutput, ReadOnlyList from torba.client.hash import hash160, sha256, Base58 from lbrynet.schema.claim import Claim from lbrynet.wallet.account import Account @@ -117,6 +117,7 @@ class Output(BaseOutput): return True def sign(self, channel: 'Output', first_input_id=None): + self.channel = channel self.claim.signing_channel_hash = channel.claim_hash digest = sha256(b''.join([ first_input_id or self.tx_ref.tx.inputs[0].txo_ref.id.encode(), @@ -129,8 +130,9 @@ class Output(BaseOutput): def generate_channel_private_key(self): private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256) - self.private_key = private_key.to_pem() + self.private_key = private_key.to_pem().decode() self.claim.channel.public_key_bytes = private_key.get_verifying_key().to_der() + self.script.generate() return self.private_key def is_channel_private_key(self, private_key_pem): @@ -169,6 +171,9 @@ class Transaction(BaseTransaction): input_class = Input output_class = Output + outputs: ReadOnlyList[Output] + inputs: ReadOnlyList[Input] + @classmethod def pay(cls, amount: int, address: bytes, funding_accounts: List[Account], change_account: Account): ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) @@ -176,42 +181,52 @@ class Transaction(BaseTransaction): return cls.create([], [output], funding_accounts, change_account) @classmethod - def claim(cls, name: str, claim: Claim, amount: int, holding_address: bytes, - funding_accounts: List[Account], change_account: Account): + def claim_create( + cls, name: str, claim: Claim, amount: int, holding_address: str, + funding_accounts: List[Account], change_account: Account, signing_channel: Output = None): ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) claim_output = Output.pay_claim_name_pubkey_hash( amount, name, claim, ledger.address_to_hash160(holding_address) ) - return cls.create([], [claim_output], funding_accounts, change_account) + if signing_channel is not None: + claim_output.sign(signing_channel, b'placeholder txid:nout') + return cls.create([], [claim_output], funding_accounts, change_account, sign=False) + + @classmethod + def claim_update( + cls, previous_claim: Output, amount: int, holding_address: str, + funding_accounts: List[Account], change_account: Account, signing_channel: Output = None): + ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) + updated_claim = Output.pay_update_claim_pubkey_hash( + amount, previous_claim.claim_name, previous_claim.claim_id, + previous_claim.claim, ledger.address_to_hash160(holding_address) + ) + if signing_channel is not None: + updated_claim.sign(signing_channel, b'placeholder txid:nout') + return cls.create( + [Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account, sign=False + ) + + @classmethod + def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: str, + funding_accounts: List[Account], change_account: Account, signing_channel: Output = None): + ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) + support_output = Output.pay_support_pubkey_hash( + amount, claim_name, claim_id, ledger.address_to_hash160(holding_address) + ) + if signing_channel is not None: + support_output.sign(signing_channel, b'placeholder txid:nout') + return cls.create([], [support_output], funding_accounts, change_account, sign=False) @classmethod def purchase(cls, claim: Output, amount: int, merchant_address: bytes, - funding_accounts: List[Account], change_account: Account): + funding_accounts: List[Account], change_account: Account): ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) claim_output = Output.purchase_claim_pubkey_hash( amount, claim.claim_id, ledger.address_to_hash160(merchant_address) ) return cls.create([], [claim_output], funding_accounts, change_account) - @classmethod - def update(cls, previous_claim: Output, claim: Claim, amount: int, holding_address: bytes, - funding_accounts: List[Account], change_account: Account): - ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) - updated_claim = Output.pay_update_claim_pubkey_hash( - amount, previous_claim.claim_name, previous_claim.claim_id, - claim, ledger.address_to_hash160(holding_address) - ) - return cls.create([Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account) - - @classmethod - def support(cls, claim_name: str, claim_id: str, amount: int, holding_address: bytes, - funding_accounts: List[Account], change_account: Account): - ledger = cls.ensure_all_have_same_ledger(funding_accounts, change_account) - output = Output.pay_support_pubkey_hash( - amount, claim_name, claim_id, ledger.address_to_hash160(holding_address) - ) - return cls.create([], [output], funding_accounts, change_account) - @classmethod def abandon(cls, claims: Iterable[Output], funding_accounts: Iterable[Account], change_account: Account): return cls.create([Input.spend(txo) for txo in claims], [], funding_accounts, change_account) diff --git a/setup.py b/setup.py index d581c58bf..b7108a488 100644 --- a/setup.py +++ b/setup.py @@ -38,5 +38,6 @@ setup( 'torba', 'pyyaml==3.13', 'docopt==0.6.2', + 'hachoir', ], ) diff --git a/tests/integration/test_chris45.py b/tests/integration/test_chris45.py index dd4c9fe3d..995b962a6 100644 --- a/tests/integration/test_chris45.py +++ b/tests/integration/test_chris45.py @@ -19,9 +19,8 @@ class EpicAdventuresOfChris45(CommandTestCase): # While making the spamdwich he wonders... has anyone on LBRY # registered the @spam channel yet? "I should do that!" he # exclaims and goes back to his computer to do just that! - channel = await self.out(self.daemon.jsonrpc_channel_new('@spam', "1.0")) - self.assertTrue(channel['success']) - await self.confirm_tx(channel['tx']['txid']) + tx = await self.create_channel('@spam', '1.0') + channel_id = tx['outputs'][0]['claim_id'] # Do we have it locally? channels = await self.out(self.daemon.jsonrpc_channel_list()) @@ -52,17 +51,12 @@ class EpicAdventuresOfChris45(CommandTestCase): # And so, many hours later, Chris is finished writing his epic story # about eels driving a hovercraft across the wetlands while eating spam # and decides it's time to publish it to the @spam channel. - with tempfile.NamedTemporaryFile() as file: - file.write(b'blah blah blah...') - file.write(b'[insert long story about eels driving hovercraft]') - file.write(b'yada yada yada!') - file.write(b'the end') - file.flush() - claim1 = await self.out(self.daemon.jsonrpc_publish( - 'hovercraft', '1.0', file_path=file.name, channel_id=channel['claim_id'] - )) - self.assertTrue(claim1['success']) - await self.confirm_tx(claim1['tx']['txid']) + tx = await self.create_claim( + 'hovercraft', '1.0', + data=b'[insert long story about eels driving hovercraft]', + channel_id=channel_id + ) + claim_id = tx['outputs'][0]['claim_id'] # He quickly checks the unconfirmed balance to make sure everything looks # correct. @@ -84,21 +78,11 @@ class EpicAdventuresOfChris45(CommandTestCase): # As people start reading his story they discover some typos and notify # Chris who explains in despair "Oh! Noooooos!" but then remembers # "No big deal! I can update my claim." And so he updates his claim. - with tempfile.NamedTemporaryFile() as file: - file.write(b'blah blah blah...') - file.write(b'[typo fixing sounds being made]') - file.write(b'yada yada yada!') - file.flush() - claim2 = await self.out(self.daemon.jsonrpc_publish( - 'hovercraft', '1.0', file_path=file.name, channel_name='@spam' - )) - self.assertTrue(claim2['success']) - self.assertEqual(claim2['claim_id'], claim1['claim_id']) - await self.confirm_tx(claim2['tx']['txid']) + await self.update_claim(claim_id, data=b'[typo fixing sounds being made]') # After some soul searching Chris decides that his story needs more # heart and a better ending. He takes down the story and begins the rewrite. - abandon = await self.out(self.daemon.jsonrpc_claim_abandon(claim1['claim_id'], blocking=False)) + abandon = await self.out(self.daemon.jsonrpc_claim_abandon(claim_id, blocking=False)) self.assertTrue(abandon['success']) await self.confirm_tx(abandon['tx']['txid']) @@ -134,17 +118,10 @@ class EpicAdventuresOfChris45(CommandTestCase): # After Chris is done with all the "helping other people" stuff he decides that it's time to # write a new story and publish it to lbry. All he needed was a fresh start and he came up with: - with tempfile.NamedTemporaryFile() as file: - file.write(b'Amazingly Original First Line') - file.write(b'Super plot for the grand novel') - file.write(b'Totally un-cliched ending') - file.write(b'**Audience Gasps**') - file.flush() - claim3 = await self.out(self.daemon.jsonrpc_publish( - 'fresh-start', '1.0', file_path=file.name, channel_name='@spam' - )) - self.assertTrue(claim3['success']) - await self.confirm_tx(claim3['tx']['txid']) + tx = await self.create_claim( + 'fresh-start', '1.0', data=b'Amazingly Original First Line', channel_id=channel_id + ) + claim_id2 = tx['outputs'][0]['claim_id'] await self.generate(5) @@ -154,7 +131,7 @@ class EpicAdventuresOfChris45(CommandTestCase): # And voila, and bravo and encore! His Best Friend Ramsey read the story and immediately knew this was a hit # Now to keep this claim winning on the lbry blockchain he immediately supports the claim tx = await self.out(self.daemon.jsonrpc_claim_new_support( - 'fresh-start', claim3['claim_id'], '0.2', account_id=ramsey_account_id + 'fresh-start', claim_id2, '0.2', account_id=ramsey_account_id )) await self.confirm_tx(tx['txid']) @@ -170,7 +147,7 @@ class EpicAdventuresOfChris45(CommandTestCase): # Now he also wanted to support the original creator of the Award Winning Novel # So he quickly decides to send a tip to him tx = await self.out( - self.daemon.jsonrpc_claim_tip(claim3['claim_id'], '0.3', account_id=ramsey_account_id)) + self.daemon.jsonrpc_claim_tip(claim_id2, '0.3', account_id=ramsey_account_id)) await self.confirm_tx(tx['txid']) # And again checks if it went to the just right place @@ -181,7 +158,7 @@ class EpicAdventuresOfChris45(CommandTestCase): await self.generate(5) # Seeing the ravishing success of his novel Chris adds support to his claim too - tx = await self.out(self.daemon.jsonrpc_claim_new_support('fresh-start', claim3['claim_id'], '0.4')) + tx = await self.out(self.daemon.jsonrpc_claim_new_support('fresh-start', claim_id2, '0.4')) await self.confirm_tx(tx['txid']) # And check if his support showed up @@ -197,16 +174,9 @@ class EpicAdventuresOfChris45(CommandTestCase): # his song, seeing as his novel had smashed all the records, he was the perfect candidate! # ....... # Chris agrees.. 17 hours 43 minutes and 14 seconds later, he makes his publish - with tempfile.NamedTemporaryFile() as file: - file.write(b'The Whale amd The Bookmark') - file.write(b'I know right? Totally a hit song') - file.write(b'That\'s what goes around for songs these days anyways') - file.flush() - claim4 = await self.out(self.daemon.jsonrpc_publish( - 'hit-song', '1.0', file_path=file.name, channel_id=channel['claim_id'] - )) - self.assertTrue(claim4['success']) - await self.confirm_tx(claim4['tx']['txid']) + tx = await self.out(self.daemon.jsonrpc_publish( + 'hit-song', '1.0', data=b'The Whale and The Bookmark', channel_id=channel_id + )) await self.generate(5) @@ -215,7 +185,7 @@ class EpicAdventuresOfChris45(CommandTestCase): # But sadly Ramsey wasn't so pleased. It was hard for him to tell Chris... # Chris, though a bit heartbroken, abandoned the claim for now, but instantly started working on new hit lyrics - abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=claim4['tx']['txid'], nout=0, blocking=False)) + abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=tx['txid'], nout=0, blocking=False)) self.assertTrue(abandon['success']) await self.confirm_tx(abandon['tx']['txid']) diff --git a/tests/integration/test_claim_commands.py b/tests/integration/test_claim_commands.py index d61d170d7..d6337e665 100644 --- a/tests/integration/test_claim_commands.py +++ b/tests/integration/test_claim_commands.py @@ -1,79 +1,224 @@ import hashlib -import tempfile from binascii import unhexlify import ecdsa from lbrynet.wallet.transaction import Transaction, Output -from lbrynet.error import InsufficientFundsError -from lbrynet.schema.claim import Claim +from torba.client.errors import InsufficientFundsError from lbrynet.schema.compat import OldClaimMessage from integration.testcase import CommandTestCase from torba.client.hash import sha256, Base58 +class ChannelCommands(CommandTestCase): + + async def test_create_channel_names(self): + # claim new name + await self.create_channel('@foo') + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1) + await self.assertBalance(self.account, '8.991893') + + # fail to claim duplicate + with self.assertRaisesRegex(Exception, "You already have a channel under the name '@foo'."): + await self.create_channel('@foo') + + # fail to claim invalid name + with self.assertRaisesRegex(Exception, "Cannot make a new channel for a non channel name"): + await self.create_channel('foo') + + # nothing's changed after failed attempts + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1) + await self.assertBalance(self.account, '8.991893') + + # succeed overriding duplicate restriction + await self.create_channel('@foo', allow_duplicate_name=True) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2) + await self.assertBalance(self.account, '7.983786') + + async def test_channel_bids(self): + # enough funds + tx = await self.create_channel('@foo', '5.0') + claim_id = tx['outputs'][0]['claim_id'] + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1) + await self.assertBalance(self.account, '4.991893') + + # bid preserved on update + tx = await self.update_channel(claim_id) + self.assertEqual(tx['outputs'][0]['amount'], '5.0') + + # bid changed on update + tx = await self.update_channel(claim_id, bid='4.0') + self.assertEqual(tx['outputs'][0]['amount'], '4.0') + + await self.assertBalance(self.account, '5.991447') + + # not enough funds + with self.assertRaisesRegex( + InsufficientFundsError, "Not enough funds to cover this transaction."): + await self.create_channel('@foo2', '9.0') + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1) + await self.assertBalance(self.account, '5.991447') + + # spend exactly amount available, no change + tx = await self.create_channel('@foo3', '5.981266') + await self.assertBalance(self.account, '0.0') + self.assertEqual(len(tx['outputs']), 1) # no change + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2) + + async def test_setting_channel_fields(self): + values = { + 'title': "Cool Channel", + 'description': "Best channel on LBRY.", + 'contact_email': "human@email.com", + 'tags': ["cool", "awesome"], + 'cover_url': "https://co.ol/cover.png", + 'homepage_url': "https://co.ol", + 'thumbnail_url': "https://co.ol/thumbnail.png", + 'language': "en" + } + + # create new channel with all fields set + tx = await self.out(self.create_channel('@bigchannel', **values)) + txo = tx['outputs'][0] + self.assertEqual( + txo['value']['channel'], + {'public_key': txo['value']['channel']['public_key'], **values} + ) + + # create channel with nothing set + tx = await self.out(self.create_channel('@lightchannel')) + txo = tx['outputs'][0] + self.assertEqual( + txo['value']['channel'], + {'public_key': txo['value']['channel']['public_key']} + ) + + # create channel with just some tags + tx = await self.out(self.create_channel('@updatedchannel', tags='blah')) + txo = tx['outputs'][0] + claim_id = txo['claim_id'] + public_key = txo['value']['channel']['public_key'] + self.assertEqual( + txo['value']['channel'], + {'public_key': public_key, 'tags': ['blah']} + ) + + # update channel setting all fields + tx = await self.out(self.update_channel(claim_id, **values)) + txo = tx['outputs'][0] + values['public_key'] = public_key + values['tags'].insert(0, 'blah') # existing tag + self.assertEqual( + txo['value']['channel'], + values + ) + + # clearing and settings tags + tx = await self.out(self.update_channel(claim_id, tags='single', clear_tags=True)) + txo = tx['outputs'][0] + values['tags'] = ['single'] + self.assertEqual( + txo['value']['channel'], + values + ) + + # reset signing key + tx = await self.out(self.update_channel(claim_id, new_signing_key=True)) + txo = tx['outputs'][0] + self.assertNotEqual( + txo['value']['channel']['public_key'], + values['public_key'] + ) + + # send channel to someone else + 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']) + + # before sending + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 3) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 0) + + other_address = await account2.receiving.get_or_create_usable_address() + tx = await self.out(self.update_channel(claim_id, claim_address=other_address)) + + # after sending + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1) + + # shoud not have private key + txo = (await account2.get_channels())[0] + self.assertIsNone(txo.private_key) + + # send the private key too + txoid = f"{tx['outputs'][0]['txid']}:{tx['outputs'][0]['nout']}" + account2.channel_keys[txoid] = self.account.channel_keys[txoid] + + # now should have private key + txo = (await account2.get_channels())[0] + self.assertIsNotNone(txo.private_key) + + class ClaimCommands(CommandTestCase): - async def test_create_update_and_abandon_claim(self): - await self.assertBalance(self.account, '10.0') + async def test_create_claim_names(self): + # claim new name + await self.create_claim('foo') + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1) + await self.assertBalance(self.account, '8.993893') - claim = await self.make_claim(amount='2.5') # creates new claim - txs = await self.out(self.daemon.jsonrpc_transaction_list()) - self.assertEqual(len(txs[0]['claim_info']), 1) - self.assertEqual(txs[0]['confirmations'], 1) - self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5') - self.assertEqual(txs[0]['claim_info'][0]['claim_id'], claim['claim_id']) - self.assertEqual(txs[0]['value'], '0.0') - self.assertEqual(txs[0]['fee'], '-0.020107') - await self.assertBalance(self.account, '7.479893') + # fail to claim duplicate + with self.assertRaisesRegex(Exception, "You already have a claim published under the name 'foo'."): + await self.create_claim('foo') - await self.make_claim(amount='1.0') # updates previous claim - txs = await self.out(self.daemon.jsonrpc_transaction_list()) - self.assertEqual(len(txs[0]['update_info']), 1) - self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5') - self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim['claim_id']) - self.assertEqual(txs[0]['value'], '0.0') - self.assertEqual(txs[0]['fee'], '-0.000182') - await self.assertBalance(self.account, '8.979711') + # fail claim starting with @ + with self.assertRaisesRegex(Exception, "Claim names cannot start with @ symbol."): + await self.create_claim('@foo') - await self.out(self.daemon.jsonrpc_claim_abandon(claim['claim_id'])) - txs = await self.out(self.daemon.jsonrpc_transaction_list()) - self.assertEqual(len(txs[0]['abandon_info']), 1) - self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0') - self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim['claim_id']) - self.assertEqual(txs[0]['value'], '0.0') - self.assertEqual(txs[0]['fee'], '-0.000107') - await self.assertBalance(self.account, '9.979604') + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1) + await self.assertBalance(self.account, '8.993893') - async def test_update_claim_holding_address(self): - other_account_id = (await self.daemon.jsonrpc_account_create('second account'))['id'] - other_account = self.daemon.get_account_or_error(other_account_id) - other_address = await other_account.receiving.get_or_create_usable_address() + # succeed overriding duplicate restriction + await self.create_claim('foo', allow_duplicate_name=True) + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2) + await self.assertBalance(self.account, '7.987786') - await self.assertBalance(self.account, '10.0') + async def test_bids(self): + # enough funds + tx = await self.create_claim('foo', '2.0') + claim_id = tx['outputs'][0]['claim_id'] + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1) + await self.assertBalance(self.account, '7.993893') - # create the initial name claim - claim = await self.make_claim() + # bid preserved on update + tx = await self.update_claim(claim_id) + self.assertEqual(tx['outputs'][0]['amount'], '2.0') - self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine()), 1) - self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine(account_id=other_account_id)), 0) - tx = await self.daemon.jsonrpc_claim_send_to_address( - claim['claim_id'], other_address - ) - await self.ledger.wait(tx) - self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine()), 0) - self.assertEqual(len(await self.daemon.jsonrpc_claim_list_mine(account_id=other_account_id)), 1) + # bid changed on update + tx = await self.update_claim(claim_id, bid='3.0') + self.assertEqual(tx['outputs'][0]['amount'], '3.0') - async def test_publishing_checks_all_accounts_for_certificate(self): + await self.assertBalance(self.account, '6.993384') + + # not enough funds + with self.assertRaisesRegex( + InsufficientFundsError, "Not enough funds to cover this transaction."): + await self.create_claim('foo2', '9.0') + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1) + await self.assertBalance(self.account, '6.993384') + + # spend exactly amount available, no change + tx = await self.create_claim('foo3', '6.98527700') + await self.assertBalance(self.account, '0.0') + self.assertEqual(len(tx['outputs']), 1) # no change + self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2) + + async def test_publishing_checks_all_accounts_for_channel(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']) - + await self.out(self.create_channel('@spam', '1.0')) self.assertEqual('8.989893', await self.daemon.jsonrpc_account_balance()) result = await self.out(self.daemon.jsonrpc_wallet_send( @@ -84,9 +229,8 @@ class ClaimCommands(CommandTestCase): 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']) + baz_tx = await self.out(self.create_channel('@baz', '1.0', account_id=account2_id)) + baz_id = baz_tx['outputs'][0]['claim_id'] channels = await self.out(self.daemon.jsonrpc_channel_list(account1_id)) self.assertEqual(len(channels), 1) @@ -98,84 +242,158 @@ class ClaimCommands(CommandTestCase): 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']) - + await self.create_claim('hovercraft1', channel_id=baz_id) # 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']) - + await self.create_claim('hovercraft2', channel_id=baz_id, channel_account_id=[account2_id]) # 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] - )) + with self.assertRaisesRegex(ValueError, "Couldn't find channel with channel_id"): + await self.create_claim( + 'hovercraft3', channel_id=baz_id, channel_account_id=[account1_id] + ) - async def test_updating_claim_includes_claim_value_in_balance_check(self): + async def test_setting_claim_fields(self): + values = { + 'title': "Cool Channel", + 'description': "Best channel on LBRY.", + 'contact_email': "human@email.com", + 'tags': ["cool", "awesome"], + 'cover_url': "https://co.ol/cover.png", + 'homepage_url': "https://co.ol", + 'thumbnail_url': "https://co.ol/thumbnail.png", + 'language': "en" + } + + # create new channel with all fields set + tx = await self.out(self.create_channel('@bigchannel', **values)) + txo = tx['outputs'][0] + self.assertEqual( + txo['value']['channel'], + {'public_key': txo['value']['channel']['public_key'], **values} + ) + + # create channel with nothing set + tx = await self.out(self.create_channel('@lightchannel')) + txo = tx['outputs'][0] + self.assertEqual( + txo['value']['channel'], + {'public_key': txo['value']['channel']['public_key']} + ) + + # create channel with just some tags + tx = await self.out(self.create_channel('@updatedchannel', tags='blah')) + txo = tx['outputs'][0] + claim_id = txo['claim_id'] + public_key = txo['value']['channel']['public_key'] + self.assertEqual( + txo['value']['channel'], + {'public_key': public_key, 'tags': ['blah']} + ) + + # update channel setting all fields + tx = await self.out(self.update_channel(claim_id, **values)) + txo = tx['outputs'][0] + values['public_key'] = public_key + values['tags'].insert(0, 'blah') # existing tag + self.assertEqual( + txo['value']['channel'], + values + ) + + # clearing and settings tags + tx = await self.out(self.update_channel(claim_id, tags='single', clear_tags=True)) + txo = tx['outputs'][0] + values['tags'] = ['single'] + self.assertEqual( + txo['value']['channel'], + values + ) + + # reset signing key + tx = await self.out(self.update_channel(claim_id, new_signing_key=True)) + txo = tx['outputs'][0] + self.assertNotEqual( + txo['value']['channel']['public_key'], + values['public_key'] + ) + + # send channel to someone else + 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']) + + # before sending + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 3) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 0) + + other_address = await account2.receiving.get_or_create_usable_address() + tx = await self.out(self.update_channel(claim_id, claim_address=other_address)) + + # after sending + self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2) + self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1) + + # shoud not have private key + txo = (await account2.get_channels())[0] + self.assertIsNone(txo.private_key) + + # send the private key too + txoid = f"{tx['outputs'][0]['txid']}:{tx['outputs'][0]['nout']}" + account2.channel_keys[txoid] = self.account.channel_keys[txoid] + + # now should have private key + txo = (await account2.get_channels())[0] + self.assertIsNotNone(txo.private_key) + + async def test_create_update_and_abandon_claim(self): await self.assertBalance(self.account, '10.0') - await self.make_claim(amount='9.0') - await self.assertBalance(self.account, '0.979893') + tx = await self.create_claim(bid='2.5') # creates new claim + claim_id = tx['outputs'][0]['claim_id'] + txs = await self.out(self.daemon.jsonrpc_transaction_list()) + self.assertEqual(len(txs[0]['claim_info']), 1) + self.assertEqual(txs[0]['confirmations'], 1) + self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5') + self.assertEqual(txs[0]['claim_info'][0]['claim_id'], claim_id) + self.assertEqual(txs[0]['value'], '0.0') + self.assertEqual(txs[0]['fee'], '-0.020107') + await self.assertBalance(self.account, '7.479893') - # update the same claim - await self.make_claim(amount='9.0') - await self.assertBalance(self.account, '0.979637') + await self.update_claim(claim_id, bid='1.0') # updates previous claim + txs = await self.out(self.daemon.jsonrpc_transaction_list()) + self.assertEqual(len(txs[0]['update_info']), 1) + self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5') + self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id) + self.assertEqual(txs[0]['value'], '0.0') + self.assertEqual(txs[0]['fee'], '-0.000184') + await self.assertBalance(self.account, '8.979709') - # update the claim a second time but use even more funds - await self.make_claim(amount='9.97') - await self.assertBalance(self.account, '0.009381') - - # fails when specifying more than available - with tempfile.NamedTemporaryFile() as file: - file.write(b'hi!') - file.flush() - with self.assertRaisesRegex( - InsufficientFundsError, - "Please lower the bid value, the maximum amount" - " you can specify for this claim is 9.979307." - ): - await self.out(self.daemon.jsonrpc_publish( - 'hovercraft', '9.98', file_path=file.name - )) + await self.out(self.daemon.jsonrpc_claim_abandon(claim_id)) + txs = await self.out(self.daemon.jsonrpc_transaction_list()) + self.assertEqual(len(txs[0]['abandon_info']), 1) + self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0') + self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id) + self.assertEqual(txs[0]['value'], '0.0') + self.assertEqual(txs[0]['fee'], '-0.000107') + await self.assertBalance(self.account, '9.979602') async def test_abandoning_claim_at_loss(self): await self.assertBalance(self.account, '10.0') - claim = await self.make_claim(amount='0.0001') + tx = await self.create_claim(bid='0.0001') await self.assertBalance(self.account, '9.979793') - await self.out(self.daemon.jsonrpc_claim_abandon(claim['claim_id'])) + await self.out(self.daemon.jsonrpc_claim_abandon(tx['outputs'][0]['claim_id'])) await self.assertBalance(self.account, '9.97968399') async def test_claim_show(self): - channel = await self.out(self.daemon.jsonrpc_channel_new('@abc', "1.0")) - self.assertTrue(channel['success']) - await self.confirm_tx(channel['tx']['txid']) + channel = await self.create_channel('@abc', '1.0') channel_from_claim_show = await self.out( - self.daemon.jsonrpc_claim_show(txid=channel['tx']['txid'], nout=channel['output']['nout']) + self.daemon.jsonrpc_claim_show(txid=channel['txid'], nout=0) ) - self.assertEqual(channel_from_claim_show['value'], channel['output']['value']) + self.assertEqual(channel_from_claim_show['value'], channel['outputs'][0]['value']) channel_from_claim_show = await self.out( - self.daemon.jsonrpc_claim_show(claim_id=channel['claim_id']) + self.daemon.jsonrpc_claim_show(claim_id=channel['outputs'][0]['claim_id']) ) - self.assertEqual(channel_from_claim_show['value'], channel['output']['value']) + self.assertEqual(channel_from_claim_show['value'], channel['outputs'][0]['value']) - abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=channel['tx']['txid'], nout=0, blocking=False)) + abandon = await self.out(self.daemon.jsonrpc_claim_abandon(txid=channel['txid'], nout=0, blocking=False)) self.assertTrue(abandon['success']) await self.confirm_tx(abandon['tx']['txid']) not_a_claim = await self.out( @@ -184,13 +402,10 @@ class ClaimCommands(CommandTestCase): self.assertEqual(not_a_claim, 'claim not found') async def test_claim_list(self): - channel = await self.out(self.daemon.jsonrpc_channel_new('@abc', "1.0")) - self.assertTrue(channel['success']) - await self.confirm_tx(channel['tx']['txid']) - claim = await self.make_claim(amount='0.0001', name='on-channel-claim', channel_name='@abc') - self.assertTrue(claim['success']) - unsigned_claim = await self.make_claim(amount='0.0001', name='unsigned') - self.assertTrue(claim['success']) + channel = await self.create_channel('@abc', '1.0') + channel_id = channel['outputs'][0]['claim_id'] + claim = await self.create_claim('on-channel-claim', '0.0001', channel_id=channel_id) + unsigned_claim = await self.create_claim('unsigned', '0.0001') channel_from_claim_list = await self.out(self.daemon.jsonrpc_claim_list('@abc')) self.assertEqual(channel_from_claim_list['claims'][0]['value'], channel['output']['value']) @@ -373,8 +588,8 @@ class ClaimCommands(CommandTestCase): # this test assumes that the lbrycrd forks normalization at height == 250 on regtest - c1 = await self.make_claim('ΣίσυφοςfiÆ', '0.1') - c2 = await self.make_claim('ΣΊΣΥΦΟσFIæ', '0.2') + c1 = await self.create_claim('ΣίσυφοςfiÆ', '0.1') + c2 = await self.create_claim('ΣΊΣΥΦΟσFIæ', '0.2') r1 = await self.daemon.jsonrpc_resolve(urls='lbry://ΣίσυφοςfiÆ') r2 = await self.daemon.jsonrpc_resolve(urls='lbry://ΣΊΣΥΦΟσFIæ') diff --git a/tests/integration/test_file_commands.py b/tests/integration/test_file_commands.py index 9f8b83eed..1222ce20f 100644 --- a/tests/integration/test_file_commands.py +++ b/tests/integration/test_file_commands.py @@ -4,7 +4,6 @@ import os from integration.testcase import CommandTestCase from lbrynet.blob_exchange.downloader import BlobDownloader -from lbrynet.error import InsufficientFundsError class FileCommands(CommandTestCase): @@ -12,8 +11,8 @@ class FileCommands(CommandTestCase): VERBOSITY = logging.WARN async def test_file_management(self): - await self.make_claim('foo', '0.01') - await self.make_claim('foo2', '0.01') + await self.create_claim('foo', '0.01') + await self.create_claim('foo2', '0.01') file1, file2 = self.daemon.jsonrpc_file_list('claim_name') self.assertEqual(file1['claim_name'], 'foo') @@ -28,8 +27,8 @@ class FileCommands(CommandTestCase): self.assertEqual(len(self.daemon.jsonrpc_file_list()), 1) async def test_download_different_timeouts(self): - claim = await self.make_claim('foo', '0.01') - sd_hash = claim['output']['value']['stream']['hash'] + tx = await self.create_claim('foo', '0.01') + sd_hash = tx['outputs'][0]['value']['stream']['hash'] await self.daemon.jsonrpc_file_delete(claim_name='foo') all_except_sd = [ blob_hash for blob_hash in self.server.blob_manager.completed_blob_hashes if blob_hash != sd_hash @@ -49,7 +48,7 @@ class FileCommands(CommandTestCase): await asyncio.sleep(0.01) async def test_filename_conflicts_management_on_resume_download(self): - await self.make_claim('foo', '0.01', data=bytes([0]*(1<<23))) + await self.create_claim('foo', '0.01', data=bytes([0]*(1<<23))) file_info = self.daemon.jsonrpc_file_list()[0] original_path = os.path.join(self.daemon.conf.download_dir, file_info['file_name']) await self.daemon.jsonrpc_file_delete(claim_name='foo') @@ -70,8 +69,8 @@ class FileCommands(CommandTestCase): # this used to be inconsistent, if it becomes again it would create weird bugs, so worth checking async def test_incomplete_downloads_erases_output_file_on_stop(self): - claim = await self.make_claim('foo', '0.01') - sd_hash = claim['output']['value']['stream']['hash'] + tx = await self.create_claim('foo', '0.01') + sd_hash = tx['outputs'][0]['value']['stream']['hash'] file_info = self.daemon.jsonrpc_file_list()[0] await self.daemon.jsonrpc_file_delete(claim_name='foo') blobs = await self.server_storage.get_blobs_for_stream( @@ -89,8 +88,8 @@ class FileCommands(CommandTestCase): self.assertFalse(os.path.isfile(os.path.join(self.daemon.conf.download_dir, file_info['file_name']))) async def test_incomplete_downloads_retry(self): - claim = await self.make_claim('foo', '0.01') - sd_hash = claim['output']['value']['stream']['hash'] + tx = await self.create_claim('foo', '0.01') + sd_hash = tx['outputs'][0]['value']['stream']['hash'] await self.daemon.jsonrpc_file_delete(claim_name='foo') blobs = await self.server_storage.get_blobs_for_stream( await self.server_storage.get_stream_hash_for_sd_hash(sd_hash) @@ -129,8 +128,8 @@ class FileCommands(CommandTestCase): async def test_unban_recovers_stream(self): BlobDownloader.BAN_TIME = .5 # fixme: temporary field, will move to connection manager or a conf - claim = await self.make_claim('foo', '0.01', data=bytes([0]*(1<<23))) - sd_hash = claim['output']['value']['stream']['hash'] + tx = await self.create_claim('foo', '0.01', data=bytes([0]*(1<<23))) + sd_hash = tx['outputs'][0]['value']['stream']['hash'] missing_blob_hash = (await self.daemon.jsonrpc_blob_list(sd_hash=sd_hash))[-2] await self.daemon.jsonrpc_file_delete(claim_name='foo') # backup blob @@ -151,27 +150,30 @@ class FileCommands(CommandTestCase): target_address = await self.blockchain.get_raw_change_address() # FAIL: beyond available balance - await self.make_claim( + await self.create_claim( 'expensive', '0.01', data=b'pay me if you can', - fee={'currency': 'LBC', 'amount': 11.0, 'address': target_address}) + fee_currency='LBC', fee_amount='11.0', fee_address=target_address + ) await self.daemon.jsonrpc_file_delete(claim_name='expensive') response = await self.daemon.jsonrpc_get('lbry://expensive') self.assertEqual(response['error'], 'fee of 11.00000 exceeds max available balance') self.assertEqual(len(self.daemon.jsonrpc_file_list()), 0) # FAIL: beyond maximum key fee - await self.make_claim( + await self.create_claim( 'maxkey', '0.01', data=b'no pay me, no', - fee={'currency': 'LBC', 'amount': 111.0, 'address': target_address}) + fee_currency='LBC', fee_amount='111.0', fee_address=target_address + ) await self.daemon.jsonrpc_file_delete(claim_name='maxkey') response = await self.daemon.jsonrpc_get('lbry://maxkey') self.assertEqual(len(self.daemon.jsonrpc_file_list()), 0) self.assertEqual(response['error'], 'fee of 111.00000 exceeds max configured to allow of 50.00000') # PASS: purchase is successful - await self.make_claim( + await self.create_claim( 'icanpay', '0.01', data=b'I got the power!', - fee={'currency': 'LBC', 'amount': 1.0, 'address': target_address}) + fee_currency='LBC', fee_amount='1.0', fee_address=target_address + ) await self.daemon.jsonrpc_file_delete(claim_name='icanpay') await self.assertBalance(self.account, '9.925679') response = await self.daemon.jsonrpc_get('lbry://icanpay') diff --git a/tests/integration/test_resolve_command.py b/tests/integration/test_resolve_command.py index 9f2ce1730..c82496fa7 100644 --- a/tests/integration/test_resolve_command.py +++ b/tests/integration/test_resolve_command.py @@ -4,7 +4,8 @@ from integration.testcase import CommandTestCase class ResolveCommand(CommandTestCase): async def test_resolve(self): - await self.make_channel('@abc', '0.01') + tx = await self.create_channel('@abc', '0.01') + channel_id = tx['outputs'][0]['claim_id'] # resolving a channel @abc response = await self.resolve('lbry://@abc') @@ -14,8 +15,8 @@ class ResolveCommand(CommandTestCase): self.assertEqual(response['lbry://@abc']['certificate']['name'], '@abc') self.assertEqual(response['lbry://@abc']['claims_in_channel'], 0) - await self.make_claim('foo', '0.01', channel_name='@abc') - await self.make_claim('foo2', '0.01', channel_name='@abc') + await self.create_claim('foo', '0.01', channel_id=channel_id) + await self.create_claim('foo2', '0.01', channel_id=channel_id) # resolving a channel @abc with some claims in it response = await self.resolve('lbry://@abc') diff --git a/tests/integration/test_sync.py b/tests/integration/test_sync.py index 547a7d0be..829b47d27 100644 --- a/tests/integration/test_sync.py +++ b/tests/integration/test_sync.py @@ -62,7 +62,7 @@ class AccountSynchronization(AsyncioTestCase): self.account.modified_on = 123.456 self.assertEqual(self.daemon.jsonrpc_sync_hash(), starting_hash) self.assertEqual(self.daemon.jsonrpc_sync_apply('password')['hash'], starting_hash) - self.assertFalse(self.account.certificates) + self.assertFalse(self.account.channel_keys) hash_w_cert = '974721f42dab42657b5911b7caf4af98ce4d3879eea6ac23d50c1d79bc5020ef' add_cert = ( @@ -78,9 +78,9 @@ class AccountSynchronization(AsyncioTestCase): ) self.daemon.jsonrpc_sync_apply('password', data=add_cert) self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_cert) - self.assertEqual(self.account.certificates, {'abcdefg1234:0': '---PRIVATE KEY---'}) + self.assertEqual(self.account.channel_keys, {'abcdefg1234:0': '---PRIVATE KEY---'}) # applying the same diff is idempotent self.daemon.jsonrpc_sync_apply('password', data=add_cert) self.assertEqual(self.daemon.jsonrpc_sync_hash(), hash_w_cert) - self.assertEqual(self.account.certificates, {'abcdefg1234:0': '---PRIVATE KEY---'}) + self.assertEqual(self.account.channel_keys, {'abcdefg1234:0': '---PRIVATE KEY---'}) diff --git a/tests/integration/testcase.py b/tests/integration/testcase.py index 84abf8e32..7f78b2e0b 100644 --- a/tests/integration/testcase.py +++ b/tests/integration/testcase.py @@ -155,29 +155,53 @@ class CommandTestCase(IntegrationTestCase): to JSON and then back to a dictionary. """ return json.loads(jsonrpc_dumps_pretty(await awaitable, ledger=self.ledger))['result'] - async def make_claim(self, name='hovercraft', amount='1.0', data=b'hi!', - channel_name=None, confirm=True, account_id=None, fee=None): + async def create_claim(self, name='hovercraft', bid='1.0', data=b'hi!', confirm=True, **kwargs): with tempfile.NamedTemporaryFile() as file: file.write(data) file.flush() - claim = await self.out(self.daemon.jsonrpc_publish( - name, amount, file_path=file.name, channel_name=channel_name, account_id=account_id, - fee=fee - )) - self.assertTrue(claim['success']) + claim = await self.out( + self.daemon.jsonrpc_publish(name, bid, file_path=file.name, **kwargs) + ) + self.assertEqual(claim['outputs'][0]['name'], name) if confirm: - await self.on_transaction_dict(claim['tx']) + await self.on_transaction_dict(claim) await self.generate(1) - await self.on_transaction_dict(claim['tx']) + await self.on_transaction_dict(claim) return claim - async def make_channel(self, name='@arena', amount='1.0', confirm=True, account_id=None): - channel = await self.out(self.daemon.jsonrpc_channel_new(name, amount, account_id)) - self.assertTrue(channel['success']) + async def update_claim(self, claim_id, data=None, confirm=True, **kwargs): + if data: + with tempfile.NamedTemporaryFile() as file: + file.write(data) + file.flush() + claim = await self.out( + self.daemon.jsonrpc_claim_update(claim_id, file_path=file.name, **kwargs) + ) + else: + claim = await self.out(self.daemon.jsonrpc_claim_update(claim_id, **kwargs)) + self.assertIsNotNone(claim['outputs'][0]['name']) if confirm: - await self.on_transaction_dict(channel['tx']) + await self.on_transaction_dict(claim) await self.generate(1) - await self.on_transaction_dict(channel['tx']) + await self.on_transaction_dict(claim) + return claim + + async def create_channel(self, name='@arena', bid='1.0', confirm=True, **kwargs): + channel = await self.out(self.daemon.jsonrpc_channel_create(name, bid, **kwargs)) + self.assertEqual(channel['outputs'][0]['name'], name) + if confirm: + await self.on_transaction_dict(channel) + await self.generate(1) + await self.on_transaction_dict(channel) + return channel + + async def update_channel(self, claim_id, confirm=True, **kwargs): + channel = await self.out(self.daemon.jsonrpc_channel_update(claim_id, **kwargs)) + self.assertTrue(channel['outputs'][0]['name'].startswith('@')) + if confirm: + await self.on_transaction_dict(channel) + await self.generate(1) + await self.on_transaction_dict(channel) return channel async def resolve(self, uri): diff --git a/tests/unit/lbrynet_daemon/test_mime_types.py b/tests/unit/lbrynet_daemon/test_mime_types.py index 52abe8852..6d2143471 100644 --- a/tests/unit/lbrynet_daemon/test_mime_types.py +++ b/tests/unit/lbrynet_daemon/test_mime_types.py @@ -1,5 +1,5 @@ import unittest -from lbrynet.extras.daemon import mime_types +from lbrynet.schema import mime_types class TestMimeTypes(unittest.TestCase):