From 8ef2647fa95ec6c3b259804c1cd502d3279e8cde Mon Sep 17 00:00:00 2001
From: Lex Berezhny <lex@damoti.com>
Date: Sun, 8 Mar 2020 23:11:03 -0400
Subject: [PATCH] is_received

---
 lbry/extras/daemon/daemon.py                  | 23 ++++++++++++++---
 lbry/extras/daemon/json_response_encoder.py   |  5 +++-
 lbry/wallet/database.py                       | 25 +++++++++++++++----
 lbry/wallet/transaction.py                    |  5 ++--
 .../blockchain/test_claim_commands.py         | 20 +++++++++++++++
 5 files changed, 66 insertions(+), 12 deletions(-)

diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py
index 1e796e4d7..c91c72f14 100644
--- a/lbry/extras/daemon/daemon.py
+++ b/lbry/extras/daemon/daemon.py
@@ -3902,18 +3902,19 @@ class Daemon(metaclass=JSONRPCServerType):
         return tx
 
     @requires(WALLET_COMPONENT)
-    def jsonrpc_support_list(self, *args, **kwargs):
+    def jsonrpc_support_list(self, *args, tips=None, **kwargs):
         """
         List supports and tips in my control.
 
         Usage:
             support_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
-                         [--name=<name>...] [--claim_id=<claim_id>...]
+                         [--name=<name>...] [--claim_id=<claim_id>...] [--tips]
                          [--page=<page>] [--page_size=<page_size>]
 
         Options:
             --name=<name>              : (str or list) claim name
             --claim_id=<claim_id>      : (str or list) claim id
+            --tips                     : (bool) only show tips (is_received=true)
             --account_id=<account_id>  : (str) id of the account to query
             --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
             --page=<page>              : (int) page to return during paginating
@@ -3923,6 +3924,9 @@ class Daemon(metaclass=JSONRPCServerType):
         """
         kwargs['type'] = 'support'
         kwargs['unspent'] = True
+        kwargs['include_is_received'] = True
+        if tips is True:
+            kwargs['is_received'] = True
         return self.jsonrpc_txo_list(*args, **kwargs)
 
     @requires(WALLET_COMPONENT)
