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
@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 <urls>... [--wallet_id=<wallet_id>]
[--include_purchase_receipt]
[--include_is_my_output]
[--include_sent_supports]
[--include_sent_tips]
[--include_received_tips]
Options:
--urls=<urls> : (str, list) one or more urls to resolve
--wallet_id=<wallet_id> : (str) wallet to check for claim purchase reciepts
--urls=<urls> : (str, list) one or more urls to resolve
--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:
Dictionary of results, keyed by url
@ -1002,7 +1019,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 \
@ -2232,7 +2249,7 @@ class Daemon(metaclass=JSONRPCServerType):
[--any_locations=<any_locations>...] [--all_locations=<all_locations>...]
[--not_locations=<not_locations>...]
[--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:
--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
(significant performance boost)
--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]}
"""
@ -5100,8 +5121,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)

View file

@ -174,6 +174,12 @@ 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.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:
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")
try:
response = await asyncio.wait_for(
manager.ledger.resolve(wallet.accounts, [uri]),
manager.ledger.resolve(wallet.accounts, [uri], include_purchase_receipt=True),
resolve_timeout
)
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
)
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']

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_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)

View file

@ -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_sent_supports=False,
include_sent_tips=False,
include_received_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,48 @@ 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_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
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):
@ -688,8 +731,14 @@ class Ledger(metaclass=LedgerRegistry):
result[url] = txo
return result
async def claim_search(self, accounts, **kwargs) -> Tuple[List[Output], dict, int, int]:
return await self._inflate_outputs(self.network.claim_search(**kwargs), accounts)
async def claim_search(
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:
for claim in (await self.claim_search(accounts, claim_id=claim_id))[0]:

View file

@ -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', 'sent_supports', 'sent_tips', 'received_tips',
'purchase', 'purchased_claim', 'purchase_receipt',
'reposted_claim', 'claims',
)
@ -217,6 +217,8 @@ 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,
sent_supports: Optional[int] = None, sent_tips: Optional[int] = None,
received_tips: Optional[int] = None,
channel: Optional['Output'] = None, private_key: Optional[str] = None
) -> None:
super().__init__(tx_ref, position)
@ -226,6 +228,9 @@ class Output(InputOutput):
self.is_spent = is_spent
self.is_my_output = is_my_output
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.private_key = private_key
self.purchase: 'Output' = None # txo containing purchase metadata
@ -241,11 +246,17 @@ class Output(InputOutput):
self.is_spent = None
self.is_my_output = None
self.is_my_input = None
self.sent_supports = None
self.sent_tips = None
self.received_tips = None
else:
self.is_internal_transfer = annotated.is_internal_transfer
self.is_spent = annotated.is_spent
self.is_my_output = annotated.is_my_output
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.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'][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[1]['claim_id'], result[1]['purchase_receipt']['claim_id'])

View file

@ -263,6 +263,66 @@ 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('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):

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)
)
async def mock_resolve(*args):
async def mock_resolve(*args, **kwargs):
result = {txo.meta['permanent_url']: txo}
claims = [
StreamManager._convert_to_old_resolve_output(manager, result)[txo.meta['permanent_url']]