Merge pull request #2870 from lbryio/resolve_includes

added new flags to `resolve` (and a few to `claim_search`) commands: `--include_purchase_receipt`, `--include_is_my_output`, `--include_sent_supports`, `--include_sent_tips` and `--include_received_tips`
This commit is contained in:
Lex Berezhny 2020-03-22 02:08:28 -04:00 committed by GitHub
commit f0217f6821
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 179 additions and 27 deletions

View file

@ -920,16 +920,33 @@ class Daemon(metaclass=JSONRPCServerType):
return self.platform_info return self.platform_info
@requires(WALLET_COMPONENT) @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. Get the claim that a URL refers to.
Usage: Usage:
resolve <urls>... [--wallet_id=<wallet_id>] resolve <urls>... [--wallet_id=<wallet_id>]
[--include_purchase_receipt]
[--include_is_my_output]
[--include_sent_supports]
[--include_sent_tips]
[--include_received_tips]
Options: Options:
--urls=<urls> : (str, list) one or more urls to resolve --urls=<urls> : (str, list) one or more urls to resolve
--wallet_id=<wallet_id> : (str) wallet to check for claim purchase reciepts --wallet_id=<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_sent_supports : (bool) lookup and sum the total amount
of supports you've made to this claim
--include_sent_tips : (bool) lookup and sum the total amount
of tips you've made to this claim
(only makes sense when claim is not yours)
--include_received_tips : (bool) lookup and sum the total amount
of tips you've received to this claim
(only makes sense when claim is yours)
Returns: Returns:
Dictionary of results, keyed by url Dictionary of results, keyed by url
@ -1002,7 +1019,7 @@ class Daemon(metaclass=JSONRPCServerType):
except ValueError: except ValueError:
results[url] = {"error": f"{url} is not a valid url"} 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: for resolved_uri in resolved:
results[resolved_uri] = resolved[resolved_uri] if resolved[resolved_uri] is not None else \ results[resolved_uri] = resolved[resolved_uri] if resolved[resolved_uri] is not None else \
@ -2232,7 +2249,7 @@ class Daemon(metaclass=JSONRPCServerType):
[--any_locations=<any_locations>...] [--all_locations=<all_locations>...] [--any_locations=<any_locations>...] [--all_locations=<all_locations>...]
[--not_locations=<not_locations>...] [--not_locations=<not_locations>...]
[--order_by=<order_by>...] [--page=<page>] [--page_size=<page_size>] [--order_by=<order_by>...] [--page=<page>] [--page_size=<page_size>]
[--wallet_id=<wallet_id>] [--wallet_id=<wallet_id>] [--include_purchase_receipt] [--include_is_my_output]
Options: Options:
--name=<name> : (str) claim name (normalized) --name=<name> : (str) claim name (normalized)
@ -2334,6 +2351,10 @@ class Daemon(metaclass=JSONRPCServerType):
--no_totals : (bool) do not calculate the total number of pages and items in result set --no_totals : (bool) do not calculate the total number of pages and items in result set
(significant performance boost) (significant performance boost)
--wallet_id=<wallet_id> : (str) wallet to check for claim purchase reciepts --wallet_id=<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
--include_is_my_output : (bool) lookup and include a boolean indicating
if claim being resolved is yours
Returns: {Paginated[Output]} Returns: {Paginated[Output]}
""" """
@ -5100,8 +5121,8 @@ class Daemon(metaclass=JSONRPCServerType):
except ValueError as e: except ValueError as e:
raise ValueError(f"Invalid value for '{argument}': {e.args[0]}") raise ValueError(f"Invalid value for '{argument}': {e.args[0]}")
async def resolve(self, accounts, urls): async def resolve(self, accounts, urls, **kwargs):
results = await self.ledger.resolve(accounts, urls) results = await self.ledger.resolve(accounts, urls, **kwargs)
if self.conf.save_resolved_claims and results: if self.conf.save_resolved_claims and results:
try: try:
claims = self.stream_manager._convert_to_old_resolve_output(self.wallet_manager, results) claims = self.stream_manager._convert_to_old_resolve_output(self.wallet_manager, results)

View file

