diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index 8b098e451..c83d1462b 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -4342,7 +4342,8 @@ class Daemon(metaclass=JSONRPCServerType): @staticmethod def _constrain_txo_from_kwargs( constraints, type=None, txid=None, # pylint: disable=redefined-builtin - claim_id=None, channel_id=None, name=None, reposted_claim_id=None, + claim_id=None, channel_id=None, not_channel_id=None, + name=None, reposted_claim_id=None, is_spent=False, is_not_spent=False, is_my_input_or_output=None, exclude_internal_transfers=False, is_my_output=None, is_not_my_output=None, @@ -4365,6 +4366,7 @@ class Daemon(metaclass=JSONRPCServerType): constraints['is_my_output'] = False database.constrain_single_or_list(constraints, 'txo_type', type, lambda x: TXO_TYPES[x]) database.constrain_single_or_list(constraints, 'channel_id', channel_id) + database.constrain_single_or_list(constraints, 'channel_id', not_channel_id, negate=True) database.constrain_single_or_list(constraints, 'claim_id', claim_id) database.constrain_single_or_list(constraints, 'claim_name', name) database.constrain_single_or_list(constraints, 'txid', txid) @@ -4379,9 +4381,9 @@ class Daemon(metaclass=JSONRPCServerType): List my transaction outputs. Usage: - txo_list [--account_id=] [--type=...] [--txid=...] - [--claim_id=...] [--channel_id=...] [--name=...] - [--is_spent | --is_not_spent] + txo_list [--account_id=] [--type=...] [--txid=...] [--claim_id=...] + [--channel_id=...] [--not_channel_id=...] + [--name=...] [--is_spent | --is_not_spent] [--is_my_input_or_output | [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]] ] @@ -4395,6 +4397,7 @@ class Daemon(metaclass=JSONRPCServerType): --txid= : (str or list) transaction id of outputs --claim_id= : (str or list) claim id --channel_id= : (str or list) claims in this channel + --not_channel_id=: (str or list) claims not in this channel --name= : (str or list) claim name --is_spent : (bool) only show spent txos --is_not_spent : (bool) only show not spent txos @@ -4454,9 +4457,9 @@ class Daemon(metaclass=JSONRPCServerType): Spend transaction outputs, batching into multiple transactions as necessary. Usage: - txo_spend [--account_id=] [--type=...] [--txid=...] - [--claim_id=...] [--channel_id=...] [--name=...] - [--is_my_input | --is_not_my_input] + txo_spend [--account_id=] [--type=...] [--txid=...] [--claim_id=...] + [--channel_id=...] [--not_channel_id=...] + [--name=...] [--is_my_input | --is_not_my_input] [--exclude_internal_transfers] [--wallet_id=] [--preview] [--blocking] [--batch_size=] [--include_full_tx] @@ -4466,6 +4469,7 @@ class Daemon(metaclass=JSONRPCServerType): --txid= : (str or list) transaction id of outputs --claim_id= : (str or list) claim id --channel_id= : (str or list) claims in this channel + --not_channel_id=: (str or list) claims not in this channel --name= : (str or list) claim name --is_my_input : (bool) show outputs created by you --is_not_my_input : (bool) show outputs not created by you @@ -4510,6 +4514,7 @@ class Daemon(metaclass=JSONRPCServerType): Usage: txo_list [--account_id=] [--type=...] [--txid=...] + [--channel_id=...] [--not_channel_id=...] [--claim_id=...] [--name=...] [--is_spent] [--is_not_spent] [--is_my_input_or_output | @@ -4523,6 +4528,8 @@ class Daemon(metaclass=JSONRPCServerType): --txid= : (str or list) transaction id of outputs --claim_id= : (str or list) claim id --name= : (str or list) claim name + --channel_id= : (str or list) claims in this channel + --not_channel_id=: (str or list) claims not in this channel --is_spent : (bool) only show spent txos --is_not_spent : (bool) only show not spent txos --is_my_input_or_output : (bool) txos which have your inputs or your outputs, @@ -4557,6 +4564,7 @@ class Daemon(metaclass=JSONRPCServerType): Usage: txo_plot [--account_id=] [--type=...] [--txid=...] [--claim_id=...] [--name=...] [--is_spent] [--is_not_spent] + [--channel_id=...] [--not_channel_id=...] [--is_my_input_or_output | [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]] ] @@ -4571,6 +4579,8 @@ class Daemon(metaclass=JSONRPCServerType): --txid= : (str or list) transaction id of outputs --claim_id= : (str or list) claim id --name= : (str or list) claim name + --channel_id= : (str or list) claims in this channel + --not_channel_id=: (str or list) claims not in this channel --is_spent : (bool) only show spent txos --is_not_spent : (bool) only show not spent txos --is_my_input_or_output : (bool) txos which have your inputs or your outputs, diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index 5ddbf3fb3..f73560bca 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -387,14 +387,31 @@ def interpolate(sql, values): return sql -def constrain_single_or_list(constraints, column, value, convert=lambda x: x): +def constrain_single_or_list(constraints, column, value, convert=lambda x: x, negate=False): if value is not None: if isinstance(value, list): value = [convert(v) for v in value] if len(value) == 1: - constraints[column] = value[0] + if negate: + constraints[f"{column}__or"] = { + f"{column}__is_null": True, + f"{column}__not": value[0] + } + else: + constraints[column] = value[0] elif len(value) > 1: - constraints[f"{column}__in"] = value + if negate: + constraints[f"{column}__or"] = { + f"{column}__is_null": True, + f"{column}__not_in": value + } + else: + constraints[f"{column}__in"] = value + elif negate: + constraints[f"{column}__or"] = { + f"{column}__is_null": True, + f"{column}__not": convert(value) + } else: constraints[column] = convert(value) return constraints diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index 17a322ed5..e174056a4 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -461,6 +461,29 @@ class TransactionCommands(ClaimTestCase): class TransactionOutputCommands(ClaimTestCase): + async def test_txo_list_by_channel_filtering(self): + channel_foo = self.get_claim_id(await self.channel_create('@foo')) + channel_bar = self.get_claim_id(await self.channel_create('@bar')) + stream_a = self.get_claim_id(await self.stream_create('a', channel_id=channel_foo)) + stream_b = self.get_claim_id(await self.stream_create('b', channel_id=channel_foo)) + stream_c = self.get_claim_id(await self.stream_create('c', channel_id=channel_bar)) + stream_d = self.get_claim_id(await self.stream_create('d')) + + r = await self.txo_list(type='stream') + self.assertEqual({stream_a, stream_b, stream_c, stream_d}, {c['claim_id'] for c in r}) + + r = await self.txo_list(type='stream', channel_id=channel_foo) + self.assertEqual({stream_a, stream_b}, {c['claim_id'] for c in r}) + + r = await self.txo_list(type='stream', channel_id=[channel_foo, channel_bar]) + self.assertEqual({stream_a, stream_b, stream_c}, {c['claim_id'] for c in r}) + + r = await self.txo_list(type='stream', not_channel_id=channel_foo) + self.assertEqual({stream_c, stream_d}, {c['claim_id'] for c in r}) + + r = await self.txo_list(type='stream', not_channel_id=[channel_foo, channel_bar]) + self.assertEqual({stream_d}, {c['claim_id'] for c in r}) + async def test_txo_list_and_sum_filtering(self): channel_id = self.get_claim_id(await self.channel_create()) self.assertEqual('1.0', lbc(await self.txo_sum(type='channel', is_not_spent=True)))