@@ -4098,6 +4102,7 @@ class Daemon(metaclass=JSONRPCServerType):
     def jsonrpc_txo_list(
             self, account_id=None, type=None, txid=None,  # pylint: disable=redefined-builtin
             claim_id=None, name=None, unspent=False,
+            include_is_received=False, is_received=None, is_not_received=None,
             wallet_id=None, page=None, page_size=None, resolve=False):
         """
         List my transaction outputs.
@@ -4105,7 +4110,8 @@ class Daemon(metaclass=JSONRPCServerType):
         Usage:
             txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]
                      [--claim_id=<claim_id>...] [--name=<name>...] [--unspent]
-                     [--wallet_id=<wallet_id>]
+                     [--include_is_received] [--is_received] [--is_not_received]
+                     [--wallet_id=<wallet_id>] [--include_is_received] [--is_received]
                      [--page=<page>] [--page_size=<page_size>]
                      [--resolve]
 
@@ -4116,6 +4122,11 @@ class Daemon(metaclass=JSONRPCServerType):
             --unspent                  : (bool) hide spent outputs, show only unspent ones
             --claim_id=<claim_id>      : (str or list) claim id
             --name=<name>              : (str or list) claim name
+            --include_is_received      : (bool) calculate the is_received property and
+                                         include in output, this happens automatically if you
+                                         use the --is_received or --is_not_received filters
+            --is_received              : (bool) only return txos sent from others to this account
+            --is_not_received          : (bool) only return txos created by this account
             --account_id=<account_id>  : (str) id of the account to query
             --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
             --page=<page>              : (int) page to return during paginating
@@ -4132,7 +4143,11 @@ class Daemon(metaclass=JSONRPCServerType):
         else:
             claims = partial(self.ledger.get_txos, wallet=wallet, accounts=wallet.accounts)
             claim_count = partial(self.ledger.get_txo_count, wallet=wallet, accounts=wallet.accounts)
-        constraints = {'resolve': resolve, 'unspent': unspent}
+        constraints = {'resolve': resolve, 'unspent': unspent, 'include_is_received': include_is_received}
+        if is_received is True:
+            constraints['is_received'] = True
+        elif is_not_received is True:
+            constraints['is_received'] = False
         database.constrain_single_or_list(constraints, 'txo_type', type, lambda x: TXO_TYPES[x])
         database.constrain_single_or_list(constraints, 'claim_id', claim_id)
         database.constrain_single_or_list(constraints, 'claim_name', name)
diff --git a/lbry/extras/daemon/json_response_encoder.py b/lbry/extras/daemon/json_response_encoder.py
index 4c400f8f6..a3c9e318d 100644
--- a/lbry/extras/daemon/json_response_encoder.py
+++ b/lbry/extras/daemon/json_response_encoder.py
@@ -25,7 +25,8 @@ def encode_txo_doc():
         'address': "address of who can spend the txo",
         'confirmations': "number of confirmed blocks",
         'is_change': "payment to change address, only available when it can be determined",
-        'is_spent': "true if txo is spent, false or None if it could not be determined",
+        'is_received': "true if txo was sent from external account to this account",
+        'is_spent': "true if txo is spent",
         'is_mine': "payment to one of your accounts, only available when it can be determined",
         'type': "one of 'claim', 'support' or 'purchase'",
         'name': "when type is 'claim' or 'support', this is the claim name",
@@ -169,6 +170,8 @@ class JSONResponseEncoder(JSONEncoder):
         }
         if txo.is_change is not None:
             output['is_change'] = txo.is_change
+        if txo.is_received is not None:
+            output['is_received'] = txo.is_received
         if txo.is_spent is not None:
             output['is_spent'] = txo.is_spent
         if txo.is_my_account is not None:
diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py
index 4e75c0a69..95e0e82c2 100644
--- a/lbry/wallet/database.py
+++ b/lbry/wallet/database.py
@@ -577,7 +577,19 @@ class Database(SQLiteMixin):
         if txs:
             return txs[0]
 
-    async def select_txos(self, cols, **constraints):
+    async def select_txos(self, cols, wallet=None, include_is_received=False, **constraints):
+        if include_is_received:
+            assert wallet is not None, 'cannot use is_recieved filter without wallet argument'
+            account_in_wallet, values = constraints_to_sql({
+                '$$account__in#is_received': [a.public_key.address for a in wallet.accounts]
+            })
+            cols += f""",
+            NOT EXISTS(
+                SELECT 1 FROM txi JOIN account_address USING (address)
+                WHERE txi.txid=txo.txid AND {account_in_wallet}
+            ) as is_received
+            """
+            constraints.update(values)
         sql = f"SELECT {cols} FROM txo JOIN tx USING (txid)"
         if 'accounts' in constraints:
             sql += " JOIN account_address USING (address)"
@@ -588,7 +600,8 @@ class Database(SQLiteMixin):
         constraints['is_reserved'] = False
         constraints['txoid__not_in'] = "SELECT txoid FROM txi"
 
-    async def get_txos(self, wallet=None, no_tx=False, unspent=False, **constraints):
+    async def get_txos(self, wallet=None, no_tx=False, unspent=False, include_is_received=False, **constraints):
+        include_is_received = include_is_received or 'is_received' in constraints
         if unspent:
             self.constrain_unspent(constraints)
         my_accounts = {a.public_key.address for a in wallet.accounts} if wallet else set()
@@ -601,9 +614,9 @@ class Database(SQLiteMixin):
             tx.txid, raw, tx.height, tx.position, tx.is_verified, txo.position, amount, script, (
                 select group_concat(account||"|"||chain) from account_address
                 where account_address.address=txo.address
-            ), exists(select txoid from txi where txi.txoid=txo.txoid)
+            ), exists(select 1 from txi where txi.txoid=txo.txoid)
             """,
