lbry-sdk/lbry/tests/integration/test_claim_commands.py
2019-06-22 23:24:56 -04:00

1114 lines
52 KiB
Python

import os.path
import tempfile
import logging
from binascii import unhexlify
from urllib.request import urlopen
from torba.client.errors import InsufficientFundsError
from lbry.testcase import CommandTestCase
from lbry.wallet.transaction import Transaction
log = logging.getLogger(__name__)
class ClaimTestCase(CommandTestCase):
files_directory = os.path.join(os.path.dirname(__file__), 'files')
video_file_url = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
video_file_name = os.path.join(files_directory, 'ForBiggerEscapes.mp4')
def setUp(self):
if not os.path.exists(self.video_file_name):
if not os.path.exists(self.files_directory):
os.mkdir(self.files_directory)
log.info(f'downloading test video from {self.video_file_name}')
with urlopen(self.video_file_url) as response, \
open(self.video_file_name, 'wb') as video_file:
video_file.write(response.read())
async def image_stream_create(self, name='blank-image', bid='1.0', confirm=True):
with tempfile.NamedTemporaryFile(suffix='.png') as file:
file.write(unhexlify(
b'89504e470d0a1a0a0000000d49484452000000050000000708020000004fc'
b'510b9000000097048597300000b1300000b1301009a9c1800000015494441'
b'5408d763fcffff3f031260624005d4e603004c45030b5286e9ea000000004'
b'9454e44ae426082'
))
file.flush()
tx = await self.out(
self.daemon.jsonrpc_stream_create(
name, bid, file_path=file.name
)
)
if confirm:
await self.on_transaction_dict(tx)
await self.generate(1)
await self.on_transaction_dict(tx)
return tx
async def video_stream_create(self, name='chrome', bid='1.0', confirm=True):
tx = await self.out(
self.daemon.jsonrpc_stream_create(
name, bid, file_path=self.video_file_name
)
)
if confirm:
await self.on_transaction_dict(tx)
await self.generate(1)
await self.on_transaction_dict(tx)
return tx
class ClaimSearchCommand(ClaimTestCase):
async def create_channel(self):
self.channel = await self.channel_create('@abc', '1.0')
self.channel_id = self.channel['outputs'][0]['claim_id']
async def create_lots_of_streams(self):
tx = await self.daemon.jsonrpc_account_fund(None, None, '0.001', outputs=100, broadcast=True)
await self.confirm_tx(tx.id)
# 4 claims per block, 3 blocks. Sorted by height (descending) then claim name (ascending).
self.streams = []
for j in range(3):
same_height_claims = []
for k in range(3):
claim_tx = await self.stream_create(
f'c{j}-{k}', '0.000001', channel_id=self.channel_id, confirm=False)
same_height_claims.append(claim_tx['outputs'][0]['name'])
await self.on_transaction_dict(claim_tx)
claim_tx = await self.stream_create(
f'c{j}-4', '0.000001', channel_id=self.channel_id, confirm=True)
same_height_claims.append(claim_tx['outputs'][0]['name'])
self.streams = same_height_claims + self.streams
async def assertFindsClaim(self, claim, **kwargs):
await self.assertFindsClaims([claim], **kwargs)
async def assertFindsClaims(self, claims, **kwargs):
results = await self.claim_search(**kwargs)
self.assertEqual(len(claims), len(results))
for claim, result in zip(claims, results):
self.assertEqual(
(claim['txid'], claim['outputs'][0]['claim_id']),
(result['txid'], result['claim_id'])
)
async def test_basic_claim_search(self):
await self.create_channel()
channel_txo = self.channel['outputs'][0]
channel2 = await self.channel_create('@abc', '0.1', allow_duplicate_name=True)
channel_txo2 = channel2['outputs'][0]
channel_id2 = channel_txo2['claim_id']
# finding a channel
await self.assertFindsClaims([channel2, self.channel], name='@abc')
await self.assertFindsClaim(self.channel, name='@abc', is_controlling=True)
await self.assertFindsClaim(self.channel, claim_id=self.channel_id)
await self.assertFindsClaim(self.channel, txid=self.channel['txid'], nout=0)
await self.assertFindsClaim(channel2, claim_id=channel_id2)
await self.assertFindsClaim(channel2, txid=channel2['txid'], nout=0)
await self.assertFindsClaim(
channel2, public_key_id=channel_txo2['value']['public_key_id'])
await self.assertFindsClaim(
self.channel, public_key_id=channel_txo['value']['public_key_id'])
signed = await self.stream_create('on-channel-claim', '0.001', channel_id=self.channel_id)
signed2 = await self.stream_create('on-channel-claim', '0.0001', channel_id=channel_id2,
allow_duplicate_name=True)
unsigned = await self.stream_create('unsigned', '0.0001')
# finding claims with and without a channel
await self.assertFindsClaims([signed2, signed], name='on-channel-claim')
await self.assertFindsClaims([signed2, signed], channel_ids=[self.channel_id, channel_id2])
await self.assertFindsClaim(signed, name='on-channel-claim', channel_ids=[self.channel_id])
await self.assertFindsClaim(signed2, name='on-channel-claim', channel_ids=[channel_id2])
await self.assertFindsClaim(unsigned, name='unsigned')
await self.assertFindsClaim(unsigned, txid=unsigned['txid'], nout=0)
await self.assertFindsClaim(unsigned, claim_id=unsigned['outputs'][0]['claim_id'])
two = await self.stream_create('on-channel-claim-2', '0.0001', channel_id=self.channel_id)
three = await self.stream_create('on-channel-claim-3', '0.0001', channel_id=self.channel_id)
# three streams in channel, zero streams in abandoned channel
claims = [three, two, signed]
await self.assertFindsClaims(claims, channel_ids=[self.channel_id])
await self.assertFindsClaims(claims, channel=f"@abc#{self.channel_id}")
await self.assertFindsClaims([three, two, signed2, signed], channel_ids=[channel_id2, self.channel_id])
await self.channel_abandon(claim_id=self.channel_id)
await self.assertFindsClaims([], channel=f"@abc#{self.channel_id}", valid_channel_signatures=True)
await self.assertFindsClaims([], channel_ids=[self.channel_id], valid_channel_signatures=True)
await self.assertFindsClaims([signed2], channel_ids=[channel_id2], valid_channel_signatures=True)
# pass `invalid_channel_signatures=False` to catch a bug in argument processing
await self.assertFindsClaims([signed2], channel_ids=[channel_id2, self.channel_id],
valid_channel_signatures=True, invalid_channel_signatures=False)
# invalid signature still returns channel_id
self.ledger._tx_cache.clear()
invalid_claims = await self.claim_search(invalid_channel_signatures=True)
self.assertEqual(3, len(invalid_claims))
self.assertTrue(all([not c['is_channel_signature_valid'] for c in invalid_claims]))
self.assertEqual({'channel_id': self.channel_id}, invalid_claims[0]['signing_channel'])
valid_claims = await self.claim_search(valid_channel_signatures=True)
self.assertEqual(1, len(valid_claims))
self.assertTrue(all([c['is_channel_signature_valid'] for c in valid_claims]))
self.assertEqual('@abc', valid_claims[0]['signing_channel']['name'])
# abandoned stream won't show up for streams in channel search
await self.stream_abandon(txid=signed2['txid'], nout=0)
await self.assertFindsClaims([], channel_ids=[channel_id2])
async def test_pagination(self):
await self.create_channel()
await self.create_lots_of_streams()
page = await self.claim_search(page_size=20, channel='@abc')
page_claim_ids = [item['name'] for item in page]
self.assertEqual(page_claim_ids, self.streams)
page = await self.claim_search(page_size=6, channel='@abc')
page_claim_ids = [item['name'] for item in page]
self.assertEqual(page_claim_ids, self.streams[:6])
page = await self.claim_search(page=2, page_size=6, channel='@abc')
page_claim_ids = [item['name'] for item in page]
self.assertEqual(page_claim_ids, self.streams[6:])
out_of_bounds = await self.claim_search(page=2, page_size=20, channel='@abc')
self.assertEqual(out_of_bounds, [])
async def test_tag_search(self):
claim1 = await self.stream_create('claim1', tags=['aBc'])
claim2 = await self.stream_create('claim2', tags=['#abc', 'def'])
claim3 = await self.stream_create('claim3', tags=['abc', 'ghi', 'jkl'])
claim4 = await self.stream_create('claim4', tags=['abc\t', 'ghi', 'mno'])
claim5 = await self.stream_create('claim5', tags=['pqr'])
# any_tags
await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], any_tags=['\tabc', 'pqr'])
await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tags=['abc'])
await self.assertFindsClaims([claim4, claim3, claim2, claim1], any_tags=['abc', 'ghi'])
await self.assertFindsClaims([claim4, claim3], any_tags=['ghi'])
await self.assertFindsClaims([claim4, claim3], any_tags=['ghi', 'xyz'])
await self.assertFindsClaims([], any_tags=['xyz'])
# all_tags
await self.assertFindsClaims([], all_tags=['abc', 'pqr'])
await self.assertFindsClaims([claim4, claim3, claim2, claim1], all_tags=['ABC'])
await self.assertFindsClaims([claim4, claim3], all_tags=['abc', 'ghi'])
await self.assertFindsClaims([claim4, claim3], all_tags=['ghi'])
await self.assertFindsClaims([], all_tags=['ghi', 'xyz'])
await self.assertFindsClaims([], all_tags=['xyz'])
# not_tags
await self.assertFindsClaims([], not_tags=['abc', 'pqr'])
await self.assertFindsClaims([claim5], not_tags=['abC'])
await self.assertFindsClaims([claim5], not_tags=['abc', 'ghi'])
await self.assertFindsClaims([claim5, claim2, claim1], not_tags=['ghi'])
await self.assertFindsClaims([claim5, claim2, claim1], not_tags=['ghi', 'xyz'])
await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], not_tags=['xyz'])
# combinations
await self.assertFindsClaims([claim3], all_tags=['abc', 'ghi'], not_tags=['mno'])
await self.assertFindsClaims([claim3], all_tags=['abc', 'ghi'], any_tags=['jkl'], not_tags=['mno'])
await self.assertFindsClaims([claim4, claim3, claim2], all_tags=['abc'], any_tags=['def', 'ghi'])
async def test_order_by(self):
height = await self.ledger.network.get_server_height()
claims = [await self.stream_create(f'claim{i}') for i in range(5)]
await self.assertFindsClaims(claims, order_by=["^height"])
await self.assertFindsClaims(list(reversed(claims)), order_by=["height"])
await self.assertFindsClaims([claims[0]], height=height+1)
await self.assertFindsClaims([claims[4]], height=height+5)
await self.assertFindsClaims(claims[:1], height=f'<{height+2}', order_by=["^height"])
await self.assertFindsClaims(claims[:2], height=f'<={height+2}', order_by=["^height"])
await self.assertFindsClaims(claims[2:], height=f'>{height+2}', order_by=["^height"])
await self.assertFindsClaims(claims[1:], height=f'>={height+2}', order_by=["^height"])
await self.assertFindsClaims(claims, order_by=["^name"])
async def test_search_by_fee(self):
claim1 = await self.stream_create('claim1', fee_amount='1.0', fee_currency='lbc')
claim2 = await self.stream_create('claim2', fee_amount='0.9', fee_currency='lbc')
claim3 = await self.stream_create('claim3', fee_amount='0.5', fee_currency='lbc')
claim4 = await self.stream_create('claim4', fee_amount='0.1', fee_currency='lbc')
claim5 = await self.stream_create('claim5', fee_amount='1.0', fee_currency='usd')
await self.assertFindsClaims([claim5, claim4, claim3, claim2, claim1], fee_amount='>0')
await self.assertFindsClaims([claim4, claim3, claim2, claim1], fee_currency='lbc')
await self.assertFindsClaims([claim3, claim2, claim1], fee_amount='>0.1', fee_currency='lbc')
await self.assertFindsClaims([claim4, claim3, claim2], fee_amount='<1.0', fee_currency='lbc')
await self.assertFindsClaims([claim3], fee_amount='0.5', fee_currency='lbc')
await self.assertFindsClaims([claim5], fee_currency='usd')
await self.assertFindsClaims([], fee_currency='foo')
async def test_claim_type_and_media_type_search(self):
# create an invalid/unknown claim
address = await self.account.receiving.get_or_create_usable_address()
tx = await Transaction.claim_create(
'unknown', b'{"sources":{"lbry_sd_hash":""}}', 1, address, [self.account], self.account)
await tx.sign([self.account])
await self.broadcast(tx)
await self.confirm_tx(tx.id)
octet = await self.stream_create()
video = await self.video_stream_create()
image = await self.image_stream_create()
channel = await self.channel_create()
unknown = self.sout(tx)
# claim_type
await self.assertFindsClaims([image, video, octet, unknown], claim_type='stream')
await self.assertFindsClaims([channel], claim_type='channel')
# stream_type
await self.assertFindsClaims([octet, unknown], stream_types=['binary'])
await self.assertFindsClaims([video], stream_types=['video'])
await self.assertFindsClaims([image], stream_types=['image'])
await self.assertFindsClaims([image, video], stream_types=['video', 'image'])
# stream_type
await self.assertFindsClaims([octet, unknown], media_types=['application/octet-stream'])
await self.assertFindsClaims([video], media_types=['video/mp4'])
await self.assertFindsClaims([image], media_types=['image/png'])
await self.assertFindsClaims([image, video], media_types=['video/mp4', 'image/png'])
class ChannelCommands(CommandTestCase):
async def test_create_channel_names(self):
# claim new name
await self.channel_create('@foo')
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '8.991893')
# fail to claim duplicate
with self.assertRaisesRegex(Exception, "You already have a channel under the name '@foo'."):
await self.channel_create('@foo')
# fail to claim invalid name
with self.assertRaisesRegex(Exception, "Channel names must start with '@' symbol."):
await self.channel_create('foo')
# nothing's changed after failed attempts
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '8.991893')
# succeed overriding duplicate restriction
await self.channel_create('@foo', allow_duplicate_name=True)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
await self.assertBalance(self.account, '7.983786')
async def test_channel_bids(self):
# enough funds
tx = await self.channel_create('@foo', '5.0')
claim_id = tx['outputs'][0]['claim_id']
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '4.991893')
# bid preserved on update
tx = await self.channel_update(claim_id)
self.assertEqual(tx['outputs'][0]['amount'], '5.0')
# bid changed on update
tx = await self.channel_update(claim_id, bid='4.0')
self.assertEqual(tx['outputs'][0]['amount'], '4.0')
await self.assertBalance(self.account, '5.991447')
# not enough funds
with self.assertRaisesRegex(
InsufficientFundsError, "Not enough funds to cover this transaction."):
await self.channel_create('@foo2', '9.0')
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 1)
await self.assertBalance(self.account, '5.991447')
# spend exactly amount available, no change
tx = await self.channel_create('@foo3', '5.981266')
await self.assertBalance(self.account, '0.0')
self.assertEqual(len(tx['outputs']), 1) # no change
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
async def test_setting_channel_fields(self):
values = {
'title': "Cool Channel",
'description': "Best channel on LBRY.",
'thumbnail_url': "https://co.ol/thumbnail.png",
'tags': ["cool", "awesome"],
'languages': ["en-US"],
'locations': ['US::Manchester'],
'email': "human@email.com",
'website_url': "https://co.ol",
'cover_url': "https://co.ol/cover.png",
'featured': ['cafe']
}
fixed_values = values.copy()
fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}
fixed_values['locations'] = [{'country': 'US', 'city': 'Manchester'}]
fixed_values['cover'] = {'url': fixed_values.pop('cover_url')}
# create new channel with all fields set
tx = await self.out(self.channel_create('@bigchannel', **values))
channel = tx['outputs'][0]['value']
self.assertEqual(channel, {
'public_key': channel['public_key'],
'public_key_id': channel['public_key_id'],
**fixed_values
})
# create channel with nothing set
tx = await self.out(self.channel_create('@lightchannel'))
channel = tx['outputs'][0]['value']
self.assertEqual(
channel, {'public_key': channel['public_key'], 'public_key_id': channel['public_key_id']})
# create channel with just a featured claim
tx = await self.out(self.channel_create('@featurechannel', featured='beef'))
txo = tx['outputs'][0]
claim_id, channel = txo['claim_id'], txo['value']
fixed_values['public_key'] = channel['public_key']
fixed_values['public_key_id'] = channel['public_key_id']
self.assertEqual(channel, {
'public_key': fixed_values['public_key'],
'public_key_id': fixed_values['public_key_id'],
'featured': ['beef']
})
# update channel "@featurechannel" setting all fields
tx = await self.out(self.channel_update(claim_id, **values))
channel = tx['outputs'][0]['value']
fixed_values['featured'].insert(0, 'beef') # existing featured claim
self.assertEqual(channel, fixed_values)
# clearing and settings featured content
tx = await self.out(self.channel_update(claim_id, featured='beefcafe', clear_featured=True))
channel = tx['outputs'][0]['value']
fixed_values['featured'] = ['beefcafe']
self.assertEqual(channel, fixed_values)
# reset signing key
tx = await self.out(self.channel_update(claim_id, new_signing_key=True))
channel = tx['outputs'][0]['value']
self.assertNotEqual(channel['public_key'], fixed_values['public_key'])
# replace mode (clears everything except public_key)
tx = await self.out(self.channel_update(claim_id, replace=True, title='foo', email='new@email.com'))
self.assertEqual(tx['outputs'][0]['value'], {
'public_key': channel['public_key'],
'public_key_id': channel['public_key_id'],
'title': 'foo', 'email': 'new@email.com'}
)
# send channel to someone else
new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))
account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id'])
# before sending
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 3)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 0)
other_address = await account2.receiving.get_or_create_usable_address()
tx = await self.out(self.channel_update(claim_id, claim_address=other_address))
# after sending
self.assertEqual(len(await self.daemon.jsonrpc_channel_list()), 2)
self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1)
# shoud not have private key
txo = (await account2.get_channels())[0]
self.assertIsNone(txo.private_key)
# send the private key too
private_key = self.account.get_channel_private_key(unhexlify(channel['public_key']))
account2.add_channel_private_key(private_key)
# now should have private key
txo = (await account2.get_channels())[0]
self.assertIsNotNone(txo.private_key)
async def test_channel_export_import_into_new_account(self):
tx = await self.channel_create('@foo', '1.0')
claim_id = tx['outputs'][0]['claim_id']
channel_private_key = (await self.account.get_channels())[0].private_key
exported_data = await self.out(self.daemon.jsonrpc_channel_export(claim_id))
daemon2 = await self.add_daemon()
# before importing channel
self.assertEqual(1, len(daemon2.default_wallet.accounts))
# importing channel which will create a new single key account
await daemon2.jsonrpc_channel_import(exported_data)
# after import
self.assertEqual(2, len(daemon2.default_wallet.accounts))
new_account = daemon2.default_wallet.accounts[1]
await daemon2.ledger._update_tasks.done.wait()
channels = await new_account.get_channels()
self.assertEqual(1, len(channels))
self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string())
async def test_channel_export_import_into_existing_account(self):
tx = await self.channel_create('@foo', '1.0')
claim_id = tx['outputs'][0]['claim_id']
channel_private_key = (await self.account.get_channels())[0].private_key
exported_data = await self.out(self.daemon.jsonrpc_channel_export(claim_id))
daemon2 = await self.add_daemon(seed=self.account.seed)
await daemon2.ledger._update_tasks.done.wait() # will sync channel previously created
# before importing channel key, has channel without key
self.assertEqual(1, len(daemon2.default_wallet.accounts))
channels = await daemon2.default_account.get_channels()
self.assertEqual(1, len(channels))
self.assertIsNone(channels[0].private_key)
# importing channel will add it to existing account
await daemon2.jsonrpc_channel_import(exported_data)
# after import, still just one account but with private key now
self.assertEqual(1, len(daemon2.default_wallet.accounts))
channels = await daemon2.default_account.get_channels()
self.assertEqual(1, len(channels))
self.assertEqual(channel_private_key.to_string(), channels[0].private_key.to_string())
class StreamCommands(ClaimTestCase):
async def test_create_stream_names(self):
# claim new name
await self.stream_create('foo')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '8.993893')
# fail to claim duplicate
with self.assertRaisesRegex(
Exception, "You already have a stream claim published under the name 'foo'."):
await self.stream_create('foo')
# fail claim starting with @
with self.assertRaisesRegex(
Exception, "Stream names cannot start with '@' symbol."):
await self.stream_create('@foo')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '8.993893')
# succeed overriding duplicate restriction
await self.stream_create('foo', allow_duplicate_name=True)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2)
await self.assertBalance(self.account, '7.987786')
async def test_stream_bids(self):
# enough funds
tx = await self.stream_create('foo', '2.0')
claim_id = tx['outputs'][0]['claim_id']
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '7.993893')
# bid preserved on update
tx = await self.stream_update(claim_id)
self.assertEqual(tx['outputs'][0]['amount'], '2.0')
# bid changed on update
tx = await self.stream_update(claim_id, bid='3.0')
self.assertEqual(tx['outputs'][0]['amount'], '3.0')
await self.assertBalance(self.account, '6.993319')
# not enough funds
with self.assertRaisesRegex(
InsufficientFundsError, "Not enough funds to cover this transaction."):
await self.stream_create('foo2', '9.0')
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 1)
await self.assertBalance(self.account, '6.993319')
# spend exactly amount available, no change
tx = await self.stream_create('foo3', '6.98523')
await self.assertBalance(self.account, '0.0')
self.assertEqual(len(tx['outputs']), 1) # no change
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 2)
async def test_publishing_checks_all_accounts_for_channel(self):
account1_id, account1 = self.account.id, self.account
new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))
account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id'])
await self.out(self.channel_create('@spam', '1.0'))
self.assertEqual('8.989893', await self.daemon.jsonrpc_account_balance())
result = await self.out(self.daemon.jsonrpc_account_send(
'5.0', await self.daemon.jsonrpc_address_unused(account2_id)
))
await self.confirm_tx(result['txid'])
self.assertEqual('3.989769', await self.daemon.jsonrpc_account_balance())
self.assertEqual('5.0', await self.daemon.jsonrpc_account_balance(account2_id))
baz_tx = await self.out(self.channel_create('@baz', '1.0', account_id=account2_id))
baz_id = baz_tx['outputs'][0]['claim_id']
channels = await self.out(self.daemon.jsonrpc_channel_list(account1_id))
self.assertEqual(len(channels), 1)
self.assertEqual(channels[0]['name'], '@spam')
self.assertEqual(channels, await self.out(self.daemon.jsonrpc_channel_list()))
channels = await self.out(self.daemon.jsonrpc_channel_list(account2_id))
self.assertEqual(len(channels), 1)
self.assertEqual(channels[0]['name'], '@baz')
# defaults to using all accounts to lookup channel
await self.stream_create('hovercraft1', '0.1', channel_id=baz_id)
self.assertEqual((await self.claim_search(name='hovercraft1'))[0]['signing_channel']['name'], '@baz')
# lookup by channel_name in all accounts
await self.stream_create('hovercraft2', '0.1', channel_name='@baz')
self.assertEqual((await self.claim_search(name='hovercraft2'))[0]['signing_channel']['name'], '@baz')
# uses only the specific accounts which contains the channel
await self.stream_create('hovercraft3', '0.1', channel_id=baz_id, channel_account_id=[account2_id])
self.assertEqual((await self.claim_search(name='hovercraft3'))[0]['signing_channel']['name'], '@baz')
# lookup by channel_name in specific account
await self.stream_create('hovercraft4', '0.1', channel_name='@baz', channel_account_id=[account2_id])
self.assertEqual((await self.claim_search(name='hovercraft4'))[0]['signing_channel']['name'], '@baz')
# fails when specifying account which does not contain channel
with self.assertRaisesRegex(ValueError, "Couldn't find channel with channel_id"):
await self.stream_create(
'hovercraft5', '0.1', channel_id=baz_id, channel_account_id=[account1_id]
)
# fail with channel_name
with self.assertRaisesRegex(ValueError, "Couldn't find channel with channel_name '@baz'"):
await self.stream_create(
'hovercraft5', '0.1', channel_name='@baz', channel_account_id=[account1_id]
)
async def test_preview_works_with_signed_streams(self):
await self.out(self.channel_create('@spam', '1.0'))
signed = await self.out(self.stream_create('bar', '1.0', channel_name='@spam', preview=True, confirm=False))
self.assertTrue(signed['outputs'][0]['is_channel_signature_valid'])
async def test_publish_updates_file_list(self):
tx = await self.out(self.stream_create(title='created'))
txo = tx['outputs'][0]
claim_id, expected = txo['claim_id'], txo['value']
files = self.sout(self.daemon.jsonrpc_file_list())
self.assertEqual(1, len(files))
self.assertEqual(tx['txid'], files[0]['txid'])
self.assertEqual(expected, files[0]['metadata'])
# update with metadata-only changes
tx = await self.out(self.stream_update(claim_id, title='update 1'))
files = self.sout(self.daemon.jsonrpc_file_list())
expected['title'] = 'update 1'
self.assertEqual(1, len(files))
self.assertEqual(tx['txid'], files[0]['txid'])
self.assertEqual(expected, files[0]['metadata'])
# update with new data
tx = await self.out(self.stream_update(claim_id, title='update 2', data=b'updated data'))
expected = tx['outputs'][0]['value']
files = self.sout(self.daemon.jsonrpc_file_list())
self.assertEqual(1, len(files))
self.assertEqual(tx['txid'], files[0]['txid'])
self.assertEqual(expected, files[0]['metadata'])
async def test_setting_stream_fields(self):
values = {
'title': "Cool Content",
'description': "Best content on LBRY.",
'thumbnail_url': "https://co.ol/thumbnail.png",
'tags': ["cool", "awesome"],
'languages': ["en"],
'locations': ['US:NH:Manchester:03101:42.990605:-71.460989'],
'author': "Jules Verne",
'license': 'Public Domain',
'license_url': "https://co.ol/license",
'release_time': 123456,
'fee_currency': 'usd',
'fee_amount': '2.99',
'fee_address': 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca',
}
fixed_values = values.copy()
fixed_values['locations'] = [{
'country': 'US',
'state': 'NH',
'city': 'Manchester',
'code': '03101',
'latitude': '42.990605',
'longitude': '-71.460989'
}]
fixed_values['thumbnail'] = {'url': fixed_values.pop('thumbnail_url')}
fixed_values['release_time'] = str(values['release_time'])
fixed_values['stream_type'] = 'binary'
fixed_values['source'] = {
'hash': '56bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454f4edd1373e2b64ee2e68350d916e',
'media_type': 'application/octet-stream',
'size': '3'
}
fixed_values['fee'] = {
'address': fixed_values.pop('fee_address'),
'amount': fixed_values.pop('fee_amount'),
'currency': fixed_values.pop('fee_currency').upper()
}
# create new stream with all fields set
tx = await self.out(self.stream_create('big', **values))
stream = tx['outputs'][0]['value']
fixed_values['source']['name'] = stream['source']['name']
fixed_values['source']['sd_hash'] = stream['source']['sd_hash']
self.assertEqual(stream, fixed_values)
# create stream with nothing set
tx = await self.out(self.stream_create('light'))
stream = tx['outputs'][0]['value']
self.assertEqual(
stream, {
'stream_type': 'binary',
'source': {
'size': '3',
'media_type': 'application/octet-stream',
'name': stream['source']['name'],
'hash': '56bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454f4edd1373e2b64ee2e68350d916e',
'sd_hash': stream['source']['sd_hash']
},
}
)
# create stream with just some tags, langs and locations
tx = await self.out(self.stream_create('updated', tags='blah', languages='uk', locations='UA::Kyiv'))
txo = tx['outputs'][0]
claim_id, stream = txo['claim_id'], txo['value']
fixed_values['source']['name'] = stream['source']['name']
fixed_values['source']['sd_hash'] = stream['source']['sd_hash']
self.assertEqual(
stream, {
'stream_type': 'binary',
'source': {
'size': '3',
'media_type': 'application/octet-stream',
'name': fixed_values['source']['name'],
'hash': '56bf5dbae43f77a63d075b0f2ae9c7c3e3098db93779c7f9840da0f4db9c2f8c8454f4edd1373e2b64ee2e68350d916e',
'sd_hash': fixed_values['source']['sd_hash'],
},
'tags': ['blah'],
'languages': ['uk'],
'locations': [{'country': 'UA', 'city': 'Kyiv'}]
}
)
# update stream setting all fields, 'source' doesn't change
tx = await self.out(self.stream_update(claim_id, **values))
stream = tx['outputs'][0]['value']
fixed_values['tags'].insert(0, 'blah') # existing tag
fixed_values['languages'].insert(0, 'uk') # existing language
fixed_values['locations'].insert(0, {'country': 'UA', 'city': 'Kyiv'}) # existing location
self.assertEqual(stream, fixed_values)
# clearing and settings tags, languages and locations
tx = await self.out(self.stream_update(
claim_id, tags='single', clear_tags=True,
languages='pt', clear_languages=True,
locations='BR', clear_locations=True,
))
txo = tx['outputs'][0]
fixed_values['tags'] = ['single']
fixed_values['languages'] = ['pt']
fixed_values['locations'] = [{'country': 'BR'}]
self.assertEqual(txo['value'], fixed_values)
# modifying hash/size/name
fixed_values['source']['name'] = 'changed_name'
fixed_values['source']['hash'] = 'cafebeef'
fixed_values['source']['size'] = '42'
tx = await self.out(self.stream_update(
claim_id, file_name='changed_name', file_hash='cafebeef', file_size=42
))
self.assertEqual(tx['outputs'][0]['value'], fixed_values)
# stream_update re-signs with the same channel
channel_id = (await self.channel_create('@chan'))['outputs'][0]['claim_id']
tx = await self.stream_update(claim_id, channel_id=channel_id)
self.assertEqual(tx['outputs'][0]['signing_channel']['name'], '@chan')
tx = await self.stream_update(claim_id, title='channel re-signs')
self.assertEqual(tx['outputs'][0]['value']['title'], 'channel re-signs')
self.assertEqual(tx['outputs'][0]['signing_channel']['name'], '@chan')
# send claim to someone else
new_account = await self.out(self.daemon.jsonrpc_account_create('second account'))
account2_id, account2 = new_account['id'], self.daemon.get_account_or_error(new_account['id'])
# before sending
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 4)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=account2_id)), 0)
other_address = await account2.receiving.get_or_create_usable_address()
tx = await self.out(self.stream_update(claim_id, claim_address=other_address))
# after sending
self.assertEqual(len(await self.daemon.jsonrpc_claim_list()), 3)
self.assertEqual(len(await self.daemon.jsonrpc_claim_list(account_id=account2_id)), 1)
async def test_setting_fee_fields(self):
tx = await self.out(self.stream_create('paid-stream'))
txo = tx['outputs'][0]
claim_id, stream = txo['claim_id'], txo['value']
fee_address = 'mmCsWAiXMUVecFQ3fVzUwvpT9XFMXno2Ca'
self.assertNotIn('fee', stream)
# --replace=false
# validation
with self.assertRaisesRegex(Exception, 'please specify a fee currency'):
await self.stream_update(claim_id, fee_amount='0.1')
with self.assertRaisesRegex(Exception, 'unknown currency provided: foo'):
await self.stream_update(claim_id, fee_amount='0.1', fee_currency="foo")
with self.assertRaisesRegex(Exception, 'please specify a fee amount'):
await self.stream_update(claim_id, fee_currency='usd')
with self.assertRaisesRegex(Exception, 'please specify a fee amount'):
await self.stream_update(claim_id, fee_address=fee_address)
# set just amount and currency with default address
tx = await self.stream_update(
claim_id, fee_amount='0.99', fee_currency='lbc'
)
self.assertEqual(
tx['outputs'][0]['value']['fee'],
{'amount': '0.99', 'currency': 'LBC', 'address': txo['address']}
)
# set all fee fields
tx = await self.stream_update(
claim_id, fee_amount='0.1', fee_currency='usd', fee_address=fee_address
)
self.assertEqual(
tx['outputs'][0]['value']['fee'],
{'amount': '0.1', 'currency': 'USD', 'address': fee_address}
)
# change just address
tx = await self.stream_update(claim_id, fee_address=txo['address'])
self.assertEqual(
tx['outputs'][0]['value']['fee'],
{'amount': '0.1', 'currency': 'USD', 'address': txo['address']}
)
# change just amount (does not reset fee_address)
tx = await self.stream_update(claim_id, fee_amount='0.2')
self.assertEqual(
tx['outputs'][0]['value']['fee'],
{'amount': '0.2', 'currency': 'USD', 'address': txo['address']}
)
# changing currency without an amount is never allowed, even if previous amount exists
with self.assertRaises(Exception, msg='In order to set a fee currency, please specify a fee amount'):
await self.stream_update(claim_id, fee_currency='usd')
# clearing fee
tx = await self.out(self.stream_update(claim_id, clear_fee=True))
self.assertNotIn('fee', tx['outputs'][0]['value'])
# --replace=true
# set just amount and currency with default address
tx = await self.stream_update(
claim_id, fee_amount='0.99', fee_currency='lbc', replace=True
)
self.assertEqual(
tx['outputs'][0]['value']['fee'],
{'amount': '0.99', 'currency': 'LBC', 'address': txo['address']}
)
# set all fee fields
tx = await self.stream_update(
claim_id, fee_amount='0.1', fee_currency='usd', fee_address=fee_address, replace=True
)
self.assertEqual(
tx['outputs'][0]['value']['fee'],
{'amount': '0.1', 'currency': 'USD', 'address': fee_address}
)
# validation
with self.assertRaisesRegex(Exception, 'please specify a fee currency'):
await self.stream_update(claim_id, fee_amount='0.1', replace=True)
with self.assertRaisesRegex(Exception, 'unknown currency provided: foo'):
await self.stream_update(claim_id, fee_amount='0.1', fee_currency="foo", replace=True)
with self.assertRaisesRegex(Exception, 'please specify a fee amount'):
await self.stream_update(claim_id, fee_currency='usd', replace=True)
with self.assertRaisesRegex(Exception, 'please specify a fee amount'):
await self.stream_update(claim_id, fee_address=fee_address, replace=True)
async def test_automatic_type_and_metadata_detection_for_image(self):
txo = (await self.image_stream_create())['outputs'][0]
self.assertEqual(
txo['value'], {
'source': {
'size': '99',
'name': txo['value']['source']['name'],
'media_type': 'image/png',
'hash': '6c7df435d412c603390f593ef658c199817c7830ba3f16b7eadd8f99fa50e85dbd0d2b3dc61eadc33fe096e3872d1545',
'sd_hash': txo['value']['source']['sd_hash'],
},
'stream_type': 'image',
'image': {
'width': 5,
'height': 7
}
}
)
async def test_automatic_type_and_metadata_detection_for_video(self):
txo = (await self.video_stream_create())['outputs'][0]
self.assertEqual(
txo['value'], {
'source': {
'size': '2299653',
'name': 'ForBiggerEscapes.mp4',
'media_type': 'video/mp4',
'hash': '5f6811c83c1616df06f10bf5309ca61edb5ff949a9c1212ce784602d837bfdfc1c3db1e0580ef7bd1dadde41d8acf315',
'sd_hash': txo['value']['source']['sd_hash'],
},
'stream_type': 'video',
'video': {
'width': 1280,
'height': 720,
'duration': 15
}
}
)
async def test_overriding_automatic_metadata_detection(self):
tx = await self.out(
self.daemon.jsonrpc_stream_create(
'chrome', '1.0', file_path=self.video_file_name, width=99, height=88, duration=9
)
)
txo = tx['outputs'][0]
self.assertEqual(
txo['value'], {
'source': {
'size': '2299653',
'name': 'ForBiggerEscapes.mp4',
'media_type': 'video/mp4',
'hash': '5f6811c83c1616df06f10bf5309ca61edb5ff949a9c1212ce784602d837bfdfc1c3db1e0580ef7bd1dadde41d8acf315',
'sd_hash': txo['value']['source']['sd_hash'],
},
'stream_type': 'video',
'video': {
'width': 99,
'height': 88,
'duration': 9
}
}
)
async def test_replace_mode_preserves_source_and_type(self):
expected = {
'tags': ['blah'],
'languages': ['uk'],
'locations': [{'country': 'UA', 'city': 'Kyiv'}],
'source': {
'size': '2299653',
'name': 'ForBiggerEscapes.mp4',
'media_type': 'video/mp4',
'hash': '5f6811c83c1616df06f10bf5309ca61edb5ff949a9c1212ce784602d837bfdfc1c3db1e0580ef7bd1dadde41d8acf315',
},
'stream_type': 'video',
'video': {
'width': 1280,
'height': 720,
'duration': 15
}
}
channel = await self.channel_create('@chan')
tx = await self.out(self.daemon.jsonrpc_stream_create(
'chrome', '1.0', file_path=self.video_file_name,
tags='blah', languages='uk', locations='UA::Kyiv',
channel_id=channel['outputs'][0]['claim_id']
))
await self.on_transaction_dict(tx)
txo = tx['outputs'][0]
expected['source']['sd_hash'] = txo['value']['source']['sd_hash']
self.assertEqual(txo['value'], expected)
self.assertEqual(txo['signing_channel']['name'], '@chan')
tx = await self.out(self.daemon.jsonrpc_stream_update(
txo['claim_id'], title='new title', replace=True
))
txo = tx['outputs'][0]
expected['title'] = 'new title'
del expected['tags']
del expected['languages']
del expected['locations']
self.assertEqual(txo['value'], expected)
self.assertNotIn('signing_channel', txo)
async def test_create_update_and_abandon_stream(self):
await self.assertBalance(self.account, '10.0')
tx = await self.stream_create(bid='2.5') # creates new claim
claim_id = tx['outputs'][0]['claim_id']
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['claim_info']), 1)
self.assertEqual(txs[0]['confirmations'], 1)
self.assertEqual(txs[0]['claim_info'][0]['balance_delta'], '-2.5')
self.assertEqual(txs[0]['claim_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.020107')
await self.assertBalance(self.account, '7.479893')
self.assertEqual(1, len(self.daemon.jsonrpc_file_list()))
await self.daemon.jsonrpc_file_delete(delete_all=True)
self.assertEqual(0, len(self.daemon.jsonrpc_file_list()))
await self.stream_update(claim_id, bid='1.0') # updates previous claim
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['update_info']), 1)
self.assertEqual(txs[0]['update_info'][0]['balance_delta'], '1.5')
self.assertEqual(txs[0]['update_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.0002165')
await self.assertBalance(self.account, '8.9796765')
await self.stream_abandon(claim_id)
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['abandon_info']), 1)
self.assertEqual(txs[0]['abandon_info'][0]['balance_delta'], '1.0')
self.assertEqual(txs[0]['abandon_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['value'], '0.0')
self.assertEqual(txs[0]['fee'], '-0.000107')
await self.assertBalance(self.account, '9.9795695')
async def test_abandoning_stream_at_loss(self):
await self.assertBalance(self.account, '10.0')
tx = await self.stream_create(bid='0.0001')
await self.assertBalance(self.account, '9.979793')
await self.stream_abandon(tx['outputs'][0]['claim_id'])
await self.assertBalance(self.account, '9.97968399')
async def test_publish(self):
# errors on missing arguments to create a stream
with self.assertRaisesRegex(Exception, "'bid' is a required argument for new publishes."):
await self.daemon.jsonrpc_publish('foo')
with self.assertRaisesRegex(Exception, "'file_path' is a required argument for new publishes."):
await self.daemon.jsonrpc_publish('foo', bid='1.0')
# successfully create stream
with tempfile.NamedTemporaryFile() as file:
file.write(b'hi')
file.flush()
tx1 = await self.publish('foo', bid='1.0', file_path=file.name)
self.assertEqual(1, len(self.daemon.jsonrpc_file_list()))
# doesn't error on missing arguments when doing an update stream
tx2 = await self.publish('foo', tags='updated')
self.assertEqual(1, len(self.daemon.jsonrpc_file_list()))
self.assertEqual(
tx1['outputs'][0]['claim_id'],
tx2['outputs'][0]['claim_id']
)
# update conflict with two claims of the same name
tx3 = await self.stream_create('foo', allow_duplicate_name=True)
with self.assertRaisesRegex(Exception, "There are 2 claims for 'foo'"):
await self.daemon.jsonrpc_publish('foo')
self.assertEqual(2, len(self.daemon.jsonrpc_file_list()))
# abandon duplicate stream
await self.stream_abandon(tx3['outputs'][0]['claim_id'])
# publish to a channel
await self.channel_create('@abc')
tx3 = await self.publish('foo', channel_name='@abc')
self.assertEqual(2, len(self.daemon.jsonrpc_file_list()))
r = await self.resolve('lbry://@abc/foo')
self.assertEqual(
r['lbry://@abc/foo']['claim_id'],
tx3['outputs'][0]['claim_id']
)
# publishing again clears channel
tx4 = await self.publish('foo', languages='uk-UA')
self.assertEqual(2, len(self.daemon.jsonrpc_file_list()))
r = await self.resolve('lbry://foo')
claim = r['lbry://foo']
self.assertEqual(claim['txid'], tx4['outputs'][0]['txid'])
self.assertNotIn('signing_channel', claim)
self.assertEqual(claim['value']['languages'], ['uk-UA'])
class SupportCommands(CommandTestCase):
async def test_regular_supports_and_tip_supports(self):
# account2 will be used to send tips and supports to account1
account2_id = (await self.out(self.daemon.jsonrpc_account_create('second account')))['id']
account2 = self.daemon.get_account_or_error(account2_id)
# send account2 5 LBC out of the 10 LBC in account1
result = await self.out(self.daemon.jsonrpc_account_send(
'5.0', await self.daemon.jsonrpc_address_unused(account2_id)
))
await self.on_transaction_dict(result)
# account1 and account2 balances:
await self.assertBalance(self.account, '4.999876')
await self.assertBalance(account2, '5.0')
# create the claim we'll be tipping and supporting
tx = await self.stream_create()
claim_id = tx['outputs'][0]['claim_id']
# account1 and account2 balances:
await self.assertBalance(self.account, '3.979769')
await self.assertBalance(account2, '5.0')
# send a tip to the claim using account2
tip = await self.out(
self.daemon.jsonrpc_support_create(claim_id, '1.0', True, account2_id)
)
await self.on_transaction_dict(tip)
await self.generate(1)
await self.on_transaction_dict(tip)
# tips don't affect balance so account1 balance is same but account2 balance went down
await self.assertBalance(self.account, '3.979769')
await self.assertBalance(account2, '3.9998585')
# verify that the incoming tip is marked correctly as is_tip=True in account1
txs = await self.out(self.daemon.jsonrpc_transaction_list())
self.assertEqual(len(txs[0]['support_info']), 1)
self.assertEqual(txs[0]['support_info'][0]['balance_delta'], '1.0')
self.assertEqual(txs[0]['support_info'][0]['claim_id'], claim_id)
self.assertEqual(txs[0]['support_info'][0]['is_tip'], True)
self.assertEqual(txs[0]['value'], '1.0')
self.assertEqual(txs[0]['fee'], '0.0')
# verify that the outgoing tip is marked correctly as is_tip=True in account2
txs2 = await self.out(
self.daemon.jsonrpc_transaction_list(account2_id)
)
self.assertEqual(len(txs2[0]['support_info']), 1)
self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-1.0')
self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id)
self.assertEqual(txs2[0]['support_info'][0]['is_tip'], True)
self.assertEqual(txs2[0]['value'], '-1.0')
self.assertEqual(txs2[0]['fee'], '-0.0001415')
# send a support to the claim using account2
support = await self.out(
self.daemon.jsonrpc_support_create(claim_id, '2.0', False, account2_id)
)
await self.on_transaction_dict(support)
await self.generate(1)
await self.on_transaction_dict(support)
# account2 balance went down ~2
await self.assertBalance(self.account, '3.979769')
await self.assertBalance(account2, '1.999717')
# verify that the outgoing support is marked correctly as is_tip=False in account2
txs2 = await self.out(self.daemon.jsonrpc_transaction_list(account2_id))
self.assertEqual(len(txs2[0]['support_info']), 1)
self.assertEqual(txs2[0]['support_info'][0]['balance_delta'], '-2.0')
self.assertEqual(txs2[0]['support_info'][0]['claim_id'], claim_id)
self.assertEqual(txs2[0]['support_info'][0]['is_tip'], False)
self.assertEqual(txs2[0]['value'], '0.0')
self.assertEqual(txs2[0]['fee'], '-0.0001415')