diff --git a/lbry/extras/daemon/daemon.py b/lbry/extras/daemon/daemon.py index cb3fbf2d9..1220092d7 100644 --- a/lbry/extras/daemon/daemon.py +++ b/lbry/extras/daemon/daemon.py @@ -920,16 +920,28 @@ class Daemon(metaclass=JSONRPCServerType): return self.platform_info @requires(WALLET_COMPONENT) - async def jsonrpc_resolve(self, urls: typing.Union[str, list], wallet_id=None): + async def jsonrpc_resolve(self, urls: typing.Union[str, list], wallet_id=None, **kwargs): """ Get the claim that a URL refers to. Usage: resolve ... [--wallet_id=] + [--include_purchase_receipt] + [--include_is_my_output] + [--include_my_supports] + [--include_my_tips] Options: - --urls= : (str, list) one or more urls to resolve - --wallet_id= : (str) wallet to check for claim purchase reciepts + --urls= : (str, list) one or more urls to resolve + --wallet_id= : (str) wallet to check for claim purchase reciepts + --include_purchase_receipt : (bool) lookup and include a receipt if this wallet + has purchased the claim being resolved + --include_is_my_output : (bool) lookup and include a boolean indicating + if claim being resolved is yours + --include_my_supports : (bool) lookup and sum the total amount + of supports you've made to this claim + --include_my_tips : (bool) lookup and sum the total amount + of tips you've made to this claim Returns: Dictionary of results, keyed by url @@ -1002,7 +1014,7 @@ class Daemon(metaclass=JSONRPCServerType): except ValueError: results[url] = {"error": f"{url} is not a valid url"} - resolved = await self.resolve(wallet.accounts, list(valid_urls)) + resolved = await self.resolve(wallet.accounts, list(valid_urls), **kwargs) for resolved_uri in resolved: results[resolved_uri] = resolved[resolved_uri] if resolved[resolved_uri] is not None else \ @@ -5100,8 +5112,8 @@ class Daemon(metaclass=JSONRPCServerType): except ValueError as e: raise ValueError(f"Invalid value for '{argument}': {e.args[0]}") - async def resolve(self, accounts, urls): - results = await self.ledger.resolve(accounts, urls) + async def resolve(self, accounts, urls, **kwargs): + results = await self.ledger.resolve(accounts, urls, **kwargs) if self.conf.save_resolved_claims and results: try: claims = self.stream_manager._convert_to_old_resolve_output(self.wallet_manager, results) diff --git a/lbry/extras/daemon/json_response_encoder.py b/lbry/extras/daemon/json_response_encoder.py index da87da92e..63714a0e1 100644 --- a/lbry/extras/daemon/json_response_encoder.py +++ b/lbry/extras/daemon/json_response_encoder.py @@ -174,6 +174,10 @@ class JSONResponseEncoder(JSONEncoder): output['is_my_output'] = txo.is_my_output if txo.is_my_input is not None: output['is_my_input'] = txo.is_my_input + if txo.my_supports is not None: + output['my_supports'] = dewies_to_lbc(txo.my_supports) + if txo.my_tips is not None: + output['my_tips'] = dewies_to_lbc(txo.my_tips) if txo.is_internal_transfer is not None: output['is_internal_transfer'] = txo.is_internal_transfer diff --git a/lbry/testcase.py b/lbry/testcase.py index 798937db6..56c9cb4c6 100644 --- a/lbry/testcase.py +++ b/lbry/testcase.py @@ -490,8 +490,8 @@ class CommandTestCase(IntegrationTestCase): self.daemon.jsonrpc_stream_update(claim_id, **kwargs), confirm ) - def stream_repost(self, claim_id, name='repost', bid='1.0', confirm=True, **kwargs): - return self.confirm_and_render( + async def stream_repost(self, claim_id, name='repost', bid='1.0', confirm=True, **kwargs): + return await self.confirm_and_render( self.daemon.jsonrpc_stream_repost(claim_id=claim_id, name=name, bid=bid, **kwargs), confirm ) @@ -502,6 +502,11 @@ class CommandTestCase(IntegrationTestCase): self.daemon.jsonrpc_stream_abandon(*args, **kwargs), confirm ) + async def purchase_create(self, *args, confirm=True, **kwargs): + return await self.confirm_and_render( + self.daemon.jsonrpc_purchase_create(*args, **kwargs), confirm + ) + async def publish(self, name, *args, confirm=True, **kwargs): return await self.confirm_and_render( self.daemon.jsonrpc_publish(name, *args, **kwargs), confirm @@ -560,8 +565,8 @@ class CommandTestCase(IntegrationTestCase): self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm ) - async def resolve(self, uri): - return (await self.out(self.daemon.jsonrpc_resolve(uri)))[uri] + async def resolve(self, uri, **kwargs): + return (await self.out(self.daemon.jsonrpc_resolve(uri, **kwargs)))[uri] async def claim_search(self, **kwargs): return (await self.out(self.daemon.jsonrpc_claim_search(**kwargs)))['items'] diff --git a/lbry/wallet/database.py b/lbry/wallet/database.py index d8265459e..53f4cf650 100644 --- a/lbry/wallet/database.py +++ b/lbry/wallet/database.py @@ -446,7 +446,7 @@ class Database(SQLiteMixin): ); create index if not exists txo_txid_idx on txo (txid); create index if not exists txo_address_idx on txo (address); - create index if not exists txo_claim_id_idx on txo (claim_id); + create index if not exists txo_claim_id_idx on txo (claim_id, txo_type); create index if not exists txo_claim_name_idx on txo (claim_name); create index if not exists txo_txo_type_idx on txo (txo_type); create index if not exists txo_channel_id_idx on txo (channel_id); @@ -676,7 +676,7 @@ class Database(SQLiteMixin): constraints.pop('limit', None) constraints.pop('order_by', None) count = await self.select_transactions('COUNT(*) as total', **constraints) - return count[0]['total'] + return count[0]['total'] or 0 async def get_transaction(self, **constraints): txs = await self.get_transactions(limit=1, **constraints) @@ -865,12 +865,12 @@ class Database(SQLiteMixin): async def get_txo_count(self, unspent=False, **constraints): self._clean_txo_constraints_for_aggregation(unspent, constraints) count = await self.select_txos('COUNT(*) as total', **constraints) - return count[0]['total'] + return count[0]['total'] or 0 async def get_txo_sum(self, unspent=False, **constraints): self._clean_txo_constraints_for_aggregation(unspent, constraints) result = await self.select_txos('SUM(amount) as total', **constraints) - return result[0]['total'] + return result[0]['total'] or 0 def get_utxos(self, read_only=False, **constraints): return self.get_txos(unspent=True, read_only=read_only, **constraints) @@ -908,7 +908,7 @@ class Database(SQLiteMixin): async def get_address_count(self, cols=None, read_only=False, **constraints): count = await self.select_addresses('COUNT(*) as total', read_only=read_only, **constraints) - return count[0]['total'] + return count[0]['total'] or 0 async def get_address(self, read_only=False, **constraints): addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints) diff --git a/lbry/wallet/ledger.py b/lbry/wallet/ledger.py index 639446d3f..12d202254 100644 --- a/lbry/wallet/ledger.py +++ b/lbry/wallet/ledger.py @@ -25,7 +25,7 @@ from .account import Account, AddressManager, SingleKey from .network import Network from .transaction import Transaction, Output from .header import Headers, UnvalidatedHeaders -from .constants import TXO_TYPES, COIN, NULL_HASH32 +from .constants import TXO_TYPES, CLAIM_TYPES, COIN, NULL_HASH32 from .bip32 import PubKey, PrivateKey from .coinselection import CoinSelector @@ -646,7 +646,13 @@ class Ledger(metaclass=LedgerRegistry): print(record['history'], addresses, tx.id) raise asyncio.TimeoutError('Timed out waiting for transaction.') - async def _inflate_outputs(self, query, accounts) -> Tuple[List[Output], dict, int, int]: + async def _inflate_outputs( + self, query, accounts, + include_purchase_receipt=False, + include_is_my_output=False, + include_my_supports=False, + include_my_tips=False + ) -> Tuple[List[Output], dict, int, int]: encoded_outputs = await query outputs = Outputs.from_base64(encoded_outputs or b'') # TODO: why is the server returning None? txs = [] @@ -654,7 +660,7 @@ class Ledger(metaclass=LedgerRegistry): txs: List[Transaction] = await asyncio.gather(*( self.cache_transaction(*tx) for tx in outputs.txs )) - if accounts: + if include_purchase_receipt and accounts: priced_claims = [] for tx in txs: for txo in tx.outputs: @@ -671,11 +677,36 @@ class Ledger(metaclass=LedgerRegistry): for txo in priced_claims: txo.purchase_receipt = receipts.get(txo.claim_id) txos, blocked = outputs.inflate(txs) + if any((include_is_my_output, include_my_supports, include_my_tips)): + for txo in txos: + if isinstance(txo, Output) and txo.can_decode_claim: + if include_is_my_output: + mine = await self.db.get_txo_count( + claim_id=txo.claim_id, txo_type__in=CLAIM_TYPES, is_my_output=True, + unspent=True, accounts=accounts + ) + if mine: + txo.is_my_output = True + else: + txo.is_my_output = False + if include_my_supports: + supports = await self.db.get_txo_sum( + claim_id=txo.claim_id, txo_type=TXO_TYPES['support'], + is_my_input=True, is_my_output=True, + unspent=True, accounts=accounts + ) + txo.my_supports = supports + if include_my_tips: + tips = await self.db.get_txo_sum( + claim_id=txo.claim_id, txo_type=TXO_TYPES['support'], + is_my_input=True, is_my_output=False, accounts=accounts + ) + txo.my_tips = tips return txos, blocked, outputs.offset, outputs.total - async def resolve(self, accounts, urls): + async def resolve(self, accounts, urls, **kwargs): resolve = partial(self.network.retriable_call, self.network.resolve) - txos = (await self._inflate_outputs(resolve(urls), accounts))[0] + txos = (await self._inflate_outputs(resolve(urls), accounts, **kwargs))[0] assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received." result = {} for url, txo in zip(urls, txos): diff --git a/lbry/wallet/transaction.py b/lbry/wallet/transaction.py index 264357f56..d8518999f 100644 --- a/lbry/wallet/transaction.py +++ b/lbry/wallet/transaction.py @@ -208,7 +208,7 @@ class Output(InputOutput): __slots__ = ( 'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input', - 'channel', 'private_key', 'meta', + 'channel', 'private_key', 'meta', 'my_supports', 'my_tips', 'purchase', 'purchased_claim', 'purchase_receipt', 'reposted_claim', 'claims', ) @@ -217,6 +217,7 @@ class Output(InputOutput): tx_ref: TXRef = None, position: int = None, is_internal_transfer: Optional[bool] = None, is_spent: Optional[bool] = None, is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None, + my_supports: Optional[int] = None, my_tips: Optional[int] = None, channel: Optional['Output'] = None, private_key: Optional[str] = None ) -> None: super().__init__(tx_ref, position) @@ -226,6 +227,8 @@ class Output(InputOutput): self.is_spent = is_spent self.is_my_output = is_my_output self.is_my_input = is_my_input + self.my_supports = my_supports + self.my_tips = my_tips self.channel = channel self.private_key = private_key self.purchase: 'Output' = None # txo containing purchase metadata diff --git a/tests/integration/blockchain/test_resolve_command.py b/tests/integration/blockchain/test_resolve_command.py index eff769302..a86330129 100644 --- a/tests/integration/blockchain/test_resolve_command.py +++ b/tests/integration/blockchain/test_resolve_command.py @@ -263,6 +263,49 @@ class ResolveCommand(BaseResolveTestCase): await self.resolve('@olds/bad_example') ) + async def test_resolve_with_includes(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.wallet_send('1.0', address2) + + stream = await self.stream_create( + 'priced', '0.1', wallet_id=wallet2.id, + fee_amount='0.5', fee_currency='LBC', fee_address=address2 + ) + stream_id = self.get_claim_id(stream) + + resolve = await self.resolve('priced') + self.assertNotIn('is_my_output', resolve) + self.assertNotIn('purchase_receipt', resolve) + self.assertNotIn('my_supports', resolve) + self.assertNotIn('my_tips', resolve) + + # is_my_output + resolve = await self.resolve('priced', include_is_my_output=True) + self.assertFalse(resolve['is_my_output']) + resolve = await self.resolve('priced', wallet_id=wallet2.id, include_is_my_output=True) + self.assertTrue(resolve['is_my_output']) + + # purchase receipt + resolve = await self.resolve('priced', include_purchase_receipt=True) + self.assertNotIn('purchase_receipt', resolve) + await self.purchase_create(stream_id) + resolve = await self.resolve('priced', include_purchase_receipt=True) + self.assertEqual('0.5', resolve['purchase_receipt']['amount']) + + # my supports and my tips + resolve = await self.resolve('priced', include_my_supports=True, include_my_tips=True) + self.assertEqual('0.0', resolve['my_supports']) + self.assertEqual('0.0', resolve['my_tips']) + await self.support_create(stream_id, '0.3') + await self.support_create(stream_id, '0.2') + await self.support_create(stream_id, '0.4', tip=True) + await self.support_create(stream_id, '0.5', tip=True) + resolve = await self.resolve('priced', include_my_supports=True, include_my_tips=True) + self.assertEqual('0.5', resolve['my_supports']) + self.assertEqual('0.9', resolve['my_tips']) + class ResolveAfterReorg(BaseResolveTestCase):