@ -174,6 +174,12 @@ class JSONResponseEncoder(JSONEncoder):
output['is_my_output'] = txo.is_my_output output['is_my_output'] = txo.is_my_output
if txo.is_my_input is not None: if txo.is_my_input is not None:
output['is_my_input'] = txo.is_my_input output['is_my_input'] = txo.is_my_input
if txo.sent_supports is not None:
output['sent_supports'] = dewies_to_lbc(txo.sent_supports)
if txo.sent_tips is not None:
output['sent_tips'] = dewies_to_lbc(txo.sent_tips)
if txo.received_tips is not None:
output['received_tips'] = dewies_to_lbc(txo.received_tips)
if txo.is_internal_transfer is not None: if txo.is_internal_transfer is not None:
output['is_internal_transfer'] = txo.is_internal_transfer output['is_internal_transfer'] = txo.is_internal_transfer

View file

@ -378,7 +378,7 @@ class StreamManager:
raise ResolveError("cannot download a channel claim, specify a /path") raise ResolveError("cannot download a channel claim, specify a /path")
try: try:
response = await asyncio.wait_for( response = await asyncio.wait_for(
manager.ledger.resolve(wallet.accounts, [uri]), manager.ledger.resolve(wallet.accounts, [uri], include_purchase_receipt=True),
resolve_timeout resolve_timeout
) )
resolved_result = self._convert_to_old_resolve_output(manager, response) resolved_result = self._convert_to_old_resolve_output(manager, response)

View file

@ -490,8 +490,8 @@ class CommandTestCase(IntegrationTestCase):
self.daemon.jsonrpc_stream_update(claim_id, **kwargs), confirm self.daemon.jsonrpc_stream_update(claim_id, **kwargs), confirm
) )
def stream_repost(self, claim_id, name='repost', bid='1.0', confirm=True, **kwargs): async def stream_repost(self, claim_id, name='repost', bid='1.0', confirm=True, **kwargs):
return self.confirm_and_render( return await self.confirm_and_render(
self.daemon.jsonrpc_stream_repost(claim_id=claim_id, name=name, bid=bid, **kwargs), confirm 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 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): async def publish(self, name, *args, confirm=True, **kwargs):
return await self.confirm_and_render( return await self.confirm_and_render(
self.daemon.jsonrpc_publish(name, *args, **kwargs), confirm self.daemon.jsonrpc_publish(name, *args, **kwargs), confirm
@ -560,8 +565,8 @@ class CommandTestCase(IntegrationTestCase):
self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm self.daemon.jsonrpc_wallet_send(*args, **kwargs), confirm
) )
async def resolve(self, uri): async def resolve(self, uri, **kwargs):
return (await self.out(self.daemon.jsonrpc_resolve(uri)))[uri] return (await self.out(self.daemon.jsonrpc_resolve(uri, **kwargs)))[uri]
async def claim_search(self, **kwargs): async def claim_search(self, **kwargs):
return (await self.out(self.daemon.jsonrpc_claim_search(**kwargs)))['items'] return (await self.out(self.daemon.jsonrpc_claim_search(**kwargs)))['items']

View file

@ -446,7 +446,7 @@ class Database(SQLiteMixin):
); );
create index if not exists txo_txid_idx on txo (txid); 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_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_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_txo_type_idx on txo (txo_type);
create index if not exists txo_channel_id_idx on txo (channel_id); 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('limit', None)
constraints.pop('order_by', None) constraints.pop('order_by', None)
count = await self.select_transactions('COUNT(*) as total', **constraints) 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): async def get_transaction(self, **constraints):
txs = await self.get_transactions(limit=1, **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): async def get_txo_count(self, unspent=False, **constraints):
self._clean_txo_constraints_for_aggregation(unspent, constraints) self._clean_txo_constraints_for_aggregation(unspent, constraints)
count = await self.select_txos('COUNT(*) as total', **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): async def get_txo_sum(self, unspent=False, **constraints):
self._clean_txo_constraints_for_aggregation(unspent, constraints) self._clean_txo_constraints_for_aggregation(unspent, constraints)
result = await self.select_txos('SUM(amount) as total', **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): def get_utxos(self, read_only=False, **constraints):
return self.get_txos(unspent=True, read_only=read_only, **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): 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) 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): async def get_address(self, read_only=False, **constraints):
addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints) addresses = await self.get_addresses(read_only=read_only, limit=1, **constraints)

View file

