diff --git a/lbry/lbry/extras/daemon/Daemon.py b/lbry/lbry/extras/daemon/Daemon.py index 58ee50920..77ac647ff 100644 --- a/lbry/lbry/extras/daemon/Daemon.py +++ b/lbry/lbry/extras/daemon/Daemon.py @@ -3271,6 +3271,337 @@ class Daemon(metaclass=JSONRPCServerType): """ return self.get_est_cost_from_uri(uri) + COLLECTION_DOC = """ + Create, list and abandon collections. + """ + + @requires(WALLET_COMPONENT) + async def jsonrpc_collection_create( + self, name, bid, claims, allow_duplicate_name=False, + channel_id=None, channel_name=None, channel_account_id=None, + account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None, + preview=False, blocking=False, **kwargs): + """ + Create a new collection .... + + Usage: + collection_create ( | --name=) ( | --bid=) + (... | --claims=...) + [--allow_duplicate_name] + [--title=] [--description=<description>] + [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...] + [--thumbnail_url=<thumbnail_url>] + [--account_id=<account_id>] [--wallet_id=<wallet_id>] + [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...] + [--preview] [--blocking] + + Options: + --name=<name> : (str) name of the collection + --bid=<bid> : (decimal) amount to back the claim + --claims=<claims> : (list) claim ids to be included in the collection + --allow_duplicate_name : (bool) create new collection even if one already exists with + given name. default: false. + --title=<title> : (str) title of the collection + --description=<description> : (str) description of the collection + --clear_languages : (bool) clear existing languages (prior to adding new ones) + --tags=<tags> : (list) content tags + --clear_languages : (bool) clear existing languages (prior to adding new ones) + --languages=<languages> : (list) languages used by the collection, + using RFC 5646 format, eg: + for English `--languages=en` + for Spanish (Spain) `--languages=es-ES` + for Spanish (Mexican) `--languages=es-MX` + for Chinese (Simplified) `--languages=zh-Hans` + for Chinese (Traditional) `--languages=zh-Hant` + --locations=<locations> : (list) locations of the collection, consisting of 2 letter + `country` code and a `state`, `city` and a postal + `code` along with a `latitude` and `longitude`. + for JSON RPC: pass a dictionary with aforementioned + attributes as keys, eg: + ... + "locations": [{'country': 'US', 'state': 'NH'}] + ... + for command line: pass a colon delimited list + with values in the following order: + + "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE" + + making sure to include colon for blank values, for + example to provide only the city: + + ... --locations="::Manchester" + + with all values set: + + ... --locations="US:NH:Manchester:03101:42.990605:-71.460989" + + optionally, you can just pass the "LATITUDE:LONGITUDE": + + ... --locations="42.990605:-71.460989" + + finally, you can also pass JSON string of dictionary + on the command line as you would via JSON RPC + + ... --locations="{'country': 'US', 'state': 'NH'}" + + --thumbnail_url=<thumbnail_url>: (str) thumbnail url + --account_id=<account_id> : (str) account to use for holding the transaction + --wallet_id=<wallet_id> : (str) restrict operation to specific wallet + --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction + --claim_address=<claim_address>: (str) address where the collection is sent to, if not specified + it will be determined automatically from the account + --preview : (bool) do not broadcast the transaction + --blocking : (bool) wait until transaction is in mempool + + Returns: {Transaction} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + account = wallet.get_account_or_default(account_id) + funding_accounts = wallet.get_accounts_or_all(funding_account_ids) + self.valid_collection_name_or_error(name) + channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True) + amount = self.get_dewies_or_error('bid', bid, positive_value=True) + claim_address = await self.get_receiving_address(claim_address, account) + + existing_collections = await self.ledger.get_collections(accounts=wallet.accounts, claim_name=name) + if len(existing_collections) > 0: + if not allow_duplicate_name: + raise Exception( + f"You already have a collection under the name '{name}'. " + f"Use --allow-duplicate-name flag to override." + ) + + claim = Claim() + claim.collection.update(claims=claims, **kwargs) # maybe specify claims=[] # here + tx = await Transaction.claim_create( + name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel + ) + new_txo = tx.outputs[0] + + if channel: + new_txo.sign(channel) + await tx.sign(funding_accounts) + if not preview: + await self.broadcast_or_release(tx, blocking) + await self.storage.save_claims([self._old_get_temp_claim_info( + tx, new_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) + async def jsonrpc_collection_update( + self, claim_id, bid=None, claim=None, allow_duplicate_name=False, + channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False, + account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None, + preview=False, blocking=False, replace=False, **kwargs): + """ + Update an existing collection claim. + + Usage: + collection_update (<claim_id> | --claim_id=<claim_id>) [--bid=<bid>] + [--claims=<claims>...] [--clear_claim_ids] + [--title=<title>] [--description=<description>] + [--tags=<tags>...] [--clear_tags] + [--languages=<languages>...] [--clear_languages] + [--locations=<locations>...] [--clear_locations] + [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>] + [--account_id=<account_id>] [--wallet_id=<wallet_id>] + [--claim_address=<claim_address>] [--new_signing_key] + [--funding_account_ids=<funding_account_ids>...] + [--preview] [--blocking] [--replace] + + Options: + --claim_id=<claim_id> : (str) claim_id of the collection to update + --bid=<bid> : (decimal) amount to back the claim + --claims=<claim_ids> : (list) claim ids + --clear_claims : (bool) clear existing claim references (prior to adding new ones) + --title=<title> : (str) title of the collection + --description=<description> : (str) description of the collection + --tags=<tags> : (list) add content tags + --clear_tags : (bool) clear existing tags (prior to adding new ones) + --languages=<languages> : (list) languages used by the collection, + using RFC 5646 format, eg: + for English `--languages=en` + for Spanish (Spain) `--languages=es-ES` + for Spanish (Mexican) `--languages=es-MX` + for Chinese (Simplified) `--languages=zh-Hans` + for Chinese (Traditional) `--languages=zh-Hant` + --clear_languages : (bool) clear existing languages (prior to adding new ones) + --locations=<locations> : (list) locations of the collection, consisting of 2 letter + `country` code and a `state`, `city` and a postal + `code` along with a `latitude` and `longitude`. + for JSON RPC: pass a dictionary with aforementioned + attributes as keys, eg: + ... + "locations": [{'country': 'US', 'state': 'NH'}] + ... + for command line: pass a colon delimited list + with values in the following order: + + "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE" + + making sure to include colon for blank values, for + example to provide only the city: + + ... --locations="::Manchester" + + with all values set: + + ... --locations="US:NH:Manchester:03101:42.990605:-71.460989" + + optionally, you can just pass the "LATITUDE:LONGITUDE": + + ... --locations="42.990605:-71.460989" + + finally, you can also pass JSON string of dictionary + on the command line as you would via JSON RPC + + ... --locations="{'country': 'US', 'state': 'NH'}" + + --clear_locations : (bool) clear existing locations (prior to adding new ones) + --thumbnail_url=<thumbnail_url>: (str) thumbnail url + #--cover_url=<cover_url> : (str) url of cover image + --account_id=<account_id> : (str) account in which to look for collection (default: all) + --wallet_id=<wallet_id> : (str) restrict operation to specific wallet + --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction + --claim_address=<claim_address>: (str) address where the collection is sent + --new_signing_key : (bool) generate a new signing key, will invalidate all previous publishes + --preview : (bool) do not broadcast the transaction + --blocking : (bool) wait until transaction is in mempool + --replace : (bool) instead of modifying specific values on + the collection, this will clear all existing values + and only save passed in values, useful for form + submissions where all values are always set + + Returns: {Transaction} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + funding_accounts = wallet.get_accounts_or_all(funding_account_ids) + if account_id: + account = wallet.get_account_or_error(account_id) + accounts = [account] + else: + account = wallet.default_account + accounts = wallet.accounts + + existing_collections = await self.ledger.get_collections( + wallet=wallet, accounts=accounts, claim_id=claim_id + ) + if len(existing_collections) != 1: + account_ids = ', '.join(f"'{account.id}'" for account in accounts) + raise Exception( + f"Can't find the collection '{claim_id}' in account(s) {account_ids}." + ) + # Here we might have a problem of replacing a stream with a collection + old_txo = existing_collections[0] + if not old_txo.claim.is_collection: # as we're only checking @ or not, this is not definitive + raise Exception( + f"A claim with id '{claim_id}' was found but it is not a stream or collection." + ) + + 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.valid_address_or_error(claim_address) + else: + claim_address = old_txo.get_address(account.ledger) + + channel = None + if channel_id or channel_name: + channel = await self.get_channel_or_error( + wallet, channel_account_id, channel_id, channel_name, for_signing=True) + elif old_txo.claim.is_signed and not clear_channel and not replace: + channel = old_txo.channel + + if replace: + claim = Claim() + claim.collection.message.source.CopyFrom( + old_txo.claim.collection.message.source + ) + + claim.collection.update(**kwargs) + else: + claim = Claim.from_bytes(old_txo.claim.to_bytes()) + claim.collection.update(**kwargs) + tx = await Transaction.claim_update( + old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel + ) + new_txo = tx.outputs[0] + + new_txo.script.generate() + + if channel: + new_txo.sign(channel) + await tx.sign(funding_accounts) + + if not preview: + await self.broadcast_or_release(tx, blocking) + await self.analytics_manager.send_claim_action('publish') + else: + await account.ledger.release_tx(tx) + + return tx + + @requires(WALLET_COMPONENT) + async def jsonrpc_collection_abandon(self, *args, **kwargs): + """ + Abandon one of my collection claims. + + Usage: + collection_abandon [<claim_id> | --claim_id=<claim_id>] + [<txid> | --txid=<txid>] [<nout> | --nout=<nout>] + [--account_id=<account_id>] [--wallet_id=<wallet_id>] + [--preview] [--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 + --wallet_id=<wallet_id> : (str) restrict operation to specific wallet + --preview : (bool) do not broadcast the transaction + --blocking : (bool) wait until abandon is in mempool + + Returns: {Transaction} + """ + return await self.jsonrpc_stream_abandon(*args, **kwargs) + + @requires(WALLET_COMPONENT) + def jsonrpc_collection_list(self, resolve_claims=False, account_id=None, wallet_id=None, page=None, page_size=None): + """ + List my collection claims. + + Usage: + collection_list [--resolve_claims] [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>] + [--page=<page>] [--page_size=<page_size>] + + Options: + --resolve_claims : (bool) resolve every claim + --account_id=<account_id> : (str) id of the account to use + --wallet_id=<wallet_id> : (str) restrict results to specific wallet + --page=<page> : (int) page to return during paginating + --page_size=<page_size> : (int) number of items on page during pagination + + Returns: {Paginated[Output]} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + if account_id: + account: LBCAccount = wallet.get_account_or_error(account_id) + collections = account.get_collections + collection_count = account.get_collection_count + else: + collections = partial(self.ledger.get_collections, wallet=wallet, accounts=wallet.accounts) + collection_count = partial(self.ledger.get_collection_count, wallet=wallet, accounts=wallet.accounts) + return paginate_rows(collections, collection_count, page, page_size, resolve=resolve_claims) + + SUPPORT_DOC = """ Create, list and abandon all types of supports. """ diff --git a/lbry/lbry/extras/daemon/collection.py b/lbry/lbry/extras/daemon/collection.py index ef487cb22..2c48cfa7b 100644 --- a/lbry/lbry/extras/daemon/collection.py +++ b/lbry/lbry/extras/daemon/collection.py @@ -9,6 +9,7 @@ async def jsonrpc_collection_create( Usage: collection_create (<name> | --name=<name>) (<bid> | --bid=<bid>) + ( --claims=<claimIds> ) [--allow_duplicate_name=<allow_duplicate_name>] [--title=<title>] [--description=<description>] [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...] @@ -20,12 +21,14 @@ async def jsonrpc_collection_create( Options: --name=<name> : (str) name of the collection --bid=<bid> : (decimal) amount to back the claim - --allow_duplicate_name=<allow_duplicate_name> : (bool) create new collection even if one already exists with + --allow_duplicate_name=<allow_duplicate_name> : (bool) create new collection even if one already exists with given name. default: false. - --claims=<claims> : (list) claim ids - --title=<title> : (str) title of the publication - --description=<description> : (str) description of the publication + --claims=<claimIds> : (list) claim ids + --title=<title> : (str) title of the collection + --description=<description> : (str) description of the collection + --clear_languages : (bool) clear existing languages (prior to adding new ones) --tags=<tags> : (list) content tags + --clear_languages : (bool) clear existing languages (prior to adding new ones) --languages=<languages> : (list) languages used by the collection, using RFC 5646 format, eg: for English `--languages=en` @@ -65,7 +68,7 @@ async def jsonrpc_collection_create( ... --locations="{'country': 'US', 'state': 'NH'}" --thumbnail_url=<thumbnail_url>: (str) thumbnail url - # --cover_url=<cover_url> : (str) url of cover image + # --cover_url=<cover_url> : (str) url of cover image --account_id=<account_id> : (str) account to use for holding the transaction --wallet_id=<wallet_id> : (str) restrict operation to specific wallet --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction @@ -94,7 +97,7 @@ async def jsonrpc_collection_create( ) claim = Claim() - claim.collection.update(**kwargs) #maybe specify claims=[] + claim.collection.update(**kwargs) #maybe specify claims=[] # here tx = await Transaction.claim_create( name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel ) @@ -138,8 +141,10 @@ async def jsonrpc_collection_update( Options: --claim_id=<claim_id> : (str) claim_id of the collection to update --bid=<bid> : (decimal) amount to back the claim - --title=<title> : (str) title of the publication - --description=<description> : (str) description of the publication + --claims=<claim_ids> : (list) claim ids + --clear_claims : (bool) clear existing claim references (prior to adding new ones) + --title=<title> : (str) title of the collection + --description=<description> : (str) description of the collection --tags=<tags> : (list) add content tags --clear_tags : (bool) clear existing tags (prior to adding new ones) --languages=<languages> : (list) languages used by the collection, diff --git a/lbry/lbry/extras/daemon/json_response_encoder.py b/lbry/lbry/extras/daemon/json_response_encoder.py index 213e961b2..5d8eff677 100644 --- a/lbry/lbry/extras/daemon/json_response_encoder.py +++ b/lbry/lbry/extras/daemon/json_response_encoder.py @@ -201,6 +201,8 @@ class JSONResponseEncoder(JSONEncoder): output['short_url'] = output['meta'].pop('short_url') if 'canonical_url' in output['meta']: output['canonical_url'] = output['meta'].pop('canonical_url') + if txo.claims is not None: + output['claims'] = [self.encode_output(o) for o in txo.claims] if txo.script.is_claim_name or txo.script.is_update_claim: try: output['value'] = txo.claim diff --git a/lbry/lbry/schema/attrs.py b/lbry/lbry/schema/attrs.py index 7586a7039..5e295d289 100644 --- a/lbry/lbry/schema/attrs.py +++ b/lbry/lbry/schema/attrs.py @@ -347,10 +347,6 @@ class ClaimList(BaseMessageList[ClaimReference]): __slots__ = () item_class = ClaimReference - @property - def _message(self): - return self.message.claim_references - def append(self, value): self.add().claim_id = value diff --git a/lbry/lbry/schema/claim.py b/lbry/lbry/schema/claim.py index 9b8efcb1d..117caa09c 100644 --- a/lbry/lbry/schema/claim.py +++ b/lbry/lbry/schema/claim.py @@ -392,9 +392,10 @@ class Collection(BaseClaim): def to_dict(self): claim = super().to_dict() if 'claim_references' in claim: - claim['claim_references'] = self.claims.ids + claim['claims'] = self.claims.ids + del claim['claim_references'] return claim @property def claims(self) -> ClaimList: - return ClaimList(self.message) + return ClaimList(self.message.claim_references) diff --git a/lbry/lbry/testcase.py b/lbry/lbry/testcase.py index 7b1fe95fb..74599959c 100644 --- a/lbry/lbry/testcase.py +++ b/lbry/lbry/testcase.py @@ -267,10 +267,25 @@ class CommandTestCase(IntegrationTestCase): return await self.confirm_and_render( self.daemon.jsonrpc_channel_abandon(*args, **kwargs), confirm ) +# ClaimIDs = .... + async def collection_create( + self, name='firstcollection', bid='1.0', confirm=True, **kwargs): + return await self.confirm_and_render( + self.daemon.jsonrpc_collection_create(name, bid, **kwargs), confirm + ) +# ClaimIDs = .... + async def collection_update( + self, claim_id, confirm=True, **kwargs): + return await self.confirm_and_render( + self.daemon.jsonrpc_collection_update(claim_id, **kwargs), confirm + ) - # async def collection_create - # async def collection_update - # async def collection_abandon + async def collection_abandon(self, *args, confirm=True, **kwargs): + if 'blocking' not in kwargs: + kwargs['blocking'] = False + return await self.confirm_and_render( + self.daemon.jsonrpc_stream_abandon(*args, **kwargs), confirm + ) async def support_create(self, claim_id, bid='1.0', confirm=True, **kwargs): return await self.confirm_and_render( diff --git a/lbry/lbry/wallet/account.py b/lbry/lbry/wallet/account.py index 1e333b552..de61a7b6b 100644 --- a/lbry/lbry/wallet/account.py +++ b/lbry/lbry/wallet/account.py @@ -5,7 +5,7 @@ from hashlib import sha256 from string import hexdigits import ecdsa -from lbry.wallet.constants import TXO_TYPES +from lbry.wallet.constants import CLAIM_TYPES from torba.client.baseaccount import BaseAccount, HierarchicalDeterministic @@ -91,7 +91,7 @@ class Account(BaseAccount): get_total_balance = partial(self.get_balance, confirmations=confirmations, include_claims=True) total = await get_total_balance() if reserved_subtotals: - claims_balance = await get_total_balance(txo_type__in=[TXO_TYPES['stream'], TXO_TYPES['channel']]) + claims_balance = await get_total_balance(txo_type__in=CLAIM_TYPES) for amount, spent, from_me, to_me, height in await self.get_support_summary(): if confirmations > 0 and not 0 < height <= self.ledger.headers.height - (confirmations - 1): continue @@ -163,6 +163,12 @@ class Account(BaseAccount): def get_channel_count(self, **constraints): return self.ledger.get_channel_count(wallet=self.wallet, accounts=[self], **constraints) + def get_collections(self, **constraints): + return self.ledger.get_collections(wallet=self.wallet, accounts=[self], **constraints) + + def get_collection_count(self, **constraints): + return self.ledger.get_collection_count(wallet=self.wallet, accounts=[self], **constraints) + def get_supports(self, **constraints): return self.ledger.get_supports(wallet=self.wallet, accounts=[self], **constraints) diff --git a/lbry/lbry/wallet/constants.py b/lbry/lbry/wallet/constants.py index 0e1854e33..fb7e69d0f 100644 --- a/lbry/lbry/wallet/constants.py +++ b/lbry/lbry/wallet/constants.py @@ -5,3 +5,9 @@ TXO_TYPES = { "purchase": 4, "collection": 5 } + +CLAIM_TYPES = [ + TXO_TYPES['stream'], + TXO_TYPES['channel'], + TXO_TYPES['collection'], +] diff --git a/lbry/lbry/wallet/database.py b/lbry/lbry/wallet/database.py index aee322cf1..e8730cf5f 100644 --- a/lbry/lbry/wallet/database.py +++ b/lbry/lbry/wallet/database.py @@ -3,7 +3,7 @@ from typing import List from torba.client.basedatabase import BaseDatabase from lbry.wallet.transaction import Output -from lbry.wallet.constants import TXO_TYPES +from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPES class WalletDatabase(BaseDatabase): @@ -140,14 +140,7 @@ class WalletDatabase(BaseDatabase): @staticmethod def constrain_claims(constraints): - constraints['txo_type__in'] = [ - TXO_TYPES['stream'], TXO_TYPES['channel'] - ] - - def constrain_claims(constraints): - constraints['txo_type__in'] = [ - TXO_TYPES['stream'], TXO_TYPES['channel'] - ] + constraints['txo_type__in'] = CLAIM_TYPES async def get_claims(self, **constraints) -> List[Output]: self.constrain_claims(constraints) @@ -198,11 +191,11 @@ class WalletDatabase(BaseDatabase): constraints['txo_type'] = TXO_TYPES['collection'] def get_collections(self, **constraints): - self.constrain_supports(constraints) + self.constrain_collections(constraints) return self.get_utxos(**constraints) def get_collection_count(self, **constraints): - self.constrain_supports(constraints) + self.constrain_collections(constraints) return self.get_utxo_count(**constraints) async def release_all_outputs(self, account): diff --git a/lbry/lbry/wallet/ledger.py b/lbry/lbry/wallet/ledger.py index 2e96b7851..d913f4da5 100644 --- a/lbry/lbry/wallet/ledger.py +++ b/lbry/lbry/wallet/ledger.py @@ -186,10 +186,29 @@ class MainNetLedger(BaseLedger): def get_channel_count(self, **constraints): return self.db.get_channel_count(**constraints) - def get_collections(self, **constraints): - return self.db.get_collections(**constraints) + async def get_collections(self, resolve=False, **constraints): + collections = await self.db.get_collections(**constraints) + if resolve: + for collection in collections: + claim_ids = collection.claim.collection.claims.ids; + try: + resolve_results, _, _ = await self.claim_search([], claim_ids=collection.claim.collection.claims.ids) + except: + log.exception("Resolve failed while looking up collection claim ids:") + claims = [] + for claim_id in claim_ids: + found = False + for txo in resolve_results: + if txo.claim_id == claim_id: + claims.append(txo) + found = True + break + if not found: + claims.append(None) + collection.claims = claims + return collections - def get_collection_count(self, **constraints): + def get_collection_count(self, resolve=False, **constraints): return self.db.get_collection_count(**constraints) def get_supports(self, **constraints): diff --git a/lbry/lbry/wallet/transaction.py b/lbry/lbry/wallet/transaction.py index fba9d3506..4de21de7b 100644 --- a/lbry/lbry/wallet/transaction.py +++ b/lbry/lbry/wallet/transaction.py @@ -32,6 +32,7 @@ class Output(BaseOutput): __slots__ = ( 'channel', 'private_key', 'meta', 'purchase', 'purchased_claim', 'purchase_receipt', + 'claims', ) def __init__(self, *args, channel: Optional['Output'] = None, @@ -42,6 +43,7 @@ class Output(BaseOutput): self.purchase: 'Output' = None # txo containing purchase metadata self.purchased_claim: 'Output' = None # resolved claim pointed to by purchase self.purchase_receipt: 'Output' = None # txo representing purchase receipt for this claim + self.claims: List['Output'] = None # resolved claims for collection self.meta = {} def update_annotations(self, annotated): diff --git a/lbry/tests/integration/test_claim_commands.py b/lbry/tests/integration/test_claim_commands.py index b9497746e..6136cb097 100644 --- a/lbry/tests/integration/test_claim_commands.py +++ b/lbry/tests/integration/test_claim_commands.py @@ -1257,3 +1257,55 @@ class SupportCommands(CommandTestCase): self.assertFalse(txs2[0]['support_info'][0]['is_tip']) self.assertEqual(txs2[0]['value'], '0.0') self.assertEqual(txs2[0]['fee'], '-0.0001415') + + +class CollectionCommands(CommandTestCase): + + async def test_collections(self): + claim_ids = [ + self.get_claim_id(tx) for tx in [ + await self.stream_create('stream-one'), + await self.stream_create('stream-two') + ] + ] + claim_ids.append(claim_ids[0]) + claim_ids.append('beef') + tx = await self.collection_create('radjingles', claims=claim_ids, title="boring title") + claim_id = self.get_claim_id(tx) + collections = await self.out(self.daemon.jsonrpc_collection_list()) + self.assertEqual(collections['items'][0]['value']['title'], 'boring title') + self.assertEqual(collections['items'][0]['value']['claims'], claim_ids) + + self.assertItemCount(collections, 1) + await self.assertBalance(self.account, '6.939679') + + with self.assertRaisesRegex(Exception, "You already have a collection under the name 'radjingles'."): + await self.collection_create('radjingles', claims=claim_ids) + + self.assertItemCount(await self.daemon.jsonrpc_collection_list(), 1) + await self.assertBalance(self.account, '6.939679') + + collections = await self.out(self.daemon.jsonrpc_collection_list()) + self.assertEqual(collections['items'][0]['value']['title'], 'boring title') + await self.collection_update(claim_id, title='fancy title') + collections = await self.out(self.daemon.jsonrpc_collection_list()) + self.assertEqual(collections['items'][0]['value']['title'], 'fancy title') + self.assertEqual(collections['items'][0]['value']['claims'], claim_ids) + self.assertNotIn('claims', collections['items'][0]) + + await self.collection_create('radjingles', claims=claim_ids, allow_duplicate_name=True) + self.assertItemCount(await self.daemon.jsonrpc_collection_list(), 2) + + await self.collection_abandon(claim_id) + self.assertItemCount(await self.daemon.jsonrpc_collection_list(), 1) + + collections = await self.out(self.daemon.jsonrpc_collection_list(resolve_claims=True)) + self.assertEqual(collections['items'][0]['claims'][0]['name'], 'stream-one') + self.assertEqual(collections['items'][0]['claims'][1]['name'], 'stream-two') + self.assertEqual(collections['items'][0]['claims'][2]['name'], 'stream-one') + self.assertIsNone(collections['items'][0]['claims'][3]) + + claims = await self.out(self.daemon.jsonrpc_claim_list()) + self.assertEqual(claims['items'][0]['name'], 'radjingles') + self.assertEqual(claims['items'][1]['name'], 'stream-two') + self.assertEqual(claims['items'][2]['name'], 'stream-one') diff --git a/lbry/tests/unit/schema/test_models.py b/lbry/tests/unit/schema/test_models.py index 2b1bfaf53..40576b708 100644 --- a/lbry/tests/unit/schema/test_models.py +++ b/lbry/tests/unit/schema/test_models.py @@ -1,7 +1,7 @@ from unittest import TestCase from decimal import Decimal -from lbry.schema.claim import Claim, Stream +from lbry.schema.claim import Claim, Stream, Collection class TestClaimContainerAwareness(TestCase): @@ -118,6 +118,26 @@ class TestTags(TestCase): self.assertEqual(claim.channel.tags, ['anime']) +class TestCollection(TestCase): + + def test_collection(self): + collection = Collection() + + collection.update(claims=['abc123', 'def123']) + self.assertListEqual(collection.claims.ids, ['abc123', 'def123']) + + collection.update(claims=['abc123', 'bbb123']) + self.assertListEqual(collection.claims.ids, ['abc123', 'def123', 'abc123', 'bbb123']) + + collection.update(clear_claims=True, claims=['bbb987', 'bb']) + self.assertListEqual(collection.claims.ids, ['bbb987', 'bb']) + + self.assertEqual(collection.to_dict(), {'claims': ['bbb987', 'bb']}) + + collection.update(clear_claims=True) + self.assertListEqual(collection.claims.ids, []) + + class TestLocations(TestCase): def test_location_successful_parsing(self):