From bf0118c8efa9f6bfcdeedc170e3ea4f7df009330 Mon Sep 17 00:00:00 2001
From: Lex Berezhny <lex@damoti.com>
Date: Thu, 21 Jan 2021 12:54:42 -0500
Subject: [PATCH] added support for --not_channel_id to txo_list commands

---
 lbry/extras/daemon/daemon.py                  | 24 +++++++++++++------
 lbry/wallet/database.py                       | 23 +++++++++++++++---
 .../blockchain/test_claim_commands.py         | 23 ++++++++++++++++++
 3 files changed, 60 insertions(+), 10 deletions(-)

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=<account_id>] [--type=<type>...] [--txid=<txid>...]
-                     [--claim_id=<claim_id>...] [--channel_id=<channel_id>...] [--name=<name>...]
-                     [--is_spent | --is_not_spent]
+            txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] [--claim_id=<claim_id>...]
+                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
+                     [--name=<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=<txid>              : (str or list) transaction id of outputs
             --claim_id=<claim_id>      : (str or list) claim id
             --channel_id=<channel_id>  : (str or list) claims in this channel
+      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel
             --name=<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=<account_id>] [--type=<type>...] [--txid=<txid>...]
-                      [--claim_id=<claim_id>...] [--channel_id=<channel_id>...] [--name=<name>...]
-                      [--is_my_input | --is_not_my_input]
+            txo_spend [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] [--claim_id=<claim_id>...]
+                      [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
+                      [--name=<name>...] [--is_my_input | --is_not_my_input]
                       [--exclude_internal_transfers] [--wallet_id=<wallet_id>]
                       [--preview] [--blocking] [--batch_size=<batch_size>] [--include_full_tx]
 
@@ -4466,6 +4469,7 @@ class Daemon(metaclass=JSONRPCServerType):
             --txid=<txid>              : (str or list) transaction id of outputs
             --claim_id=<claim_id>      : (str or list) claim id
             --channel_id=<channel_id>  : (str or list) claims in this channel
+      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel
             --name=<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=<account_id>] [--type=<type>...] [--txid=<txid>...]
+                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
                      [--claim_id=<claim_id>...] [--name=<name>...]
                      [--is_spent] [--is_not_spent]
                      [--is_my_input_or_output |
@@ -4523,6 +4528,8 @@ class Daemon(metaclass=JSONRPCServerType):
             --txid=<txid>              : (str or list) transaction id of outputs
             --claim_id=<claim_id>      : (str or list) claim id
             --name=<name>              : (str or list) claim name
+            --channel_id=<channel_id>  : (str or list) claims in this channel
+      --not_channel_id=<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=<account_id>] [--type=<type>...] [--txid=<txid>...]
                      [--claim_id=<claim_id>...] [--name=<name>...] [--is_spent] [--is_not_spent]
+                     [--channel_id=<channel_id>...] [--not_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=<txid>              : (str or list) transaction id of outputs
             --claim_id=<claim_id>      : (str or list) claim id
             --name=<name>              : (str or list) claim name
+            --channel_id=<channel_id>  : (str or list) claims in this channel
+      --not_channel_id=<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)))