@ -25,7 +25,7 @@ from .account import Account, AddressManager, SingleKey
from .network import Network from .network import Network
from .transaction import Transaction, Output from .transaction import Transaction, Output
from .header import Headers, UnvalidatedHeaders 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 .bip32 import PubKey, PrivateKey
from .coinselection import CoinSelector from .coinselection import CoinSelector
@ -646,7 +646,13 @@ class Ledger(metaclass=LedgerRegistry):
print(record['history'], addresses, tx.id) print(record['history'], addresses, tx.id)
raise asyncio.TimeoutError('Timed out waiting for transaction.') 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_sent_supports=False,
include_sent_tips=False,
include_received_tips=False) -> Tuple[List[Output], dict, int, int]:
encoded_outputs = await query encoded_outputs = await query
outputs = Outputs.from_base64(encoded_outputs or b'') # TODO: why is the server returning None? outputs = Outputs.from_base64(encoded_outputs or b'') # TODO: why is the server returning None?
txs = [] txs = []
@ -654,7 +660,7 @@ class Ledger(metaclass=LedgerRegistry):
txs: List[Transaction] = await asyncio.gather(*( txs: List[Transaction] = await asyncio.gather(*(
self.cache_transaction(*tx) for tx in outputs.txs self.cache_transaction(*tx) for tx in outputs.txs
)) ))
if accounts: if include_purchase_receipt and accounts:
priced_claims = [] priced_claims = []
for tx in txs: for tx in txs:
for txo in tx.outputs: for txo in tx.outputs:
@ -671,11 +677,48 @@ class Ledger(metaclass=LedgerRegistry):
for txo in priced_claims: for txo in priced_claims:
txo.purchase_receipt = receipts.get(txo.claim_id) txo.purchase_receipt = receipts.get(txo.claim_id)
txos, blocked = outputs.inflate(txs) txos, blocked = outputs.inflate(txs)
if any((include_is_my_output, include_sent_supports, include_sent_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_sent_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.sent_supports = supports
if include_sent_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.sent_tips = tips
if include_received_tips:
tips = await self.db.get_txo_sum(
claim_id=txo.claim_id, txo_type=TXO_TYPES['support'],
is_my_input=False, is_my_output=True,
accounts=accounts
)
txo.received_tips = tips
if not include_purchase_receipt:
# txo's are cached across wallets, this prevents
# leaking receipts between wallets
txo.purchase_receipt = None
return txos, blocked, outputs.offset, outputs.total 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) 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." assert len(urls) == len(txos), "Mismatch between urls requested for resolve and responses received."
result = {} result = {}
for url, txo in zip(urls, txos): for url, txo in zip(urls, txos):
@ -688,8 +731,14 @@ class Ledger(metaclass=LedgerRegistry):
result[url] = txo result[url] = txo
return result return result
async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]: async def claim_search(
return await self._inflate_outputs(self.network.claim_search(**kwargs), accounts) self, accounts, include_purchase_receipt=False, include_is_my_output=False,
**kwargs) -> Tuple[List[Output], dict, int, int]:
return await self._inflate_outputs(
self.network.claim_search(**kwargs), accounts,
include_purchase_receipt=include_purchase_receipt,
include_is_my_output=include_is_my_output
)
async def get_claim_by_claim_id(self, accounts, claim_id) -> Output: async def get_claim_by_claim_id(self, accounts, claim_id) -> Output:
for claim in (await self.claim_search(accounts, claim_id=claim_id))[0]: for claim in (await self.claim_search(accounts, claim_id=claim_id))[0]:

View file

