diff --git a/lbry/lbry/extras/daemon/Daemon.py b/lbry/lbry/extras/daemon/Daemon.py index 3e1bee1de..c5795102c 100644 --- a/lbry/lbry/extras/daemon/Daemon.py +++ b/lbry/lbry/extras/daemon/Daemon.py @@ -1,4 +1,5 @@ import os +import re import asyncio import logging import json @@ -113,6 +114,8 @@ SHORT_ID_LEN = 20 MAX_UPDATE_FEE_ESTIMATE = 0.3 DEFAULT_PAGE_SIZE = 20 +VALID_FULL_CLAIM_ID = re.compile('[0-9a-fA-F]{40}') + def encode_pagination_doc(items): return { @@ -2812,6 +2815,84 @@ class Daemon(metaclass=JSONRPCServerType): f"to update a specific stream claim." ) + @requires(WALLET_COMPONENT, STREAM_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT) + async def jsonrpc_stream_repost(self, name, bid, claim_id, allow_duplicate_name=False, channel_id=None, + channel_name=None, channel_account_id=None, account_id=None, wallet_id=None, + claim_address=None, funding_account_ids=None, preview=False, blocking=False, + **kwargs): + """ + Creates a claim that references an existing stream by its claim id. + + Usage: + stream_repost ( | --name=) ( | --bid=) ( | --claim_id=) + [--allow_duplicate_name=] + [--channel_id= | --channel_name=] + [--channel_account_id=...] + [--account_id=] [--wallet_id=] + [--claim_address=] [--funding_account_ids=...] + [--preview] [--blocking] + + Options: + --name= : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash)) + --bid= : (decimal) amount to back the claim + --allow_duplicate_name= : (bool) create new claim even if one already exists with + given name. default: false. + --channel_id= : (str) claim id of the publisher channel + --channel_name= : (str) name of the publisher channel + --channel_account_id=: (str) one or more account ids for accounts to look in + for channel certificates, defaults to all accounts. + --account_id= : (str) account to use for holding the transaction + --wallet_id= : (str) restrict operation to specific wallet + --funding_account_ids=: (list) ids of accounts to fund this transaction + --claim_address=: (str) address where the claim is sent to, if not specified + it will be determined automatically from the account + --preview : (bool) do not broadcast the transaction + --blocking : (bool) wait until transaction is in mempool + + Returns: {Transaction} + """ + wallet = self.wallet_manager.get_wallet_or_default(wallet_id) + self.valid_stream_name_or_error(name) + account = wallet.get_account_or_default(account_id) + funding_accounts = wallet.get_accounts_or_all(funding_account_ids) + channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True) + amount = self.get_dewies_or_error('bid', bid, positive_value=True) + claim_address = await self.get_receiving_address(claim_address, account) + claims = await account.get_claims(claim_name=name) + if len(claims) > 0: + if not allow_duplicate_name: + raise Exception( + f"You already have a stream claim published under the name '{name}'. " + f"Use --allow-duplicate-name flag to override." + ) + if not VALID_FULL_CLAIM_ID.fullmatch(claim_id): + raise Exception('Invalid claim id. It is expected to be a 40 characters long hexadecimal string.') + + claim = Claim() + claim.repost.reference.claim_id = claim_id + tx = await Transaction.claim_create( + name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel + ) + new_txo = tx.outputs[0] + + if not preview: + new_txo.script.generate() + + if channel: + new_txo.sign(channel) + await tx.sign(funding_accounts) + + if not preview: + await self.broadcast_or_release(tx, blocking) + #await self.storage.save_claims([self._old_get_temp_claim_info( + # tx, new_txo, claim_address, claim, name, dewies_to_lbc(amount) + #)]) + # await self.analytics_manager.send_claim_action('publish') todo: what to send? + else: + await account.ledger.release_tx(tx) + + return tx + @requires(WALLET_COMPONENT, STREAM_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT) async def jsonrpc_stream_create( self, name, bid, file_path, allow_duplicate_name=False, diff --git a/lbry/lbry/testcase.py b/lbry/lbry/testcase.py index c78951559..5e013250e 100644 --- a/lbry/lbry/testcase.py +++ b/lbry/lbry/testcase.py @@ -204,6 +204,16 @@ class CommandTestCase(IntegrationTestCase): """ Synchronous version of `out` method. """ return json.loads(jsonrpc_dumps_pretty(value, ledger=self.ledger))['result'] + async def stream_repost(self, claim_id, name='repost', bid='1.0', **kwargs): + tx = await self.out( + self.daemon.jsonrpc_stream_repost(claim_id=claim_id, name=name, bid=bid, **kwargs) + ) + if kwargs.get('confirm', True): + await self.on_transaction_dict(tx) + await self.generate(1) + await self.on_transaction_dict(tx) + return tx + async def confirm_and_render(self, awaitable, confirm) -> Transaction: tx = await awaitable if confirm: diff --git a/lbry/tests/integration/test_claim_commands.py b/lbry/tests/integration/test_claim_commands.py index 43a658230..3b5e4cd83 100644 --- a/lbry/tests/integration/test_claim_commands.py +++ b/lbry/tests/integration/test_claim_commands.py @@ -714,6 +714,24 @@ class StreamCommands(ClaimTestCase): 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_repost(self): + await self.out(self.channel_create('@goodies', '1.0')) + tx = await self.out(self.stream_create('newstuff', '1.1', channel_name='@goodies')) + claim_id = tx['outputs'][0]['claim_id'] + await self.stream_repost(claim_id, 'newstuff-again', '1.1') + claim_list = await self.out(self.daemon.jsonrpc_claim_list()) + reposts_on_claim_list = [claim for claim in claim_list if claim['value_type'] == 'repost'] + self.assertEqual(len(reposts_on_claim_list), 1) + await self.out(self.channel_create('@reposting-goodies', '1.0')) + await self.stream_repost(claim_id, 'repost-on-channel', '1.1', channel_name='@reposting-goodies') + claim_list = await self.out(self.daemon.jsonrpc_claim_list()) + reposts_on_claim_list = [claim for claim in claim_list if claim['value_type'] == 'repost'] + self.assertEqual(len(reposts_on_claim_list), 2) + signed_reposts = [repost for repost in reposts_on_claim_list if repost.get('is_channel_signature_valid')] + self.assertEqual(len(signed_reposts), 1) + search_results = await self.claim_search(name='newstuff-again') + self.assertEqual(len(search_results), 1) + async def test_publish_updates_file_list(self): tx = await self.out(self.stream_create(title='created')) txo = tx['outputs'][0]