-            **constraints
+            wallet=wallet, include_is_received=include_is_received, **constraints
         )
         txos = []
         txs = {}
@@ -624,6 +637,8 @@ class Database(SQLiteMixin):
             row_accounts = dict(a.split('|') for a in row[8].split(','))
             account_match = set(row_accounts) & my_accounts
             txo.is_spent = bool(row[9])
+            if include_is_received:
+                txo.is_received = bool(row[10])
             if account_match:
                 txo.is_my_account = True
                 txo.is_change = row_accounts[account_match.pop()] == '1'
@@ -660,8 +675,8 @@ class Database(SQLiteMixin):
         return txos
 
     async def get_txo_count(self, unspent=False, **constraints):
+        constraints['include_is_received'] = 'is_received' in constraints
         constraints.pop('resolve', None)
-        constraints.pop('wallet', None)
         constraints.pop('offset', None)
         constraints.pop('limit', None)
         constraints.pop('order_by', None)
diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py
index 882e93df4..cfd637687 100644
--- a/lbry/wallet/transaction.py
+++ b/lbry/wallet/transaction.py
@@ -207,7 +207,7 @@ class OutputEffectiveAmountEstimator:
 class Output(InputOutput):
 
     __slots__ = (
-        'amount', 'script', 'is_change', 'is_spent', 'is_my_account',
+        'amount', 'script', 'is_change', 'is_spent', 'is_received', 'is_my_account',
         'channel', 'private_key', 'meta',
         'purchase', 'purchased_claim', 'purchase_receipt',
         'reposted_claim', 'claims',
@@ -216,7 +216,7 @@ class Output(InputOutput):
     def __init__(self, amount: int, script: OutputScript,
                  tx_ref: TXRef = None, position: int = None,
                  is_change: Optional[bool] = None, is_spent: Optional[bool] = None,
-                 is_my_account: Optional[bool] = None,
+                 is_received: Optional[bool] = None, is_my_account: Optional[bool] = None,
                  channel: Optional['Output'] = None, private_key: Optional[str] = None
                  ) -> None:
         super().__init__(tx_ref, position)
@@ -224,6 +224,7 @@ class Output(InputOutput):
         self.script = script
         self.is_change = is_change
         self.is_spent = is_spent
+        self.is_received = is_received
         self.is_my_account = is_my_account
         self.channel = channel
         self.private_key = private_key
diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py
index 7edd0b823..a99106751 100644
--- a/tests/integration/blockchain/test_claim_commands.py
+++ b/tests/integration/blockchain/test_claim_commands.py
@@ -480,6 +480,26 @@ class TransactionOutputCommands(ClaimTestCase):
         self.assertTrue(r[0]['is_spent'])
         self.assertTrue(r[1]['is_spent'])
 
+    async def test_txo_list_received_filtering(self):
+        wallet2 = await self.daemon.jsonrpc_wallet_create('wallet2', create_account=True)
+        address2 = await self.daemon.jsonrpc_address_unused(wallet_id=wallet2.id)
+        await self.channel_create(claim_address=address2)
+
+        r = await self.txo_list(include_is_received=True)
+        self.assertEqual(2, len(r))
+        self.assertFalse(r[0]['is_received'])
+        self.assertTrue(r[1]['is_received'])
+        rt = await self.txo_list(is_not_received=True)
+        self.assertEqual(1, len(rt))
+        self.assertEqual(rt[0], r[0])
+        rf = await self.txo_list(is_received=True)
+        self.assertEqual(1, len(rf))
+        self.assertEqual(rf[0], r[1])
+
+        r = await self.txo_list(include_is_received=True, wallet_id=wallet2.id)
+        self.assertEqual(1, len(r))
+        self.assertTrue(r[0]['is_received'])
+
 
 class ClaimCommands(ClaimTestCase):