From 8ef2647fa95ec6c3b259804c1cd502d3279e8cde Mon Sep 17 00:00:00 2001 From: Lex Berezhny 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=] [--wallet_id=] - [--name=...] [--claim_id=...] + [--name=...] [--claim_id=...] [--tips] [--page=] [--page_size=] Options: --name= : (str or list) claim name --claim_id= : (str or list) claim id + --tips : (bool) only show tips (is_received=true) --account_id= : (str) id of the account to query --wallet_id= : (str) restrict results to specific wallet --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=] [--type=...] [--txid=...] [--claim_id=...] [--name=...] [--unspent] - [--wallet_id=] + [--include_is_received] [--is_received] [--is_not_received] + [--wallet_id=] [--include_is_received] [--is_received] [--page=] [--page_size=] [--resolve] @@ -4116,6 +4122,11 @@ class Daemon(metaclass=JSONRPCServerType): --unspent : (bool) hide spent outputs, show only unspent ones --claim_id= : (str or list) claim id --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= : (str) id of the account to query --wallet_id= : (str) restrict results to specific wallet --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):