test_claim_commands.py

This commit is contained in:
Lex Berezhny 2020-11-16 12:31:44 -05:00
parent e6a9417988
commit 1d31a96c9b
18 changed files with 277 additions and 289 deletions

2
.gitignore vendored
View file

@ -11,7 +11,7 @@ lbry.egg-info
__pycache__ __pycache__
_trial_temp/ _trial_temp/
/tests/integration/blockchain/files /tests/integration/commands/files
/tests/.coverage.* /tests/.coverage.*
/lbry/blockchain/bin /lbry/blockchain/bin

View file

@ -1,52 +0,0 @@
from lbry.wallet.database import constraints_to_sql
CREATE_FULL_TEXT_SEARCH = """
create virtual table if not exists search using fts5(
claim_name, channel_name, title, description, author, tags,
content=claim, tokenize=porter
);
"""
FTS_ORDER_BY = "bm25(search, 4.0, 8.0, 1.0, 0.5, 1.0, 0.5)"
def fts_action_sql(claims=None, action='insert'):
select = {
'rowid': "claim.rowid",
'claim_name': "claim.normalized",
'channel_name': "channel.normalized",
'title': "claim.title",
'description': "claim.description",
'author': "claim.author",
'tags': "(select group_concat(tag, ' ') from tag where tag.claim_hash=claim.claim_hash)"
}
if action == 'delete':
select['search'] = '"delete"'
where, values = "", {}
if claims:
where, values = constraints_to_sql({'claim.claim_hash__in': claims})
where = 'WHERE '+where
return f"""
INSERT INTO search ({','.join(select.keys())})
SELECT {','.join(select.values())} FROM claim
LEFT JOIN claim as channel ON (claim.channel_hash=channel.claim_hash) {where}
""", values
def update_full_text_search(action, outputs, db, is_first_sync):
if is_first_sync:
return
if not outputs:
return
if action in ("before-delete", "before-update"):
db.execute(*fts_action_sql(outputs, 'delete'))
elif action in ("after-insert", "after-update"):
db.execute(*fts_action_sql(outputs, 'insert'))
else:
raise ValueError(f"Invalid action for updating full text search: '{action}'")
def first_sync_finished(db):
db.execute(*fts_action_sql())

View file