@ -208,7 +208,7 @@ class Output(InputOutput):
__slots__ = ( __slots__ = (
'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input', 'amount', 'script', 'is_internal_transfer', 'is_spent', 'is_my_output', 'is_my_input',
'channel', 'private_key', 'meta', 'channel', 'private_key', 'meta', 'sent_supports', 'sent_tips', 'received_tips',
'purchase', 'purchased_claim', 'purchase_receipt', 'purchase', 'purchased_claim', 'purchase_receipt',
'reposted_claim', 'claims', 'reposted_claim', 'claims',
) )
@ -217,6 +217,8 @@ class Output(InputOutput):
tx_ref: TXRef = None, position: int = None, tx_ref: TXRef = None, position: int = None,
is_internal_transfer: Optional[bool] = None, is_spent: Optional[bool] = None, is_internal_transfer: Optional[bool] = None, is_spent: Optional[bool] = None,
is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None, is_my_output: Optional[bool] = None, is_my_input: Optional[bool] = None,
sent_supports: Optional[int] = None, sent_tips: Optional[int] = None,
received_tips: Optional[int] = None,
channel: Optional['Output'] = None, private_key: Optional[str] = None channel: Optional['Output'] = None, private_key: Optional[str] = None
) -> None: ) -> None:
super().__init__(tx_ref, position) super().__init__(tx_ref, position)
@ -226,6 +228,9 @@ class Output(InputOutput):
self.is_spent = is_spent self.is_spent = is_spent
self.is_my_output = is_my_output self.is_my_output = is_my_output
self.is_my_input = is_my_input self.is_my_input = is_my_input
self.sent_supports = sent_supports
self.sent_tips = sent_tips
self.received_tips = received_tips
self.channel = channel self.channel = channel
self.private_key = private_key self.private_key = private_key
self.purchase: 'Output' = None # txo containing purchase metadata self.purchase: 'Output' = None # txo containing purchase metadata
@ -241,11 +246,17 @@ class Output(InputOutput):
self.is_spent = None self.is_spent = None
self.is_my_output = None self.is_my_output = None
self.is_my_input = None self.is_my_input = None
self.sent_supports = None
self.sent_tips = None
self.received_tips = None
else: else:
self.is_internal_transfer = annotated.is_internal_transfer self.is_internal_transfer = annotated.is_internal_transfer
self.is_spent = annotated.is_spent self.is_spent = annotated.is_spent
self.is_my_output = annotated.is_my_output self.is_my_output = annotated.is_my_output
self.is_my_input = annotated.is_my_input self.is_my_input = annotated.is_my_input
self.sent_supports = annotated.sent_supports
self.sent_tips = annotated.sent_tips
self.received_tips = annotated.received_tips
self.channel = annotated.channel if annotated else None self.channel = annotated.channel if annotated else None
self.private_key = annotated.private_key if annotated else None self.private_key = annotated.private_key if annotated else None

View file

@ -142,7 +142,7 @@ class PurchaseCommandTests(CommandTestCase):
self.assertEqual(result['items'][0]['purchase_info'][0]['claim_id'], claim_id2) self.assertEqual(result['items'][0]['purchase_info'][0]['claim_id'], claim_id2)
self.assertEqual(result['items'][2]['purchase_info'][0]['claim_id'], claim_id1) self.assertEqual(result['items'][2]['purchase_info'][0]['claim_id'], claim_id1)
result = await self.claim_search() result = await self.claim_search(include_purchase_receipt=True)
self.assertEqual(result[0]['claim_id'], result[0]['purchase_receipt']['claim_id']) self.assertEqual(result[0]['claim_id'], result[0]['purchase_receipt']['claim_id'])
self.assertEqual(result[1]['claim_id'], result[1]['purchase_receipt']['claim_id']) self.assertEqual(result[1]['claim_id'], result[1]['purchase_receipt']['claim_id'])

View file

@ -263,6 +263,66 @@ class ResolveCommand(BaseResolveTestCase):
await self.resolve('@olds/bad_example') 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('sent_supports', resolve)
self.assertNotIn('sent_tips', resolve)
self.assertNotIn('received_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_sent_supports=True, include_sent_tips=True, include_received_tips=True
)
self.assertEqual('0.0', resolve['sent_supports'])
self.assertEqual('0.0', resolve['sent_tips'])
self.assertEqual('0.0', resolve['received_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_sent_supports=True, include_sent_tips=True, include_received_tips=True
)
self.assertEqual('0.5', resolve['sent_supports'])
self.assertEqual('0.9', resolve['sent_tips'])
self.assertEqual('0.0', resolve['received_tips'])
resolve = await self.resolve(
'priced', include_sent_supports=True, include_sent_tips=True, include_received_tips=True,
wallet_id=wallet2.id
)
self.assertEqual('0.0', resolve['sent_supports'])
self.assertEqual('0.0', resolve['sent_tips'])
self.assertEqual('0.9', resolve['received_tips'])
self.assertEqual('1.4', resolve['meta']['support_amount'])
self.assertNotIn('purchase_receipt', resolve) # prevent leaking cached receipts
class ResolveAfterReorg(BaseResolveTestCase): class ResolveAfterReorg(BaseResolveTestCase):

View file

@ -105,7 +105,7 @@ async def get_mock_wallet(sd_hash, storage, balance=10.0, fee=None):
network=manager.ledger.network, server=('fakespv.lbry.com', 50001) network=manager.ledger.network, server=('fakespv.lbry.com', 50001)
) )
async def mock_resolve(*args): async def mock_resolve(*args, **kwargs):
result = {txo.meta['permanent_url']: txo} result = {txo.meta['permanent_url']: txo}
claims = [ claims = [
StreamManager._convert_to_old_resolve_output(manager, result)[txo.meta['permanent_url']] StreamManager._convert_to_old_resolve_output(manager, result)[txo.meta['permanent_url']]