diff --git a/lbrynet/extras/daemon/Daemon.py b/lbrynet/extras/daemon/Daemon.py index e88ee5e5b..2b5496a83 100644 --- a/lbrynet/extras/daemon/Daemon.py +++ b/lbrynet/extras/daemon/Daemon.py @@ -114,6 +114,7 @@ def encode_pagination_doc(items): "page": "Page number of the current items.", "page_size": "Number of items to show on a page.", "total_pages": "Total number of pages.", + "total_items": "Total number of items.", "items": [items], } @@ -125,9 +126,11 @@ async def maybe_paginate(get_records: Callable, get_record_count: Callable, "offset": page_size * (page-1), "limit": page_size }) + total_items = await get_record_count(**constraints) return { "items": await get_records(**constraints), - "total_pages": int(((await get_record_count(**constraints)) + (page_size-1)) / page_size), + "total_pages": int((total_items + (page_size-1)) / page_size), + "total_items": total_items, "page": page, "page_size": page_size } return await get_records(**constraints) @@ -1684,47 +1687,53 @@ class Daemon(metaclass=JSONRPCServerType): """ Search for stream and channel claims on the blockchain. - Use --channel_id= to list all stream claims in a channel. - Arguments marked with "supports equality constraints" allow prepending the value with an equality constraint such as '>', '>=', '<' and '<=' eg. --height=">400000" would limit results to only claims above 400k block height. Usage: - claim_search [ | --name=] [--claim_id=] [--txid= --nout=] - [--channel_id=] [--channel_name=] [--is_controlling] - [--order_by=...] - [--height=] [--publish_time=] [--release_time=] + claim_search [ | --name=] [--claim_id=] [--txid=] [--nout=] + [--channel= | --channel_ids=...] [--is_channel_signature_valid] + [--is_controlling] [--release_time=] + [--timestamp=] [--creation_timestamp=] + [--height=] [--creation_height=] + [--activation_height=] [--expiration_height=] [--amount=] [--effective_amount=] [--support_amount=] [--trending_group=] [--trending_mixed=] [--trending_local=] - [--trending_global=] + [--trending_global=...] [--all_tags=...] [--not_tags=...] [--any_languages=...] [--all_languages=...] [--not_languages=...] [--any_locations=...] [--all_locations=...] [--not_locations=...] - [--page=] [--page_size=] + [--order_by=...] [--page=] [--page_size=] Options: - --name= : (str) find claims with this name - --claim_id= : (str) find a claim with this claim_id - --txid= : (str) find a claim with this txid:nout - --nout= : (str) find a claim with this txid:nout - --channel_id= : (str) limit search to specific channel claim id (returns stream claims) - --channel_name= : (str) limit search to specific channel name (returns stream claims) - --is_controlling : (bool) limit to controlling claims for their respective name - --order_by= : (str) field to order by, default is descending order, to do an - ascending order prepend ^ to the field name, eg. '^amount' - available fields: 'name', 'height', 'release_time', - 'publish_time', 'amount', 'effective_amount', - 'support_amount', 'trending_group', 'trending_mixed', - 'trending_local', 'trending_global', 'activation_height' - --height= : (int) limit by block height (supports equality constraints) - --activation_height=: (int) height at which claim starts competing for name - (supports equality constraints) - --publish_time= : (int) limit by UTC timestamp of containing block (supports - equality constraints) + --name= : (str) claim name (normalized) + --claim_id= : (str) full or partial claim id + --txid= : (str) transaction id + --nout= : (str) position in the transaction + --channel= : (str) claims signed by this channel (argument is + a URL which automatically gets resolved), + see --channel_ids if you need to filter by + multiple channels at the same time, + includes claims with invalid signatures, + use in conjunction with --is_channel_signature_valid + --channel_ids= : (str) claims signed by any of these channels + (arguments must be claim ids of the channels), + includes claims with invalid signatures, + use in conjunction with --is_channel_signature_valid + --is_channel_signature_valid : (bool) only return claims with valid channel signatures + --is_controlling : (bool) only return winning claims of their respective name + --height= : (int) last updated block height (supports equality constraints) + --timestamp= : (int) last updated timestamp (supports equality constraints) + --creation_height= : (int) created at block height (supports equality constraints) + --creation_timestamp=: (int) created at timestamp (supports equality constraints) + --activation_height= : (int) height at which claim starts competing for name + (supports equality constraints) + --expiration_height= : (int) height at which claim will expire + (supports equality constraints) --release_time= : (int) limit to claims self-described as having been released to the public on or after this UTC timestamp, when claim does not provide @@ -1767,6 +1776,12 @@ class Daemon(metaclass=JSONRPCServerType): --not_locations= : (list) find claims not containing any of these locations --page= : (int) page to return during paginating --page_size= : (int) number of items on page during pagination + --order_by= : (str) field to order by, default is descending order, to do an + ascending order prepend ^ to the field name, eg. '^amount' + available fields: 'name', 'height', 'release_time', + 'publish_time', 'amount', 'effective_amount', + 'support_amount', 'trending_group', 'trending_mixed', + 'trending_local', 'trending_global', 'activation_height' Returns: {Paginated[Output]} """ @@ -1775,7 +1790,8 @@ class Daemon(metaclass=JSONRPCServerType): txos, offset, total = await self.ledger.claim_search(**kwargs) return { "items": txos, "page": page_num, "page_size": page_size, - "total_pages": int((total + (page_size-1)) / page_size) + "total_pages": int((total + (page_size-1)) / page_size), + "total_items": total } CHANNEL_DOC = """ diff --git a/lbrynet/extras/daemon/json_response_encoder.py b/lbrynet/extras/daemon/json_response_encoder.py index 4c415242b..5862e71d0 100644 --- a/lbrynet/extras/daemon/json_response_encoder.py +++ b/lbrynet/extras/daemon/json_response_encoder.py @@ -169,7 +169,7 @@ class JSONResponseEncoder(JSONEncoder): if txo.script.is_claim_involved: output.update({ 'name': txo.claim_name, - 'normalized': txo.normalized_name, + 'normalized_name': txo.normalized_name, 'claim_id': txo.claim_id, 'permanent_url': txo.permanent_url, 'meta': self.encode_claim_meta(txo.meta) @@ -199,6 +199,8 @@ class JSONResponseEncoder(JSONEncoder): if key.endswith('_amount'): if isinstance(value, int): meta[key] = dewies_to_lbc(value) + if meta.get('creation_height', 0) > 0: + meta['creation_timestamp'] = self.ledger.headers[meta['creation_height']]['timestamp'] return meta def encode_input(self, txi): diff --git a/lbrynet/schema/result.py b/lbrynet/schema/result.py index 81e48772f..f33931991 100644 --- a/lbrynet/schema/result.py +++ b/lbrynet/schema/result.py @@ -4,8 +4,6 @@ from typing import List from binascii import hexlify from itertools import chain -from google.protobuf.message import DecodeError - from lbrynet.schema.types.v2.result_pb2 import Outputs as OutputsMessage @@ -36,6 +34,7 @@ class Outputs: 'short_url': f'lbry://{claim.short_url}', 'canonical_url': f'lbry://{claim.canonical_url or claim.short_url}', 'is_controlling': claim.is_controlling, + 'creation_height': claim.creation_height, 'activation_height': claim.activation_height, 'expiration_height': claim.expiration_height, 'effective_amount': claim.effective_amount, @@ -47,8 +46,11 @@ class Outputs: } if claim.HasField('channel'): txo.channel = tx_map[claim.channel.tx_hash].outputs[claim.channel.nout] - if claim.claims_in_channel is not None: - txo.meta['claims_in_channel'] = claim.claims_in_channel + try: + if txo.claim.is_channel: + txo.meta['claims_in_channel'] = claim.claims_in_channel + except: + pass return txo @classmethod @@ -97,6 +99,7 @@ class Outputs: if txo['canonical_url'] is not None: txo_message.claim.canonical_url = txo['canonical_url'] txo_message.claim.is_controlling = bool(txo['is_controlling']) + txo_message.claim.creation_height = txo['creation_height'] txo_message.claim.activation_height = txo['activation_height'] txo_message.claim.expiration_height = txo['expiration_height'] if txo['claims_in_channel'] is not None: diff --git a/lbrynet/schema/types/v2/result_pb2.py b/lbrynet/schema/types/v2/result_pb2.py index bb5638c59..b23d8ebd7 100644 --- a/lbrynet/schema/types/v2/result_pb2.py +++ b/lbrynet/schema/types/v2/result_pb2.py @@ -19,7 +19,7 @@ DESCRIPTOR = _descriptor.FileDescriptor( package='pb', syntax='proto3', serialized_options=None, - serialized_pb=_b('\n\x0cresult.proto\x12\x02pb\"b\n\x07Outputs\x12\x18\n\x04txos\x18\x01 \x03(\x0b\x32\n.pb.Output\x12\x1e\n\nextra_txos\x18\x02 \x03(\x0b\x32\n.pb.Output\x12\r\n\x05total\x18\x03 \x01(\r\x12\x0e\n\x06offset\x18\x04 \x01(\r\"{\n\x06Output\x12\x0f\n\x07tx_hash\x18\x01 \x01(\x0c\x12\x0c\n\x04nout\x18\x02 \x01(\r\x12\x0e\n\x06height\x18\x03 \x01(\r\x12\x1e\n\x05\x63laim\x18\x07 \x01(\x0b\x32\r.pb.ClaimMetaH\x00\x12\x1a\n\x05\x65rror\x18\x0f \x01(\x0b\x32\t.pb.ErrorH\x00\x42\x06\n\x04meta\"\xce\x02\n\tClaimMeta\x12\x1b\n\x07\x63hannel\x18\x01 \x01(\x0b\x32\n.pb.Output\x12\x11\n\tshort_url\x18\x02 \x01(\t\x12\x15\n\rcanonical_url\x18\x03 \x01(\t\x12\x16\n\x0eis_controlling\x18\x04 \x01(\x08\x12\x19\n\x11\x61\x63tivation_height\x18\x05 \x01(\r\x12\x19\n\x11\x65xpiration_height\x18\x06 \x01(\r\x12\x19\n\x11\x63laims_in_channel\x18\x07 \x01(\r\x12\x18\n\x10\x65\x66\x66\x65\x63tive_amount\x18\n \x01(\x04\x12\x16\n\x0esupport_amount\x18\x0b \x01(\x04\x12\x16\n\x0etrending_group\x18\x0c \x01(\r\x12\x16\n\x0etrending_mixed\x18\r \x01(\x02\x12\x16\n\x0etrending_local\x18\x0e \x01(\x02\x12\x17\n\x0ftrending_global\x18\x0f \x01(\x02\"i\n\x05\x45rror\x12\x1c\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x0e.pb.Error.Code\x12\x0c\n\x04text\x18\x02 \x01(\t\"4\n\x04\x43ode\x12\x10\n\x0cUNKNOWN_CODE\x10\x00\x12\r\n\tNOT_FOUND\x10\x01\x12\x0b\n\x07INVALID\x10\x02\x62\x06proto3') + serialized_pb=_b('\n\x0cresult.proto\x12\x02pb\"b\n\x07Outputs\x12\x18\n\x04txos\x18\x01 \x03(\x0b\x32\n.pb.Output\x12\x1e\n\nextra_txos\x18\x02 \x03(\x0b\x32\n.pb.Output\x12\r\n\x05total\x18\x03 \x01(\r\x12\x0e\n\x06offset\x18\x04 \x01(\r\"{\n\x06Output\x12\x0f\n\x07tx_hash\x18\x01 \x01(\x0c\x12\x0c\n\x04nout\x18\x02 \x01(\r\x12\x0e\n\x06height\x18\x03 \x01(\r\x12\x1e\n\x05\x63laim\x18\x07 \x01(\x0b\x32\r.pb.ClaimMetaH\x00\x12\x1a\n\x05\x65rror\x18\x0f \x01(\x0b\x32\t.pb.ErrorH\x00\x42\x06\n\x04meta\"\xe7\x02\n\tClaimMeta\x12\x1b\n\x07\x63hannel\x18\x01 \x01(\x0b\x32\n.pb.Output\x12\x11\n\tshort_url\x18\x02 \x01(\t\x12\x15\n\rcanonical_url\x18\x03 \x01(\t\x12\x16\n\x0eis_controlling\x18\x04 \x01(\x08\x12\x17\n\x0f\x63reation_height\x18\x05 \x01(\r\x12\x19\n\x11\x61\x63tivation_height\x18\x06 \x01(\r\x12\x19\n\x11\x65xpiration_height\x18\x07 \x01(\r\x12\x19\n\x11\x63laims_in_channel\x18\x08 \x01(\r\x12\x18\n\x10\x65\x66\x66\x65\x63tive_amount\x18\n \x01(\x04\x12\x16\n\x0esupport_amount\x18\x0b \x01(\x04\x12\x16\n\x0etrending_group\x18\x0c \x01(\r\x12\x16\n\x0etrending_mixed\x18\r \x01(\x02\x12\x16\n\x0etrending_local\x18\x0e \x01(\x02\x12\x17\n\x0ftrending_global\x18\x0f \x01(\x02\"i\n\x05\x45rror\x12\x1c\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x0e.pb.Error.Code\x12\x0c\n\x04text\x18\x02 \x01(\t\"4\n\x04\x43ode\x12\x10\n\x0cUNKNOWN_CODE\x10\x00\x12\r\n\tNOT_FOUND\x10\x01\x12\x0b\n\x07INVALID\x10\x02\x62\x06proto3') ) @@ -45,8 +45,8 @@ _ERROR_CODE = _descriptor.EnumDescriptor( ], containing_type=None, serialized_options=None, - serialized_start=635, - serialized_end=687, + serialized_start=660, + serialized_end=712, ) _sym_db.RegisterEnumDescriptor(_ERROR_CODE) @@ -201,63 +201,70 @@ _CLAIMMETA = _descriptor.Descriptor( is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='activation_height', full_name='pb.ClaimMeta.activation_height', index=4, + name='creation_height', full_name='pb.ClaimMeta.creation_height', index=4, number=5, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='expiration_height', full_name='pb.ClaimMeta.expiration_height', index=5, + name='activation_height', full_name='pb.ClaimMeta.activation_height', index=5, number=6, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='claims_in_channel', full_name='pb.ClaimMeta.claims_in_channel', index=6, + name='expiration_height', full_name='pb.ClaimMeta.expiration_height', index=6, number=7, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='effective_amount', full_name='pb.ClaimMeta.effective_amount', index=7, + name='claims_in_channel', full_name='pb.ClaimMeta.claims_in_channel', index=7, + number=8, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='effective_amount', full_name='pb.ClaimMeta.effective_amount', index=8, number=10, type=4, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='support_amount', full_name='pb.ClaimMeta.support_amount', index=8, + name='support_amount', full_name='pb.ClaimMeta.support_amount', index=9, number=11, type=4, cpp_type=4, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='trending_group', full_name='pb.ClaimMeta.trending_group', index=9, + name='trending_group', full_name='pb.ClaimMeta.trending_group', index=10, number=12, type=13, cpp_type=3, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='trending_mixed', full_name='pb.ClaimMeta.trending_mixed', index=10, + name='trending_mixed', full_name='pb.ClaimMeta.trending_mixed', index=11, number=13, type=2, cpp_type=6, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='trending_local', full_name='pb.ClaimMeta.trending_local', index=11, + name='trending_local', full_name='pb.ClaimMeta.trending_local', index=12, number=14, type=2, cpp_type=6, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( - name='trending_global', full_name='pb.ClaimMeta.trending_global', index=12, + name='trending_global', full_name='pb.ClaimMeta.trending_global', index=13, number=15, type=2, cpp_type=6, label=1, has_default_value=False, default_value=float(0), message_type=None, enum_type=None, containing_type=None, @@ -276,7 +283,7 @@ _CLAIMMETA = _descriptor.Descriptor( oneofs=[ ], serialized_start=246, - serialized_end=580, + serialized_end=605, ) @@ -314,8 +321,8 @@ _ERROR = _descriptor.Descriptor( extension_ranges=[], oneofs=[ ], - serialized_start=582, - serialized_end=687, + serialized_start=607, + serialized_end=712, ) _OUTPUTS.fields_by_name['txos'].message_type = _OUTPUT diff --git a/lbrynet/wallet/server/db.py b/lbrynet/wallet/server/db.py index f6e11f0c9..542129525 100644 --- a/lbrynet/wallet/server/db.py +++ b/lbrynet/wallet/server/db.py @@ -109,7 +109,7 @@ class SQLDB: create index if not exists claim_id_idx on claim (claim_id); create index if not exists claim_normalized_idx on claim (normalized); - create index if not exists claim_search_idx on claim (normalized, claim_id); + create index if not exists claim_resolve_idx on claim (normalized, claim_id); create index if not exists claim_txo_hash_idx on claim (txo_hash); create index if not exists claim_channel_hash_idx on claim (channel_hash); create index if not exists claim_release_time_idx on claim (release_time); @@ -352,8 +352,8 @@ class SQLDB: 'support', {'txo_hash__in': [sqlite3.Binary(txo_hash) for txo_hash in txo_hashes]} )) - def validate_channel_signatures(self, height, new_claims, updated_claims): - if not new_claims and not updated_claims: + def validate_channel_signatures(self, height, new_claims, updated_claims, spent_claims): + if not new_claims and not updated_claims and not spent_claims: return channels, new_channel_keys, signables = {}, {}, {} @@ -456,6 +456,14 @@ class SQLDB: WHERE claim_hash=:claim_hash; """, claim_updates) + if spent_claims: + self.execute( + f""" + UPDATE claim SET is_channel_signature_valid=0, channel_join=NULL, canonical_url=NULL + WHERE channel_hash IN ({','.join('?' for _ in spent_claims)}) + """, [sqlite3.Binary(cid) for cid in spent_claims] + ) + if channels: self.db.executemany( "UPDATE claim SET public_key_bytes=:public_key_bytes WHERE claim_hash=:claim_hash", [{ @@ -620,7 +628,7 @@ class SQLDB: r(self.delete_supports, delete_support_txo_hashes) r(self.insert_claims, insert_claims, header) r(self.update_claims, update_claims, header) - r(self.validate_channel_signatures, height, insert_claims, update_claims) + r(self.validate_channel_signatures, height, insert_claims, update_claims, delete_claim_hashes) r(self.insert_supports, insert_supports) r(self.update_claimtrie, height, recalculate_claim_hashes, deleted_claim_names, forward_timer=True) r(calculate_trending, self.db, height, self.main.first_sync, daemon_height) @@ -670,21 +678,25 @@ class SQLDB: constraints['claim.claim_id'] = claim_id else: constraints['claim.claim_id__like'] = f'{claim_id[:40]}%' + if 'name' in constraints: constraints['claim.normalized'] = normalize_name(constraints.pop('name')) if 'channel' in constraints: - url = URL.parse(constraints.pop('channel')) - if url.channel.claim_id: - constraints['channel_id'] = url.channel.claim_id + channel_url = constraints.pop('channel') + match = self._resolve_one(channel_url) + if isinstance(match, sqlite3.Row): + constraints['channel_hash'] = match['claim_hash'] else: - constraints['channel_name'] = url.channel.name - if 'channel_id' in constraints: - constraints['channel_hash'] = unhexlify(constraints.pop('channel_id'))[::-1] + raise LookupError(f'Could not resolve channel "{channel_url}".') if 'channel_hash' in constraints: - constraints['channel.claim_hash'] = sqlite3.Binary(constraints.pop('channel_hash')) - if 'channel_name' in constraints: - constraints['channel.normalized'] = normalize_name(constraints.pop('channel_name')) + constraints['claim.channel_hash'] = sqlite3.Binary(constraints.pop('channel_hash')) + if 'channel_ids' in constraints: + channel_ids = constraints.pop('channel_ids') + if channel_ids: + constraints['claim.channel_hash__in'] = [ + sqlite3.Binary(unhexlify(cid)[::-1]) for cid in channel_ids + ] if 'txid' in constraints: tx_hash = unhexlify(constraints.pop('txid'))[::-1] @@ -697,23 +709,17 @@ class SQLDB: _apply_constraints_for_array_attributes(constraints, 'language') _apply_constraints_for_array_attributes(constraints, 'location') - try: - return self.db.execute(*query( - f""" - SELECT {cols} FROM claim - LEFT JOIN claimtrie USING (claim_hash) - LEFT JOIN claim as channel ON (claim.channel_hash=channel.claim_hash) - """, **constraints - )).fetchall() - except: - self.logger.exception('Failed to execute claim search query:') - print(query( - f""" + sql, values = query( + f""" SELECT {cols} FROM claim LEFT JOIN claimtrie USING (claim_hash) LEFT JOIN claim as channel ON (claim.channel_hash=channel.claim_hash) """, **constraints - )) + ) + try: + return self.db.execute(sql, values).fetchall() + except: + self.logger.exception(f'Failed to execute claim search query: {sql}') raise def get_claims_count(self, **constraints): @@ -727,8 +733,9 @@ class SQLDB: return self.get_claims( """ claimtrie.claim_hash as is_controlling, - claim.claim_hash, claim.txo_hash, claim.height, + claim.claim_hash, claim.txo_hash, claim.is_channel, claim.claims_in_channel, + claim.height, claim.creation_height, claim.activation_height, claim.expiration_height, claim.effective_amount, claim.support_amount, claim.trending_group, claim.trending_mixed, @@ -740,16 +747,16 @@ class SQLDB: ) INTEGER_PARAMS = { - 'height', 'creation_height', 'activation_height', 'tx_position', - 'release_time', 'timestamp', 'is_channel_signature_valid', 'channel_join', + 'height', 'creation_height', 'activation_height', 'expiration_height', + 'timestamp', 'creation_timestamp', 'release_time', + 'tx_position', 'channel_join', 'is_channel_signature_valid', 'amount', 'effective_amount', 'support_amount', 'trending_group', 'trending_mixed', 'trending_local', 'trending_global', } SEARCH_PARAMS = { - 'name', 'claim_id', 'txid', 'nout', - 'channel', 'channel_id', 'channel_name', + 'name', 'claim_id', 'txid', 'nout', 'channel', 'channel_ids', 'any_tags', 'all_tags', 'not_tags', 'any_locations', 'all_locations', 'not_locations', 'any_languages', 'all_languages', 'not_languages', @@ -775,50 +782,54 @@ class SQLDB: extra_txo_rows = self._search(**{'claim.claim_hash__in': [sqlite3.Binary(h) for h in channel_hashes]}) return txo_rows, extra_txo_rows, constraints['offset'], total + def _resolve_one(self, raw_url): + try: + url = URL.parse(raw_url) + except ValueError as e: + return e + + channel = None + + if url.has_channel: + query = url.channel.to_dict() + if set(query) == {'name'}: + query['is_controlling'] = True + else: + query['order_by'] = ['^height'] + matches = self._search(**query, limit=1) + if matches: + channel = matches[0] + else: + return LookupError(f'Could not find channel in "{raw_url}".') + + if url.has_stream: + query = url.stream.to_dict() + if channel is not None: + if set(query) == {'name'}: + # temporarily emulate is_controlling for claims in channel + query['order_by'] = ['effective_amount'] + else: + query['order_by'] = ['^channel_join'] + query['channel_hash'] = channel['claim_hash'] + query['is_channel_signature_valid'] = 1 + elif set(query) == {'name'}: + query['is_controlling'] = 1 + matches = self._search(**query, limit=1) + if matches: + return matches[0] + else: + return LookupError(f'Could not find stream in "{raw_url}".') + + return channel + def resolve(self, urls) -> Tuple[List, List]: result = [] channel_hashes = set() for raw_url in urls: - try: - url = URL.parse(raw_url) - except ValueError as e: - result.append(e) - continue - channel = None - if url.has_channel: - query = url.channel.to_dict() - if set(query) == {'name'}: - query['is_controlling'] = True - else: - query['order_by'] = ['^height'] - matches = self._search(**query, limit=1) - if matches: - channel = matches[0] - else: - result.append(LookupError(f'Could not find channel in "{raw_url}".')) - continue - if url.has_stream: - query = url.stream.to_dict() - if channel is not None: - if set(query) == {'name'}: - # temporarily emulate is_controlling for claims in channel - query['order_by'] = ['effective_amount'] - else: - query['order_by'] = ['^channel_join'] - query['channel_hash'] = channel['claim_hash'] - query['is_channel_signature_valid'] = 1 - elif set(query) == {'name'}: - query['is_controlling'] = True - matches = self._search(**query, limit=1) - if matches: - result.append(matches[0]) - if matches[0]['channel_hash']: - channel_hashes.add(matches[0]['channel_hash']) - else: - result.append(LookupError(f'Could not find stream in "{raw_url}".')) - continue - else: - result.append(channel) + match = self._resolve_one(raw_url) + result.append(match) + if isinstance(match, sqlite3.Row) and match['channel_hash']: + channel_hashes.add(match['channel_hash']) extra_txo_rows = [] if channel_hashes: extra_txo_rows = self._search(**{'claim.claim_hash__in': [sqlite3.Binary(h) for h in channel_hashes]}) diff --git a/tests/integration/test_claim_commands.py b/tests/integration/test_claim_commands.py index 2f74024d0..352bee622 100644 --- a/tests/integration/test_claim_commands.py +++ b/tests/integration/test_claim_commands.py @@ -68,8 +68,9 @@ class ClaimSearchCommand(CommandTestCase): # finding claims with and without a channel await self.assertFindsClaims([signed2, signed], name='on-channel-claim') - await self.assertFindsClaim(signed, name='on-channel-claim', channel_id=self.channel_id) - await self.assertFindsClaim(signed2, name='on-channel-claim', channel_id=channel_id2) + await self.assertFindsClaims([signed2, signed], channel_ids=[self.channel_id, channel_id2]) + await self.assertFindsClaim(signed, name='on-channel-claim', channel_ids=[self.channel_id]) + await self.assertFindsClaim(signed2, name='on-channel-claim', channel_ids=[channel_id2]) await self.assertFindsClaim(unsigned, name='unsigned') await self.assertFindsClaim(unsigned, txid=unsigned['txid'], nout=0) await self.assertFindsClaim(unsigned, claim_id=unsigned['outputs'][0]['claim_id']) @@ -79,37 +80,36 @@ class ClaimSearchCommand(CommandTestCase): # three streams in channel, zero streams in abandoned channel claims = [three, two, signed] - await self.assertFindsClaims(claims, channel_id=self.channel_id) - await self.assertFindsClaims([three, two, signed2, signed], channel_name="@abc") - await self.assertFindsClaims(claims, channel_name="@abc", channel_id=self.channel_id) + await self.assertFindsClaims(claims, channel_ids=[self.channel_id]) await self.assertFindsClaims(claims, channel=f"@abc#{self.channel_id}") + await self.assertFindsClaims([three, two, signed2, signed], channel_ids=[channel_id2, self.channel_id]) await self.channel_abandon(claim_id=self.channel_id) - await self.assertFindsClaims([], channel_id=self.channel_id) - await self.assertFindsClaims([signed2], channel_name="@abc") - await self.assertFindsClaims([], channel_name="@abc", channel_id=self.channel_id) - await self.assertFindsClaims([], channel=f"@abc#{self.channel_id}") + await self.assertFindsClaims([], channel_ids=[self.channel_id], is_channel_signature_valid=True) + await self.assertFindsClaims([signed2], channel_ids=[channel_id2], is_channel_signature_valid=True) + await self.assertFindsClaims([signed2], channel_ids=[channel_id2, self.channel_id], + is_channel_signature_valid=True) # abandoned stream won't show up for streams in channel search await self.stream_abandon(txid=signed2['txid'], nout=0) - await self.assertFindsClaims([], channel_name="@abc") + await self.assertFindsClaims([], channel_ids=[channel_id2]) async def test_pagination(self): await self.create_channel() await self.create_lots_of_streams() - page = await self.claim_search(page_size=20, channel_id=self.channel_id) + page = await self.claim_search(page_size=20, channel='@abc') page_claim_ids = [item['name'] for item in page] self.assertEqual(page_claim_ids, self.streams) - page = await self.claim_search(page_size=6, channel_id=self.channel_id) + page = await self.claim_search(page_size=6, channel='@abc') page_claim_ids = [item['name'] for item in page] self.assertEqual(page_claim_ids, self.streams[:6]) - page = await self.claim_search(page=2, page_size=6, channel_id=self.channel_id) + page = await self.claim_search(page=2, page_size=6, channel='@abc') page_claim_ids = [item['name'] for item in page] self.assertEqual(page_claim_ids, self.streams[6:]) - out_of_bounds = await self.claim_search(page=2, page_size=20, channel_id=self.channel_id) + out_of_bounds = await self.claim_search(page=2, page_size=20, channel='@abc') self.assertEqual(out_of_bounds, []) async def test_tag_search(self): diff --git a/tests/unit/wallet/server/test_sqldb.py b/tests/unit/wallet/server/test_sqldb.py index 7122aaff7..d8556575a 100644 --- a/tests/unit/wallet/server/test_sqldb.py +++ b/tests/unit/wallet/server/test_sqldb.py @@ -94,7 +94,7 @@ class TestSQLDB(unittest.TestCase): Input.spend(claim) ) - def get_stream_abandon(self, tx): + def get_abandon(self, tx): claim = Transaction(tx[0].serialize()).outputs[0] return self._make_tx( Output.pay_pubkey_hash(claim.amount, b'abc'), @@ -264,7 +264,7 @@ class TestSQLDB(unittest.TestCase): active=[('Claim A', 10*COIN, 10*COIN, 13)], accepted=[] ) - advance(14, [self.get_stream_abandon(stream2)]) + advance(14, [self.get_abandon(stream2)]) state( controlling=('Claim A', 10*COIN, 10*COIN, 13), active=[], @@ -281,7 +281,7 @@ class TestSQLDB(unittest.TestCase): active=[('Claim A', 10*COIN, 10*COIN, 13)], accepted=[] ) - advance(15, [self.get_stream_abandon(stream2), self.get_stream('Claim C', 12*COIN)]) + advance(15, [self.get_abandon(stream2), self.get_stream('Claim C', 12*COIN)]) state( controlling=('Claim C', 12*COIN, 12*COIN, 15), active=[('Claim A', 10*COIN, 10*COIN, 13)], @@ -380,7 +380,8 @@ class TestSQLDB(unittest.TestCase): self.assertEqual(0, self.sql._search(claim_id=txo_chan_a.claim_id, limit=1)[0]['claims_in_channel']) # reinstate previous channel public key (previous stream claim signatures become valid again) - advance(9, [self.get_channel_update(txo_chan_a, COIN, key=b'c')]) + channel_update = self.get_channel_update(txo_chan_a, COIN, key=b'c') + advance(9, [channel_update]) r_ab2, r_a2 = self.sql._search(order_by=['creation_height'], limit=2) self.assertEqual(f"foo#{a2_claim_id[:2]}", r_a2['short_url']) self.assertEqual(f"foo#{ab2_claim_id[:4]}", r_ab2['short_url']) @@ -388,6 +389,14 @@ class TestSQLDB(unittest.TestCase): self.assertEqual("@foo#a/foo#ab", r_ab2['canonical_url']) self.assertEqual(2, self.sql._search(claim_id=txo_chan_a.claim_id, limit=1)[0]['claims_in_channel']) + # delete channel, invaliding stream claim signatures + advance(10, [self.get_abandon(channel_update)]) + r_ab2, r_a2 = self.sql._search(order_by=['creation_height'], limit=2) + self.assertEqual(f"foo#{a2_claim_id[:2]}", r_a2['short_url']) + self.assertEqual(f"foo#{ab2_claim_id[:4]}", r_ab2['short_url']) + self.assertIsNone(r_a2['canonical_url']) + self.assertIsNone(r_ab2['canonical_url']) + def test_canonical_find_shortest_id(self): new_hash = 'abcdef0123456789beef' other0 = '1bcdef0123456789beef'