@ -191,16 +191,16 @@ def select_claims(cols: List = None, for_count=False, **constraints) -> Select:
if 'public_key_id' in constraints: if 'public_key_id' in constraints:
constraints['public_key_hash'] = ( constraints['public_key_hash'] = (
context().ledger.address_to_hash160(constraints.pop('public_key_id'))) context().ledger.address_to_hash160(constraints.pop('public_key_id')))
if 'channel_hash' in constraints: if 'channel_id' in constraints:
constraints['channel_hash'] = constraints.pop('channel_hash') channel_id = constraints.pop('channel_id')
if 'channel_ids' in constraints: if channel_id:
channel_ids = constraints.pop('channel_ids') if isinstance(channel_id, str):
if channel_ids: channel_id = [channel_id]
constraints['channel_hash__in'] = { constraints['channel_hash__in'] = {
unhexlify(cid)[::-1] for cid in channel_ids unhexlify(cid)[::-1] for cid in channel_id
} }
if 'not_channel_ids' in constraints: if 'not_channel_id' in constraints:
not_channel_ids = constraints.pop('not_channel_ids') not_channel_ids = constraints.pop('not_channel_id')
if not_channel_ids: if not_channel_ids:
not_channel_ids_binary = { not_channel_ids_binary = {
unhexlify(ncid)[::-1] for ncid in not_channel_ids unhexlify(ncid)[::-1] for ncid in not_channel_ids
@ -213,17 +213,18 @@ def select_claims(cols: List = None, for_count=False, **constraints) -> Select:
'signature_valid__is_null': True, 'signature_valid__is_null': True,
'channel_hash__not_in': not_channel_ids_binary 'channel_hash__not_in': not_channel_ids_binary
} }
if 'signature_valid' in constraints: if 'is_signature_valid' in constraints:
has_channel_signature = constraints.pop('has_channel_signature', False) has_channel_signature = constraints.pop('has_channel_signature', False)
is_signature_valid = constraints.pop('is_signature_valid')
if has_channel_signature: if has_channel_signature:
constraints['signature_valid'] = constraints.pop('signature_valid') constraints['is_signature_valid'] = is_signature_valid
else: else:
constraints['null_or_signature__or'] = { constraints['null_or_signature__or'] = {
'signature_valid__is_null': True, 'is_signature_valid__is_null': True,
'signature_valid': constraints.pop('signature_valid') 'is_signature_valid': is_signature_valid
} }
elif constraints.pop('has_channel_signature', False): elif constraints.pop('has_channel_signature', False):
constraints['signature_valid__is_not_null'] = True constraints['is_signature_valid__is_not_null'] = True
if 'txid' in constraints: if 'txid' in constraints:
tx_hash = unhexlify(constraints.pop('txid'))[::-1] tx_hash = unhexlify(constraints.pop('txid'))[::-1]
@ -261,7 +262,7 @@ def select_claims(cols: List = None, for_count=False, **constraints) -> Select:
constraints["search"] = constraints.pop("text") constraints["search"] = constraints.pop("text")
return query( return query(
[Claim], [Claim, TXO],
select(*cols) select(*cols)
.select_from( .select_from(
Claim.join(TXO).join(TX) Claim.join(TXO).join(TX)
@ -276,18 +277,47 @@ def protobuf_search_claims(**constraints) -> str:
def search_claims(**constraints) -> Tuple[List[Output], Optional[int], Optional[Censor]]: def search_claims(**constraints) -> Tuple[List[Output], Optional[int], Optional[Censor]]:
ctx = context()
search_censor = ctx.get_search_censor()
total = None total = None
if constraints.pop('include_total', False): if constraints.pop('include_total', False):
total = search_claim_count(**constraints) total = search_claim_count(**constraints)
constraints['offset'] = abs(constraints.get('offset', 0)) constraints['offset'] = abs(constraints.get('offset', 0))
constraints['limit'] = min(abs(constraints.get('limit', 10)), 50) constraints['limit'] = min(abs(constraints.get('limit', 10)), 50)
ctx = context()
search_censor = ctx.get_search_censor() channel_url = constraints.pop('channel', None)
rows = context().fetchall(select_claims(**constraints)) if channel_url:
from .resolve import resolve_url
channel = resolve_url(channel_url)
if isinstance(channel, Output):
constraints['channel_hash'] = channel.claim_hash
else:
return [], total, search_censor
rows = ctx.fetchall(select_claims(**constraints))
txos = rows_to_txos(rows, include_tx=False) txos = rows_to_txos(rows, include_tx=False)
annotate_with_channels(txos)
return txos, total, search_censor return txos, total, search_censor
def annotate_with_channels(txos):
channel_hashes = set()
for txo in txos:
if txo.can_decode_claim and txo.claim.is_signed:
channel_hashes.add(txo.claim.signing_channel_hash)
if channel_hashes:
rows = context().fetchall(select_claims(claim_hash__in=channel_hashes))
channels = {
txo.claim_hash: txo for txo in
rows_to_txos(rows, include_tx=False)
}
for txo in txos:
if txo.can_decode_claim and txo.claim.is_signed:
txo.channel = channels.get(txo.claim.signing_channel_hash, None)
def search_claim_count(**constraints) -> int: def search_claim_count(**constraints) -> int:
constraints.pop('offset', None) constraints.pop('offset', None)
constraints.pop('limit', None) constraints.pop('limit', None)
@ -305,9 +335,9 @@ END
def _apply_constraints_for_array_attributes(constraints, attr, cleaner, for_count=False): def _apply_constraints_for_array_attributes(constraints, attr, cleaner, for_count=False):
any_items = set(cleaner(constraints.pop(f'any_{attr}s', []))[:ATTRIBUTE_ARRAY_MAX_LENGTH]) any_items = set(cleaner(constraints.pop(f'any_{attr}', []))[:ATTRIBUTE_ARRAY_MAX_LENGTH])
all_items = set(cleaner(constraints.pop(f'all_{attr}s', []))[:ATTRIBUTE_ARRAY_MAX_LENGTH]) all_items = set(cleaner(constraints.pop(f'all_{attr}', []))[:ATTRIBUTE_ARRAY_MAX_LENGTH])
not_items = set(cleaner(constraints.pop(f'not_{attr}s', []))[:ATTRIBUTE_ARRAY_MAX_LENGTH]) not_items = set(cleaner(constraints.pop(f'not_{attr}', []))[:ATTRIBUTE_ARRAY_MAX_LENGTH])
all_items = {item for item in all_items if item not in not_items} all_items = {item for item in all_items if item not in not_items}
any_items = {item for item in any_items if item not in not_items} any_items = {item for item in any_items if item not in not_items}

View file

@ -2,7 +2,7 @@ import logging
from datetime import date from datetime import date
from typing import Tuple, List, Optional, Union from typing import Tuple, List, Optional, Union
from sqlalchemy import union, func, text, between, distinct, case from sqlalchemy import union, func, text, between, distinct, case, false
from sqlalchemy.future import select, Select from sqlalchemy.future import select, Select
from ...blockchain.transaction import ( from ...blockchain.transaction import (
@ -372,7 +372,7 @@ def select_txos(
) )
joins = TXO.join(TX) joins = TXO.join(TX)
if constraints.pop('is_spent', None) is False: if constraints.pop('is_spent', None) is False:
s = s.where((TXO.c.spent_height == 0) & (TXO.c.is_reserved == False)) s = s.where((TXO.c.spent_height == 0) & (TXO.c.is_reserved == false()))
if include_is_my_input: if include_is_my_input:
joins = joins.join(TXI, (TXI.c.position == 0) & (TXI.c.tx_hash == TXO.c.tx_hash), isouter=True) joins = joins.join(TXI, (TXI.c.position == 0) & (TXI.c.tx_hash == TXO.c.tx_hash), isouter=True)
if claim_id_not_in_claim_table: if claim_id_not_in_claim_table:
@ -534,7 +534,7 @@ def get_balance(account_ids):
else: else:
txo_address_check = TXO.c.address.in_(my_addresses) txo_address_check = TXO.c.address.in_(my_addresses)
txi_address_check = TXI.c.address.in_(my_addresses) txi_address_check = TXI.c.address.in_(my_addresses)
query = ( s: Select = (
select( select(
func.coalesce(func.sum(TXO.c.amount), 0).label("total"), func.coalesce(func.sum(TXO.c.amount), 0).label("total"),
func.coalesce(func.sum(case( func.coalesce(func.sum(case(
@ -557,7 +557,7 @@ def get_balance(account_ids):
TXO.join(TXI, (TXI.c.position == 0) & (TXI.c.tx_hash == TXO.c.tx_hash), isouter=True) TXO.join(TXI, (TXI.c.position == 0) & (TXI.c.tx_hash == TXO.c.tx_hash), isouter=True)
) )
) )
result = ctx.fetchone(query) result = ctx.fetchone(s)
return { return {
"total": result["total"], "total": result["total"],
"available": result["total"] - result["reserved"], "available": result["total"] - result["reserved"],

View file

@ -1,7 +1,7 @@
from itertools import islice from itertools import islice
from typing import List, Union from typing import List, Union
from sqlalchemy import text, and_ from sqlalchemy import text, and_, or_
from sqlalchemy.sql.expression import Select, FunctionElement from sqlalchemy.sql.expression import Select, FunctionElement
from sqlalchemy.types import Numeric from sqlalchemy.types import Numeric
from sqlalchemy.ext.compiler import compiles from sqlalchemy.ext.compiler import compiles
@ -98,9 +98,7 @@ def query(table, s: Select, **constraints) -> Select:
s = s.where(in_account_ids(account_ids)) s = s.where(in_account_ids(account_ids))
if constraints: if constraints:
s = s.where( s = s.where(and_(*constraints_to_clause(table, constraints)))
constraints_to_clause(table, constraints)
)
return s return s
@ -148,6 +146,9 @@ def constraints_to_clause(tables, constraints):
raise ValueError(f"{col} requires a list, set or string as constraint value.") raise ValueError(f"{col} requires a list, set or string as constraint value.")
else: else:
continue continue
elif key.endswith('__or'):
clause.append(or_(*constraints_to_clause(tables, constraint)))
continue
else: else:
col, op = key, '__eq__' col, op = key, '__eq__'
attr = None attr = None
@ -170,4 +171,4 @@ def constraints_to_clause(tables, constraints):
if attr is None: if attr is None:
raise ValueError(f"Attribute '{col}' not found on tables: {', '.join([t.name for t in tables])}.") raise ValueError(f"Attribute '{col}' not found on tables: {', '.join([t.name for t in tables])}.")
clause.append(getattr(attr, op)(constraint)) clause.append(getattr(attr, op)(constraint))
return and_(*clause) return clause

View file

@ -1098,8 +1098,9 @@ class API:
raise ValueError("--outputs must be an integer.") raise ValueError("--outputs must be an integer.")
if everything and outputs > 1: if everything and outputs > 1:
raise ValueError("Using --everything along with --outputs is not supported.") raise ValueError("Using --everything along with --outputs is not supported.")
return await from_account.fund( return await wallet.fund(
to_account=to_account, amount=amount, everything=everything, from_account=from_account, to_account=to_account,
amount=amount, everything=everything,
outputs=outputs, broadcast=broadcast outputs=outputs, broadcast=broadcast
) )
@ -1518,7 +1519,7 @@ class API:
claim_type: str = None, # claim type: channel, stream, repost, collection claim_type: str = None, # claim type: channel, stream, repost, collection
include_purchase_receipt=False, # lookup and include a receipt if this wallet has purchased the claim include_purchase_receipt=False, # lookup and include a receipt if this wallet has purchased the claim
include_is_my_output=False, # lookup and include a boolean indicating if claim being resolved is yours include_is_my_output=False, # lookup and include a boolean indicating if claim being resolved is yours
is_controlling=False, # winning claims of their respective name is_controlling: bool = None, # winning claims of their respective name
activation_height: int = None, # height at which claim starts competing for name activation_height: int = None, # height at which claim starts competing for name
# (supports equality constraints) # (supports equality constraints)
expiration_height: int = None, # height at which claim will expire (supports equality constraints) expiration_height: int = None, # height at which claim will expire (supports equality constraints)
@ -1578,16 +1579,23 @@ class API:
claim_filter_dict, kwargs = pop_kwargs('claim_filter', extract_claim_filter( claim_filter_dict, kwargs = pop_kwargs('claim_filter', extract_claim_filter(
**claim_filter_and_stream_filter_and_pagination_kwargs **claim_filter_and_stream_filter_and_pagination_kwargs
)) ))
stream_filter_dict, kwargs = pop_kwargs('stream_filter', extract_stream_filter(**kwargs))
pagination, kwargs = pop_kwargs('pagination', extract_pagination(**kwargs)) pagination, kwargs = pop_kwargs('pagination', extract_pagination(**kwargs))
assert_consumed_kwargs(kwargs)
wallet = self.wallets.get_or_default(wallet_id) wallet = self.wallets.get_or_default(wallet_id)
# if {'claim_id', 'claim_ids'}.issubset(kwargs): # if {'claim_id', 'claim_ids'}.issubset(kwargs):
# raise ValueError("Only 'claim_id' or 'claim_ids' is allowed, not both.") # raise ValueError("Only 'claim_id' or 'claim_ids' is allowed, not both.")
# if kwargs.pop('valid_channel_signature', False): if stream_filter_dict.pop('valid_channel_signature', False):
# kwargs['signature_valid'] = 1 stream_filter_dict['is_signature_valid'] = True
# if kwargs.pop('invalid_channel_signature', False): if stream_filter_dict.pop('invalid_channel_signature', False):
# kwargs['signature_valid'] = 0 stream_filter_dict['is_signature_valid'] = False
if is_controlling is not None:
claim_filter_dict["is_controlling"] = is_controlling
if public_key_id is not None:
claim_filter_dict["public_key_id"] = public_key_id
page_num = abs(pagination['page'] or 1) page_num = abs(pagination['page'] or 1)
page_size = min(abs(pagination['page_size'] or DEFAULT_PAGE_SIZE), 50) page_size = min(abs(pagination['page_size'] or DEFAULT_PAGE_SIZE), 50)
claim_filter_dict.update(stream_filter_dict)
claim_filter_dict.update({ claim_filter_dict.update({
'offset': page_size * (page_num - 1), 'limit': page_size, 'offset': page_size * (page_num - 1), 'limit': page_size,
'include_total': pagination['include_total'], 'include_total': pagination['include_total'],
@ -1701,31 +1709,18 @@ class API:
{kwargs} {kwargs}
""" """
wallet = self.wallets.get_or_default(wallet_id) abandon_dict, kwargs = pop_kwargs('abandon', extract_abandon(**abandon_and_tx_kwargs))
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first." tx_dict, kwargs = pop_kwargs('tx', extract_tx(**kwargs))
if account_id: assert_consumed_kwargs(kwargs)
account = wallet.get_account_or_error(account_id) wallet = self.wallets.get_or_default_for_spending(tx_dict.pop('wallet_id'))
accounts = [account] funding_accounts = wallet.accounts.get_or_all(tx_dict.pop('fund_account_id'))
else: change_account = wallet.accounts.get_or_default(tx_dict.pop('change_account_id'))
account = wallet.default_account tx = await wallet.channels.delete(
accounts = wallet.accounts claim_id=abandon_dict.pop('claim_id'),
txid=abandon_dict.pop('txid'),
if txid is not None and nout is not None: nout=abandon_dict.pop('nout'),
claims = await self.ledger.get_claims( funding_accounts=funding_accounts,
wallet=wallet, accounts=accounts, tx_hash=unhexlify(txid)[::-1], position=nout change_account=change_account
)
elif claim_id is not None:
claims = await self.ledger.get_claims(
wallet=wallet, accounts=accounts, claim_id=claim_id
)
else:
raise Exception('Must specify claim_id, or txid and nout')
if not claims:
raise Exception('No claim found for the specified claim_id or txid:nout')
tx = await Transaction.create(
[Input.spend(txo) for txo in claims], [], [account], account
) )
await self.service.maybe_broadcast_or_release(tx, **tx_dict) await self.service.maybe_broadcast_or_release(tx, **tx_dict)
return tx return tx
@ -2078,34 +2073,20 @@ class API:
{kwargs} {kwargs}
""" """
wallet = self.wallets.get_or_default(wallet_id) abandon_dict, kwargs = pop_kwargs('abandon', extract_abandon(**abandon_and_tx_kwargs))
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first." tx_dict, kwargs = pop_kwargs('tx', extract_tx(**kwargs))
if account_id: assert_consumed_kwargs(kwargs)
account = wallet.get_account_or_error(account_id) wallet = self.wallets.get_or_default_for_spending(tx_dict.pop('wallet_id'))
accounts = [account] funding_accounts = wallet.accounts.get_or_all(tx_dict.pop('fund_account_id'))
else: change_account = wallet.accounts.get_or_default(tx_dict.pop('change_account_id'))
account = wallet.default_account tx = await wallet.streams.delete(
accounts = wallet.accounts claim_id=abandon_dict.pop('claim_id'),
txid=abandon_dict.pop('txid'),
if txid is not None and nout is not None: nout=abandon_dict.pop('nout'),
claims = await self.ledger.get_claims( funding_accounts=funding_accounts,
wallet=wallet, accounts=accounts, tx_hash=unhexlify(txid)[::-1], position=nout change_account=change_account
)
elif claim_id is not None:
claims = await self.ledger.get_claims(
wallet=wallet, accounts=accounts, claim_id=claim_id
)
else:
raise Exception('Must specify claim_id, or txid and nout')
if not claims:
raise Exception('No claim found for the specified claim_id or txid:nout')
tx = await Transaction.create(
[Input.spend(txo) for txo in claims], [], accounts, account
) )
await self.service.maybe_broadcast_or_release(tx, **tx_dict)
await self.service.maybe_broadcast_or_release(tx, tx_dict)
return tx return tx
async def stream_list( async def stream_list(

View file

@ -1,11 +1,9 @@
import os
import asyncio import asyncio
import logging import logging
from typing import List, Optional, Tuple, NamedTuple, Dict from typing import List, Optional, NamedTuple, Dict
from lbry.db import Database, Result from lbry.db import Database, Result
from lbry.db.constants import TXO_TYPES from lbry.db.constants import TXO_TYPES
from lbry.schema.result import Censor
from lbry.blockchain.transaction import Transaction, Output from lbry.blockchain.transaction import Transaction, Output
from lbry.blockchain.ledger import Ledger from lbry.blockchain.ledger import Ledger
from lbry.wallet import WalletManager from lbry.wallet import WalletManager

View file

@ -177,7 +177,8 @@ class Daemon:
subscribers = self.app["subscriptions"][event_name]["subscribers"] subscribers = self.app["subscriptions"][event_name]["subscribers"]
subscribers.add(web_socket) subscribers.add(web_socket)
def broadcast_event(self, event_name, subscribers, payload): @staticmethod
def broadcast_event(event_name, subscribers, payload):
for web_socket in subscribers: for web_socket in subscribers:
asyncio.create_task(web_socket.send_json({ asyncio.create_task(web_socket.send_json({
'event': event_name, 'payload': payload 'event': event_name, 'payload': payload

View file

@ -1,10 +1,9 @@
import logging import logging
from typing import Optional, List, Dict
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from lbry.blockchain.lbrycrd import Lbrycrd from lbry.blockchain import Ledger, Transaction
from lbry.blockchain.sync import BlockchainSync from lbry.event import BroadcastSubscription
from lbry.blockchain.ledger import Ledger
from lbry.blockchain.transaction import Transaction
from .base import Service, Sync from .base import Service, Sync
from .api import Client as APIClient from .api import Client as APIClient
@ -24,27 +23,17 @@ class NoSync(Sync):
self.on_mempool = client.get_event_stream('blockchain.mempool') self.on_mempool = client.get_event_stream('blockchain.mempool')
self.on_mempool_subscription: Optional[BroadcastSubscription] = None self.on_mempool_subscription: Optional[BroadcastSubscription] = None
async def wait_for_client_ready(self):
await self.client.connect()
async def start(self): async def start(self):
self.db.stop_event.clear() pass
await self.wait_for_client_ready()
self.advance_loop_task = asyncio.create_task(self.advance())
await self.advance_loop_task
await self.client.subscribe()
self.advance_loop_task = asyncio.create_task(self.advance_loop())
self.on_block_subscription = self.on_block.listen(
lambda e: self.on_block_event.set()
)
self.on_mempool_subscription = self.on_mempool.listen(
lambda e: self.on_mempool_event.set()
)
await self.download_filters()
await self.download_headers()
async def stop(self): async def stop(self):
await self.client.disconnect() pass
async def get_block_headers(self, start_height: int, end_height: int = None):
return await self.db.get_block_headers(start_height, end_height)
async def get_best_block_height(self) -> int:
return await self.db.get_best_block_height()
class FullEndpoint(Service): class FullEndpoint(Service):
@ -59,3 +48,36 @@ class FullEndpoint(Service):
f"http://{ledger.conf.full_nodes[0][0]}:{ledger.conf.full_nodes[0][1]}/api" f"http://{ledger.conf.full_nodes[0][0]}:{ledger.conf.full_nodes[0][1]}/api"
) )
self.sync = NoSync(self, self.client) self.sync = NoSync(self, self.client)
async def get_block_headers(self, first, last=None):
return await self.db.get_block_headers(first, last)
async def get_address_filters(self, start_height: int, end_height: int = None, granularity: int = 0):
return await self.db.get_filters(
start_height=start_height, end_height=end_height, granularity=granularity
)
async def search_transactions(self, txids):
tx_hashes = [unhexlify(txid)[::-1] for txid in txids]
return {
hexlify(tx['tx_hash'][::-1]).decode(): hexlify(tx['raw']).decode()
for tx in await self.db.get_transactions(tx_hashes=tx_hashes)
}
async def broadcast(self, tx):
pass
async def wait(self, tx: Transaction, height=-1, timeout=1):
pass
async def resolve(self, urls, **kwargs):
pass
async def search_claims(self, accounts, **kwargs):
pass
async def search_supports(self, accounts, **kwargs):
pass
async def sum_supports(self, claim_hash: bytes, include_channel_content=False) -> List[Dict]:
return await self.db.sum_supports(claim_hash, include_channel_content)

View file

@ -40,7 +40,7 @@ class FullNode(Service):
return 'everything is wonderful' return 'everything is wonderful'
async def get_block_headers(self, first, last=None): async def get_block_headers(self, first, last=None):
return await self.db.get_blocks(first, last) return await self.db.get_block_headers(first, last)
async def get_address_filters(self, start_height: int, end_height: int = None, granularity: int = 0): async def get_address_filters(self, start_height: int, end_height: int = None, granularity: int = 0):
return await self.db.get_filters( return await self.db.get_filters(

View file

@ -300,13 +300,19 @@ class JSONResponseEncoder(JSONEncoder):
output['purchase_receipt'] = self.encode_output(txo.purchase_receipt) output['purchase_receipt'] = self.encode_output(txo.purchase_receipt)
if txo.claim.is_channel: if txo.claim.is_channel:
output['has_signing_key'] = txo.has_private_key output['has_signing_key'] = txo.has_private_key
if check_signature and txo.claim.is_signed: if check_signature and txo.claim.is_signed and 'is_signature_valid' in txo.meta:
if txo.channel is not None: if txo.channel is not None:
output['signing_channel'] = self.encode_output(txo.channel) output['signing_channel'] = self.encode_output(txo.channel)
output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.service.ledger)
else: else:
output['signing_channel'] = {'channel_id': txo.claim.signing_channel_id} output['signing_channel'] = {'channel_id': txo.claim.signing_channel_id}
output['is_channel_signature_valid'] = False output['is_channel_signature_valid'] = txo.meta.get('is_signature_valid', False)
# if check_signature and txo.claim.is_signed:
# if txo.channel is not None:
# output['signing_channel'] = self.encode_output(txo.channel)
# output['is_channel_signature_valid'] = txo.is_signed_by(txo.channel, self.service.ledger)
# else:
# output['signing_channel'] = {'channel_id': txo.claim.signing_channel_id}
# output['is_channel_signature_valid'] = txo.meta.get('is_signature_valid', False)
except DecodeError: except DecodeError:
pass pass
return output return output

View file

@ -1,19 +1,11 @@
import asyncio import asyncio
import logging import logging
from typing import List, Dict from typing import Dict
#from io import StringIO from typing import List, Optional, NamedTuple
#from functools import partial from binascii import unhexlify
#from operator import itemgetter
from collections import defaultdict
#from binascii import hexlify, unhexlify
from typing import List, Optional, DefaultDict, NamedTuple
#from lbry.crypto.hash import double_sha256, sha256
from lbry.tasks import TaskGroup
from lbry.blockchain import Transaction
from lbry.blockchain.block import Block, get_address_filter from lbry.blockchain.block import Block, get_address_filter
from lbry.event import BroadcastSubscription, EventController from lbry.event import BroadcastSubscription
from lbry.wallet.account import AddressManager from lbry.wallet.account import AddressManager
from lbry.blockchain import Ledger, Transaction from lbry.blockchain import Ledger, Transaction
from lbry.db import Database from lbry.db import Database
@ -119,30 +111,31 @@ class FilterManager:
async def download(self): async def download(self):
filters_response = await self.client.get_address_filters(0, 500) filters_response = await self.client.get_address_filters(0, 500)
filters = await filters_response.first filters = await filters_response.first
address = None
address_array = [bytearray(self.client.ledger.address_to_hash160(address))] address_array = [bytearray(self.client.ledger.address_to_hash160(address))]
for filter in filters: for address_filter in filters:
print(filter) print(address_filter)
filter = get_address_filter(unhexlify(filter['filter'])) address_filter = get_address_filter(unhexlify(address_filter['filter']))
print(filter.MatchAny(address_array)) print(address_filter.MatchAny(address_array))
address_array = [ # address_array = [
bytearray(a['address'].encode()) # bytearray(a['address'].encode())
for a in await self.service.db.get_all_addresses() # for a in await self.service.db.get_all_addresses()
] # ]
block_filters = await self.service.get_block_address_filters() # block_filters = await self.service.get_block_address_filters()
for block_hash, block_filter in block_filters.items(): # for block_hash, block_filter in block_filters.items():
bf = get_address_filter(block_filter) # bf = get_address_filter(block_filter)
if bf.MatchAny(address_array): # if bf.MatchAny(address_array):
print(f'match: {block_hash} - {block_filter}') # print(f'match: {block_hash} - {block_filter}')
tx_filters = await self.service.get_transaction_address_filters(block_hash=block_hash) # tx_filters = await self.service.get_transaction_address_filters(block_hash=block_hash)
for txid, tx_filter in tx_filters.items(): # for txid, tx_filter in tx_filters.items():
tf = get_address_filter(tx_filter) # tf = get_address_filter(tx_filter)
if tf.MatchAny(address_array): # if tf.MatchAny(address_array):
print(f' match: {txid} - {tx_filter}') # print(f' match: {txid} - {tx_filter}')
txs = await self.service.search_transactions([txid]) # txs = await self.service.search_transactions([txid])
tx = Transaction(unhexlify(txs[txid])) # tx = Transaction(unhexlify(txs[txid]))
await self.service.db.insert_transaction(tx) # await self.service.db.insert_transaction(tx)
async def get_filters(self, start_height, end_height, granularity): async def get_filters(self, start_height, end_height, granularity):
return await self.client.address_filter( return await self.client.address_filter(
@ -537,4 +530,4 @@ class FastSync(Sync):
# for account in self.accounts: # for account in self.accounts:
# if account.id == details['account']: # if account.id == details['account']:
# return account.address_managers[details['chain']] # return account.address_managers[details['chain']]
# return None # return None

View file

@ -515,6 +515,9 @@ class IntegrationTestCase(AsyncioTestCase):
lambda e: e.tx.hash == tx_hash lambda e: e.tx.hash == tx_hash
) )
async def on_transaction_dict(self, tx):
await self.service.wait(Transaction(unhexlify(tx['hex'])))
def on_address_update(self, address): def on_address_update(self, address):
return self.ledger.on_transaction.where( return self.ledger.on_transaction.where(
lambda e: e.address == address lambda e: e.address == address
@ -605,9 +608,6 @@ class CommandTestCase(IntegrationTestCase):
await self.on_transaction_id(txid, ledger) await self.on_transaction_id(txid, ledger)
return txid return txid
async def on_transaction_dict(self, tx):
await self.ledger.wait(Transaction(unhexlify(tx['hex'])))
@staticmethod @staticmethod
def get_all_addresses(tx): def get_all_addresses(tx):
addresses = set() addresses = set()
@ -672,9 +672,6 @@ class CommandTestCase(IntegrationTestCase):
async def account_remove(self, *args, **kwargs): async def account_remove(self, *args, **kwargs):
return await self.out(self.api.account_remove(*args, **kwargs)) return await self.out(self.api.account_remove(*args, **kwargs))
async def account_send(self, *args, **kwargs):
return await self.out(self.api.account_send(*args, **kwargs))
async def account_balance(self, *args, **kwargs): async def account_balance(self, *args, **kwargs):
return await self.out(self.api.account_balance(*args, **kwargs)) return await self.out(self.api.account_balance(*args, **kwargs))
@ -716,8 +713,6 @@ class CommandTestCase(IntegrationTestCase):
) )
async def stream_abandon(self, *args, confirm=True, **kwargs): async def stream_abandon(self, *args, confirm=True, **kwargs):
if 'blocking' not in kwargs:
kwargs['blocking'] = False
return await self.confirm_and_render( return await self.confirm_and_render(
self.api.stream_abandon(*args, **kwargs), confirm self.api.stream_abandon(*args, **kwargs), confirm
) )
@ -743,8 +738,6 @@ class CommandTestCase(IntegrationTestCase):
) )
async def channel_abandon(self, *args, confirm=True, **kwargs): async def channel_abandon(self, *args, confirm=True, **kwargs):
if 'blocking' not in kwargs:
kwargs['blocking'] = False
return await self.confirm_and_render( return await self.confirm_and_render(
self.api.channel_abandon(*args, **kwargs), confirm self.api.channel_abandon(*args, **kwargs), confirm
) )
@ -762,8 +755,6 @@ class CommandTestCase(IntegrationTestCase):
) )
async def collection_abandon(self, *args, confirm=True, **kwargs): async def collection_abandon(self, *args, confirm=True, **kwargs):
if 'blocking' not in kwargs:
kwargs['blocking'] = False
return await self.confirm_and_render( return await self.confirm_and_render(
self.api.stream_abandon(*args, **kwargs), confirm self.api.stream_abandon(*args, **kwargs), confirm
) )
@ -783,11 +774,6 @@ class CommandTestCase(IntegrationTestCase):
self.api.account_fund(*args, **kwargs), confirm self.api.account_fund(*args, **kwargs), confirm
) )
async def account_send(self, *args, confirm=True, **kwargs):
return await self.confirm_and_render(
self.api.account_send(*args, **kwargs), confirm
)
async def wallet_send(self, *args, confirm=True, **kwargs): async def wallet_send(self, *args, confirm=True, **kwargs):
return await self.confirm_and_render( return await self.confirm_and_render(
self.api.wallet_send(*args, **kwargs), confirm self.api.wallet_send(*args, **kwargs), confirm

View file

@ -4,14 +4,13 @@ import json
import logging import logging
import asyncio import asyncio
import random import random
from functools import partial
from hashlib import sha256 from hashlib import sha256
from typing import Type, Dict, Tuple, Optional, Any, List from typing import Type, Dict, Tuple, Optional, Any, List
import ecdsa import ecdsa
from lbry.constants import COIN from lbry.constants import COIN
from lbry.db import Database, CLAIM_TYPE_CODES, TXO_TYPES from lbry.db import Database
from lbry.db.tables import AccountAddress from lbry.db.tables import AccountAddress
from lbry.blockchain import Ledger from lbry.blockchain import Ledger
from lbry.error import InvalidPasswordError from lbry.error import InvalidPasswordError

View file

@ -6,6 +6,7 @@ import logging
from typing import Optional, Dict from typing import Optional, Dict
from lbry.db import Database from lbry.db import Database
from lbry.blockchain.dewies import dict_values_to_lbc
from .wallet import Wallet from .wallet import Wallet
from .account import SingleKey, HierarchicalDeterministic from .account import SingleKey, HierarchicalDeterministic
@ -106,21 +107,22 @@ class WalletManager:
async def _report_state(self): async def _report_state(self):
try: try:
for account in self.accounts: for wallet in self.wallets.values():
balance = dewies_to_lbc(await account.get_balance(include_claims=True)) for account in wallet.accounts:
_, channel_count = await account.get_channels(limit=1) balance = dict_values_to_lbc(await account.get_balance(include_claims=True))
claim_count = await account.get_claim_count() _, channel_count = await account.get_channels(limit=1)
if isinstance(account.receiving, SingleKey): claim_count = await account.get_claim_count()
log.info("Loaded single key account %s with %s LBC. " if isinstance(account.receiving, SingleKey):
"%d channels, %d certificates and %d claims", log.info("Loaded single key account %s with %s LBC. "
account.id, balance, channel_count, len(account.channel_keys), claim_count) "%d channels, %d certificates and %d claims",
else: account.id, balance, channel_count, len(account.channel_keys), claim_count)
total_receiving = len(await account.receiving.get_addresses()) else:
total_change = len(await account.change.get_addresses()) total_receiving = len(await account.receiving.get_addresses())
log.info("Loaded account %s with %s LBC, %d receiving addresses (gap: %d), " total_change = len(await account.change.get_addresses())
"%d change addresses (gap: %d), %d channels, %d certificates and %d claims. ", log.info("Loaded account %s with %s LBC, %d receiving addresses (gap: %d), "
account.id, balance, total_receiving, account.receiving.gap, total_change, "%d change addresses (gap: %d), %d channels, %d certificates and %d claims. ",
account.change.gap, channel_count, len(account.channel_keys), claim_count) account.id, balance, total_receiving, account.receiving.gap, total_change,
account.change.gap, channel_count, len(account.channel_keys), claim_count)
except Exception as err: except Exception as err:
if isinstance(err, asyncio.CancelledError): # TODO: remove when updated to 3.8 if isinstance(err, asyncio.CancelledError): # TODO: remove when updated to 3.8
raise raise
@ -135,7 +137,7 @@ class WalletStorage:
async def prepare(self): async def prepare(self):
raise NotImplementedError raise NotImplementedError
async def exists(self, walllet_id: str) -> bool: async def exists(self, wallet_id: str) -> bool:
raise NotImplementedError raise NotImplementedError
async def get(self, wallet_id: str) -> Wallet: async def get(self, wallet_id: str) -> Wallet:

View file

@ -8,6 +8,7 @@ from typing import Awaitable, Callable, List, Tuple, Optional, Iterable, Union
from hashlib import sha256 from hashlib import sha256
from operator import attrgetter from operator import attrgetter
from decimal import Decimal from decimal import Decimal
from binascii import unhexlify
from lbry.db import Database, SPENDABLE_TYPE_CODES, Result from lbry.db import Database, SPENDABLE_TYPE_CODES, Result
from lbry.event import EventController from lbry.event import EventController
@ -518,9 +519,10 @@ class ClaimListManager(BaseListManager):
return tx return tx
async def update( async def update(
self, previous_claim: Output, claim: Claim, amount: int, holding_address: str, self, previous_claim: Output, claim: Claim, amount: int, holding_address: str,
funding_accounts: List[Account], change_account: Account, funding_accounts: List[Account], change_account: Account,
signing_channel: Output = None) -> Transaction: signing_channel: Output = None
) -> Transaction:
updated_claim = Output.pay_update_claim_pubkey_hash( updated_claim = Output.pay_update_claim_pubkey_hash(
amount, previous_claim.claim_name, previous_claim.claim_id, amount, previous_claim.claim_name, previous_claim.claim_id,
claim, self.wallet.ledger.address_to_hash160(holding_address) claim, self.wallet.ledger.address_to_hash160(holding_address)
@ -533,18 +535,27 @@ class ClaimListManager(BaseListManager):
[Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account [Input.spend(previous_claim)], [updated_claim], funding_accounts, change_account
) )
async def delete(self, claim_id=None, txid=None, nout=None): async def delete(
self, claim_id=None, txid=None, nout=None,
funding_accounts: List[Account] = None, change_account: Account = None
):
claim = await self.get(claim_id=claim_id, txid=txid, nout=nout) claim = await self.get(claim_id=claim_id, txid=txid, nout=nout)
return await self.wallet.create_transaction( tx = await self.wallet.create_transaction(
[Input.spend(claim)], [], self.wallet._accounts, self.wallet._accounts[0] [Input.spend(claim)], [],
funding_accounts or self.wallet._accounts,
change_account or self.wallet._accounts[0]
) )
await self.wallet.sign(tx)
return tx
async def list(self, **constraints) -> Result[Output]: async def list(self, **constraints) -> Result[Output]:
return await self.wallet.db.get_claims(wallet=self.wallet, **constraints) return await self.wallet.db.get_claims(wallet=self.wallet, **constraints)
async def get(self, claim_id=None, claim_name=None, txid=None, nout=None) -> Output: async def get(self, claim_id=None, claim_name=None, txid=None, nout=None) -> Output:
if txid is not None and nout is not None: if txid is not None and nout is not None:
key, value, constraints = 'txid:nout', f'{txid}:{nout}', {'tx_hash': '', 'position': nout} key, value, constraints = 'txid:nout', f'{txid}:{nout}', {
'tx_hash': unhexlify(txid)[::-1], 'position': nout
}
elif claim_id is not None: elif claim_id is not None:
key, value, constraints = 'id', claim_id, {'claim_id': claim_id} key, value, constraints = 'id', claim_id, {'claim_id': claim_id}
elif claim_name is not None: elif claim_name is not None:

View file

@ -2,6 +2,7 @@ import os.path
import tempfile import tempfile
import logging import logging
import asyncio import asyncio
from unittest import skip
from binascii import unhexlify from binascii import unhexlify
from urllib.request import urlopen from urllib.request import urlopen
@ -71,6 +72,7 @@ class ClaimSearchCommand(ClaimTestCase):
(result['txid'], result['claim_id']) (result['txid'], result['claim_id'])
) )
@skip
async def test_disconnect_on_memory_error(self): async def test_disconnect_on_memory_error(self):
claim_ids = [ claim_ids = [
'0000000000000000000000000000000000000000', '0000000000000000000000000000000000000000',
@ -116,9 +118,9 @@ class ClaimSearchCommand(ClaimTestCase):
# finding claims with and without a channel # finding claims with and without a channel
await self.assertFindsClaims([signed2, signed], name='on-channel-claim') await self.assertFindsClaims([signed2, signed], name='on-channel-claim')
await self.assertFindsClaims([signed2, signed], channel_ids=[self.channel_id, channel_id2]) await self.assertFindsClaims([signed2, signed], channel_id=[self.channel_id, channel_id2])
await self.assertFindsClaim(signed, name='on-channel-claim', channel_ids=[self.channel_id]) await self.assertFindsClaim(signed, name='on-channel-claim', channel_id=[self.channel_id])
await self.assertFindsClaim(signed2, name='on-channel-claim', channel_ids=[channel_id2]) await self.assertFindsClaim(signed2, name='on-channel-claim', channel_id=[channel_id2])
await self.assertFindsClaim(unsigned, name='unsigned') await self.assertFindsClaim(unsigned, name='unsigned')
await self.assertFindsClaim(unsigned, txid=unsigned['txid'], nout=0) await self.assertFindsClaim(unsigned, txid=unsigned['txid'], nout=0)
await self.assertFindsClaim(unsigned, claim_id=self.get_claim_id(unsigned)) await self.assertFindsClaim(unsigned, claim_id=self.get_claim_id(unsigned))
@ -128,37 +130,44 @@ class ClaimSearchCommand(ClaimTestCase):
# three streams in channel, zero streams in abandoned channel # three streams in channel, zero streams in abandoned channel
claims = [three, two, signed] claims = [three, two, signed]
await self.assertFindsClaims(claims, channel_ids=[self.channel_id]) await self.assertFindsClaims(claims, channel_id=[self.channel_id])
await self.assertFindsClaims(claims, channel=f"@abc#{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.assertFindsClaims([three, two, signed2, signed], channel_id=[channel_id2, self.channel_id])
await self.channel_abandon(claim_id=self.channel_id) await self.channel_abandon(claim_id=self.channel_id)
await self.assertFindsClaims([], channel=f"@abc#{self.channel_id}", valid_channel_signature=True) await self.assertFindsClaims([], channel=f"@abc#{self.channel_id}", valid_channel_signature=True)
await self.assertFindsClaims([], channel_ids=[self.channel_id], valid_channel_signature=True) await self.assertFindsClaims([signed2], channel_id=[channel_id2], valid_channel_signature=True)
await self.assertFindsClaims([signed2], channel_ids=[channel_id2], valid_channel_signature=True)
# pass `invalid_channel_signature=False` to catch a bug in argument processing # pass `invalid_channel_signature=False` to catch a bug in argument processing
await self.assertFindsClaims([signed2], channel_ids=[channel_id2, self.channel_id], await self.assertFindsClaims([signed2], channel_id=[channel_id2],
valid_channel_signature=True, invalid_channel_signature=False) valid_channel_signature=True, invalid_channel_signature=False)
# in old SDK abandoned channels caused content to have invalid signature,
# in new SDK this is not the case
# TODO: create situation where streams legitimately have invalid signature, harder in new SDK
# await self.assertFindsClaims([], channel_id=[self.channel_id], valid_channel_signature=True)
# invalid signature still returns channel_id # invalid signature still returns channel_id
invalid_claims = await self.claim_search(invalid_channel_signature=True, has_channel_signature=True) #invalid_claims = await self.claim_search(invalid_channel_signature=True, has_channel_signature=True)
self.assertEqual(3, len(invalid_claims)) #self.assertEqual(3, len(invalid_claims))
self.assertTrue(all([not c['is_channel_signature_valid'] for c in invalid_claims])) #self.assertTrue(all([not c['is_channel_signature_valid'] for c in invalid_claims]))
self.assertEqual({'channel_id': self.channel_id}, invalid_claims[0]['signing_channel']) #self.assertEqual({'channel_id': self.channel_id}, invalid_claims[0]['signing_channel'])
self.assertEqual(
0, len(await self.claim_search(invalid_channel_signature=True, has_channel_signature=True))
)
valid_claims = await self.claim_search(valid_channel_signature=True, has_channel_signature=True) valid_claims = await self.claim_search(valid_channel_signature=True, has_channel_signature=True)
self.assertEqual(1, len(valid_claims)) self.assertEqual(4, len(valid_claims))
self.assertTrue(all([c['is_channel_signature_valid'] for c in valid_claims])) self.assertTrue(all([c['is_channel_signature_valid'] for c in valid_claims]))
self.assertEqual('@abc', valid_claims[0]['signing_channel']['name']) self.assertEqual('@abc', valid_claims[1]['signing_channel']['name'])
# abandoned stream won't show up for streams in channel search # abandoned stream won't show up for streams in channel search
await self.stream_abandon(txid=signed2['txid'], nout=0) await self.stream_abandon(txid=signed2['txid'], nout=0)
await self.assertFindsClaims([], channel_ids=[channel_id2]) await self.assertFindsClaims([], channel_id=[channel_id2])
async def test_pagination(self): async def test_pagination(self):
await self.create_channel() await self.create_channel()
await self.create_lots_of_streams() await self.create_lots_of_streams()
# with and without totals # with and without totals
results = await self.api.claim_search(include_totals=True) results = await self.api.claim_search(include_total=True)
self.assertEqual(results['total_pages'], 2) self.assertEqual(results['total_pages'], 2)
self.assertEqual(results['total_items'], 25) self.assertEqual(results['total_items'], 25)
results = await self.api.claim_search() results = await self.api.claim_search()
@ -194,40 +203,40 @@ class ClaimSearchCommand(ClaimTestCase):
self.assertEqual(out_of_bounds, []) self.assertEqual(out_of_bounds, [])
async def test_tag_search(self): async def test_tag_search(self):
claim1 = await self.stream_create('claim1', tags=['aBc']) claim1 = await self.stream_create('claim1', tag=['aBc'])
claim2 = await self.stream_create('claim2', tags=['#abc', 'def']) claim2 = await self.stream_create('claim2', tag=['#abc', 'def'])
claim3 = await self.stream_create('claim3', tags=['abc', 'ghi', 'jkl']) claim3 = await self.stream_create('claim3', tag=['abc', 'ghi', 'jkl'])
claim4 = await self.stream_create('claim4', tags=['abc\t', 'ghi', 'mno']) claim4 = await self.stream_create('claim4', tag=['abc\t', 'ghi', 'mno'])
claim5 = await self.stream_create('claim5', tags=['pqr']) claim5 = await self.stream_create('claim5', tag=['pqr'])
# any_tags # any_tags
await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], any_tags=['\tabc', 'pqr']) await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], any_tag=['\tabc', 'pqr'])
await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tags=['abc']) await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tag=['abc'])
await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tags=['abc', 'ghi']) await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tag=['abc', 'ghi'])
await self.assertFindsClaims([claim4, claim3], any_tags=['ghi']) await self.assertFindsClaims([claim4, claim3], any_tag=['ghi'])
await self.assertFindsClaims([claim4, claim3], any_tags=['ghi', 'xyz']) await self.assertFindsClaims([claim4, claim3], any_tag=['ghi', 'xyz'])
await self.assertFindsClaims([], any_tags=['xyz']) await self.assertFindsClaims([], any_tag=['xyz'])
# all_tags # all_tags
await self.assertFindsClaims([], all_tags=['abc', 'pqr']) await self.assertFindsClaims([], all_tag=['abc', 'pqr'])
await self.assertFindsClaims([claim4, claim3, claim2, claim1], all_tags=['ABC']) await self.assertFindsClaims([claim4, claim3, claim2, claim1], all_tag=['ABC'])
await self.assertFindsClaims([claim4, claim3], all_tags=['abc', 'ghi']) await self.assertFindsClaims([claim4, claim3], all_tag=['abc', 'ghi'])
await self.assertFindsClaims([claim4, claim3], all_tags=['ghi']) await self.assertFindsClaims([claim4, claim3], all_tag=['ghi'])
await self.assertFindsClaims([], all_tags=['ghi', 'xyz']) await self.assertFindsClaims([], all_tag=['ghi', 'xyz'])
await self.assertFindsClaims([], all_tags=['xyz']) await self.assertFindsClaims([], all_tag=['xyz'])
# not_tags # not_tags
await self.assertFindsClaims([], not_tags=['abc', 'pqr']) await self.assertFindsClaims([], not_tag=['abc', 'pqr'])
await self.assertFindsClaims([claim5], not_tags=['abC']) await self.assertFindsClaims([claim5], not_tag=['abC'])
await self.assertFindsClaims([claim5], not_tags=['abc', 'ghi']) await self.assertFindsClaims([claim5], not_tag=['abc', 'ghi'])
await self.assertFindsClaims([claim5, claim2, claim1], not_tags=['ghi']) await self.assertFindsClaims([claim5, claim2, claim1], not_tag=['ghi'])
await self.assertFindsClaims([claim5, claim2, claim1], not_tags=['ghi', 'xyz']) await self.assertFindsClaims([claim5, claim2, claim1], not_tag=['ghi', 'xyz'])
await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], not_tags=['xyz']) await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], not_tag=['xyz'])
# combinations # combinations
await self.assertFindsClaims([claim3], all_tags=['abc', 'ghi'], not_tags=['mno']) await self.assertFindsClaims([claim3], all_tag=['abc', 'ghi'], not_tag=['mno'])
await self.assertFindsClaims([claim3], all_tags=['abc', 'ghi'], any_tags=['jkl'], not_tags=['mno']) await self.assertFindsClaims([claim3], all_tag=['abc', 'ghi'], any_tag=['jkl'], not_tag=['mno'])
await self.assertFindsClaims([claim4, claim3, claim2], all_tags=['abc'], any_tags=['def', 'ghi']) await self.assertFindsClaims([claim4, claim3, claim2], all_tags=['abc'], any_tag=['def', 'ghi'])
async def test_order_by(self): async def test_order_by(self):
height = self.ledger.sync.network.remote_height height = self.ledger.sync.network.remote_height

View file

@ -150,6 +150,7 @@ class WalletCommands(CommandTestCase):
}) })
@skip
class WalletEncryptionAndSynchronization(CommandTestCase): class WalletEncryptionAndSynchronization(CommandTestCase):
SEED = ( SEED = (