• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lbryio / lbry-sdk / 4599645360

pending completion
4599645360

push

github

GitHub
Bump cryptography from 2.5 to 39.0.1

2807 of 6557 branches covered (42.81%)

Branch coverage included in aggregate %.

12289 of 19915 relevant lines covered (61.71%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

24.29
/lbry/extras/daemon/daemon.py
1
import linecache
1✔
2
import os
1✔
3
import re
1✔
4
import asyncio
1✔
5
import logging
1✔
6
import json
1✔
7
import time
1✔
8
import inspect
1✔
9
import typing
1✔
10
import random
1✔
11
import tracemalloc
1✔
12
import itertools
1✔
13
from urllib.parse import urlencode, quote
1✔
14
from typing import Callable, Optional, List
1✔
15
from binascii import hexlify, unhexlify
1✔
16
from traceback import format_exc
1✔
17
from functools import wraps, partial
1✔
18

19
import base58
1✔
20
from aiohttp import web
1✔
21
from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter
1✔
22
from google.protobuf.message import DecodeError
1✔
23

24
from lbry.wallet import (
1✔
25
    Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic,
26
    Transaction, Output, Input, Account, database
27
)
28
from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc
1✔
29
from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES
1✔
30
from lbry.wallet.bip32 import PrivateKey
1✔
31
from lbry.crypto.base58 import Base58
1✔
32

33
from lbry import utils
1✔
34
from lbry.conf import Config, Setting, NOT_SET
1✔
35
from lbry.blob.blob_file import is_valid_blobhash, BlobBuffer
1✔
36
from lbry.blob_exchange.downloader import download_blob
1✔
37
from lbry.dht.peer import make_kademlia_peer
1✔
38
from lbry.error import (
1✔
39
    DownloadSDTimeoutError, ComponentsNotStartedError, ComponentStartConditionNotMetError,
40
    CommandDoesNotExistError, BaseError, WalletNotFoundError, WalletAlreadyLoadedError, WalletAlreadyExistsError,
41
    ConflictingInputValueError, AlreadyPurchasedError, PrivateKeyNotFoundError, InputStringIsBlankError,
42
    InputValueError
43
)
44
from lbry.extras import system_info
1✔
45
from lbry.extras.daemon import analytics
1✔
46
from lbry.extras.daemon.components import WALLET_COMPONENT, DATABASE_COMPONENT, DHT_COMPONENT, BLOB_COMPONENT
1✔
47
from lbry.extras.daemon.components import FILE_MANAGER_COMPONENT, DISK_SPACE_COMPONENT, TRACKER_ANNOUNCER_COMPONENT
1✔
48
from lbry.extras.daemon.components import EXCHANGE_RATE_MANAGER_COMPONENT, UPNP_COMPONENT
1✔
49
from lbry.extras.daemon.componentmanager import RequiredCondition
1✔
50
from lbry.extras.daemon.componentmanager import ComponentManager
1✔
51
from lbry.extras.daemon.json_response_encoder import JSONResponseEncoder
1✔
52
from lbry.extras.daemon.undecorated import undecorated
1✔
53
from lbry.extras.daemon.security import ensure_request_allowed
1✔
54
from lbry.file_analysis import VideoFileAnalyzer
1✔
55
from lbry.schema.claim import Claim
1✔
56
from lbry.schema.url import URL
1✔
57

58

59
if typing.TYPE_CHECKING:
1!
60
    from lbry.blob.blob_manager import BlobManager
×
61
    from lbry.dht.node import Node
×
62
    from lbry.extras.daemon.components import UPnPComponent, DiskSpaceManager
×
63
    from lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager
×
64
    from lbry.extras.daemon.storage import SQLiteStorage
×
65
    from lbry.wallet import WalletManager, Ledger
×
66
    from lbry.file.file_manager import FileManager
×
67

68
log = logging.getLogger(__name__)
1✔
69

70
RANGE_FIELDS = {
1✔
71
    'height', 'creation_height', 'activation_height', 'expiration_height',
72
    'timestamp', 'creation_timestamp', 'duration', 'release_time', 'fee_amount',
73
    'tx_position', 'repost_count', 'limit_claims_per_channel',
74
    'amount', 'effective_amount', 'support_amount',
75
    'trending_score', 'censor_type', 'tx_num'
76
}
77
MY_RANGE_FIELDS = RANGE_FIELDS - {"limit_claims_per_channel"}
1✔
78
REPLACEMENTS = {
1✔
79
    'claim_name': 'normalized_name',
80
    'name': 'normalized_name',
81
    'txid': 'tx_id',
82
    'nout': 'tx_nout',
83
    'trending_group': 'trending_score',
84
    'trending_mixed': 'trending_score',
85
    'trending_global': 'trending_score',
86
    'trending_local': 'trending_score',
87
    'reposted': 'repost_count',
88
    'stream_types': 'stream_type',
89
    'media_types': 'media_type',
90
    'valid_channel_signature': 'is_signature_valid'
91
}
92

93

94
def is_transactional_function(name):
1✔
95
    for action in ('create', 'update', 'abandon', 'send', 'fund'):
1✔
96
        if action in name:
1!
97
            return True
×
98

99

100
def requires(*components, **conditions):
1✔
101
    if conditions and ["conditions"] != list(conditions.keys()):
1!
102
        raise SyntaxError("invalid conditions argument")
×
103
    condition_names = conditions.get("conditions", [])
1✔
104

105
    def _wrap(method):
1✔
106
        @wraps(method)
1✔
107
        def _inner(*args, **kwargs):
1✔
108
            component_manager = args[0].component_manager
1✔
109
            for condition_name in condition_names:
1!
110
                condition_result, err_msg = component_manager.evaluate_condition(condition_name)
×
111
                if not condition_result:
×
112
                    raise ComponentStartConditionNotMetError(err_msg)
×
113
            if not component_manager.all_components_running(*components):
1!
114
                raise ComponentsNotStartedError(
×
115
                    f"the following required components have not yet started: {json.dumps(components)}"
116
                )
117
            return method(*args, **kwargs)
1✔
118

119
        return _inner
1✔
120

121
    return _wrap
1✔
122

123

124
def deprecated(new_command=None):
1✔
125
    def _deprecated_wrapper(f):
1✔
126
        f.new_command = new_command
1✔
127
        f._deprecated = True
1✔
128
        return f
1✔
129

130
    return _deprecated_wrapper
1✔
131

132

133
INITIALIZING_CODE = 'initializing'
1✔
134

135
# TODO: make this consistent with the stages in Downloader.py
136
DOWNLOAD_METADATA_CODE = 'downloading_metadata'
1✔
137
DOWNLOAD_TIMEOUT_CODE = 'timeout'
1✔
138
DOWNLOAD_RUNNING_CODE = 'running'
1✔
139
DOWNLOAD_STOPPED_CODE = 'stopped'
1✔
140
STREAM_STAGES = [
1✔
141
    (INITIALIZING_CODE, 'Initializing'),
142
    (DOWNLOAD_METADATA_CODE, 'Downloading metadata'),
143
    (DOWNLOAD_RUNNING_CODE, 'Started %s, got %s/%s blobs, stream status: %s'),
144
    (DOWNLOAD_STOPPED_CODE, 'Paused stream'),
145
    (DOWNLOAD_TIMEOUT_CODE, 'Stream timed out')
146
]
147

148
SHORT_ID_LEN = 20
1✔
149
MAX_UPDATE_FEE_ESTIMATE = 0.3
1✔
150
DEFAULT_PAGE_SIZE = 20
1✔
151

152
VALID_FULL_CLAIM_ID = re.compile('[0-9a-fA-F]{40}')
1✔
153

154

155
def encode_pagination_doc(items):
1✔
156
    return {
×
157
        "page": "Page number of the current items.",
158
        "page_size": "Number of items to show on a page.",
159
        "total_pages": "Total number of pages.",
160
        "total_items": "Total number of items.",
161
        "items": [items],
162
    }
163

164

165
async def paginate_rows(get_records: Callable, get_record_count: Optional[Callable],
1✔
166
                        page: Optional[int], page_size: Optional[int], **constraints):
167
    page = max(1, page or 1)
×
168
    page_size = max(1, page_size or DEFAULT_PAGE_SIZE)
×
169
    constraints.update({
×
170
        "offset": page_size * (page - 1),
171
        "limit": page_size
172
    })
173
    items = await get_records(**constraints)
×
174
    result = {"items": items, "page": page, "page_size": page_size}
×
175
    if get_record_count is not None:
×
176
        total_items = await get_record_count(**constraints)
×
177
        result["total_pages"] = int((total_items + (page_size - 1)) / page_size)
×
178
        result["total_items"] = total_items
×
179
    return result
×
180

181

182
def paginate_list(items: List, page: Optional[int], page_size: Optional[int]):
1✔
183
    page = max(1, page or 1)
×
184
    page_size = max(1, page_size or DEFAULT_PAGE_SIZE)
×
185
    total_items = len(items)
×
186
    offset = page_size * (page - 1)
×
187
    subitems = []
×
188
    if offset <= total_items:
×
189
        subitems = items[offset:offset+page_size]
×
190
    return {
×
191
        "items": subitems,
192
        "total_pages": int((total_items + (page_size - 1)) / page_size),
193
        "total_items": total_items,
194
        "page": page, "page_size": page_size
195
    }
196

197

198
DHT_HAS_CONTACTS = "dht_has_contacts"
1✔
199

200

201
class DHTHasContacts(RequiredCondition):
1✔
202
    name = DHT_HAS_CONTACTS
1✔
203
    component = DHT_COMPONENT
1✔
204
    message = "your node is not connected to the dht"
1✔
205

206
    @staticmethod
1✔
207
    def evaluate(component):
1✔
208
        return len(component.contacts) > 0
×
209

210

211
class JSONRPCError:
1✔
212
    # http://www.jsonrpc.org/specification#error_object
213
    CODE_PARSE_ERROR = -32700  # Invalid JSON. Error while parsing the JSON text.
1✔
214
    CODE_INVALID_REQUEST = -32600  # The JSON sent is not a valid Request object.
1✔
215
    CODE_METHOD_NOT_FOUND = -32601  # The method does not exist / is not available.
1✔
216
    CODE_INVALID_PARAMS = -32602  # Invalid method parameter(s).
1✔
217
    CODE_INTERNAL_ERROR = -32603  # Internal JSON-RPC error (I think this is like a 500?)
1✔
218
    CODE_APPLICATION_ERROR = -32500  # Generic error with our app??
1✔
219
    CODE_AUTHENTICATION_ERROR = -32501  # Authentication failed
1✔
220

221
    MESSAGES = {
1✔
222
        CODE_PARSE_ERROR: "Parse Error. Data is not valid JSON.",
223
        CODE_INVALID_REQUEST: "JSON data is not a valid Request",
224
        CODE_METHOD_NOT_FOUND: "Method Not Found",
225
        CODE_INVALID_PARAMS: "Invalid Params",
226
        CODE_INTERNAL_ERROR: "Internal Error",
227
        CODE_AUTHENTICATION_ERROR: "Authentication Failed",
228
    }
229

230
    HTTP_CODES = {
1✔
231
        CODE_INVALID_REQUEST: 400,
232
        CODE_PARSE_ERROR: 400,
233
        CODE_INVALID_PARAMS: 400,
234
        CODE_METHOD_NOT_FOUND: 404,
235
        CODE_INTERNAL_ERROR: 500,
236
        CODE_APPLICATION_ERROR: 500,
237
        CODE_AUTHENTICATION_ERROR: 401,
238
    }
239

240
    def __init__(self, code: int, message: str, data: dict = None):
1✔
241
        assert code and isinstance(code, int), "'code' must be an int"
×
242
        assert message and isinstance(message, str), "'message' must be a string"
×
243
        assert data is None or isinstance(data, dict), "'data' must be None or a dict"
×
244
        self.code = code
×
245
        self.message = message
×
246
        self.data = data or {}
×
247

248
    def to_dict(self):
1✔
249
        return {
×
250
            'code': self.code,
251
            'message': self.message,
252
            'data': self.data,
253
        }
254

255
    @staticmethod
1✔
256
    def filter_traceback(traceback):
1✔
257
        result = []
×
258
        if traceback is not None:
×
259
            result = trace_lines = traceback.split("\n")
×
260
            for i, t in enumerate(trace_lines):
×
261
                if "--- <exception caught here> ---" in t:
×
262
                    if len(trace_lines) > i + 1:
×
263
                        result = [j for j in trace_lines[i + 1:] if j]
×
264
                        break
×
265
        return result
×
266

267
    @classmethod
1✔
268
    def create_command_exception(cls, command, args, kwargs, exception, traceback):
1✔
269
        if 'password' in kwargs and isinstance(kwargs['password'], str):
×
270
            kwargs['password'] = '*'*len(kwargs['password'])
×
271
        return cls(
×
272
            cls.CODE_APPLICATION_ERROR, str(exception), {
273
                'name': exception.__class__.__name__,
274
                'traceback': cls.filter_traceback(traceback),
275
                'command': command,
276
                'args': args,
277
                'kwargs': kwargs,
278
            }
279
        )
280

281

282
class UnknownAPIMethodError(Exception):
1✔
283
    pass
1✔
284

285

286
def jsonrpc_dumps_pretty(obj, **kwargs):
1✔
287
    if isinstance(obj, JSONRPCError):
1!
288
        data = {"jsonrpc": "2.0", "error": obj.to_dict()}
×
289
    else:
290
        data = {"jsonrpc": "2.0", "result": obj}
1✔
291
    return json.dumps(data, cls=JSONResponseEncoder, sort_keys=True, indent=2, **kwargs) + "\n"
1✔
292

293

294
def trap(err, *to_trap):
1✔
295
    err.trap(*to_trap)
×
296

297

298
class JSONRPCServerType(type):
1✔
299
    def __new__(mcs, name, bases, newattrs):
1✔
300
        klass = type.__new__(mcs, name, bases, newattrs)
1✔
301
        klass.callable_methods = {}
1✔
302
        klass.deprecated_methods = {}
1✔
303

304
        for methodname in dir(klass):
1✔
305
            if methodname.startswith("jsonrpc_"):
1✔
306
                method = getattr(klass, methodname)
1✔
307
                if not hasattr(method, '_deprecated'):
1✔
308
                    klass.callable_methods.update({methodname.split("jsonrpc_")[1]: method})
1✔
309
                else:
310
                    klass.deprecated_methods.update({methodname.split("jsonrpc_")[1]: method})
1✔
311
        return klass
1✔
312

313

314
HISTOGRAM_BUCKETS = (
1✔
315
    .005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 60.0, float('inf')
316
)
317

318

319
class Daemon(metaclass=JSONRPCServerType):
1✔
320
    """
321
    LBRYnet daemon, a jsonrpc interface to lbry functions
322
    """
323
    callable_methods: dict
1✔
324
    deprecated_methods: dict
1✔
325

326
    pending_requests_metric = Gauge(
1✔
327
        "pending_requests", "Number of running api requests", namespace="daemon_api",
328
        labelnames=("method",)
329
    )
330

331
    requests_count_metric = Counter(
1✔
332
        "requests_count", "Number of requests received", namespace="daemon_api",
333
        labelnames=("method",)
334
    )
335
    failed_request_metric = Counter(
1✔
336
        "failed_request_count", "Number of failed requests", namespace="daemon_api",
337
        labelnames=("method",)
338
    )
339
    cancelled_request_metric = Counter(
1✔
340
        "cancelled_request_count", "Number of cancelled requests", namespace="daemon_api",
341
        labelnames=("method",)
342
    )
343
    response_time_metric = Histogram(
1✔
344
        "response_time", "Response times", namespace="daemon_api", buckets=HISTOGRAM_BUCKETS,
345
        labelnames=("method",)
346
    )
347

348
    def __init__(self, conf: Config, component_manager: typing.Optional[ComponentManager] = None):
1✔
349
        self.conf = conf
1✔
350
        self.platform_info = system_info.get_platform()
1✔
351
        self._video_file_analyzer = VideoFileAnalyzer(conf)
1✔
352
        self._node_id = None
1✔
353
        self._installation_id = None
1✔
354
        self.session_id = base58.b58encode(utils.generate_id()).decode()
1✔
355
        self.analytics_manager = analytics.AnalyticsManager(conf, self.installation_id, self.session_id)
1✔
356
        self.component_manager = component_manager or ComponentManager(
1✔
357
            conf, analytics_manager=self.analytics_manager,
358
            skip_components=conf.components_to_skip or []
359
        )
360
        self.component_startup_task = None
1✔
361

362
        logging.getLogger('aiohttp.access').setLevel(logging.WARN)
1✔
363
        rpc_app = web.Application()
1✔
364
        rpc_app.router.add_get('/lbryapi', self.handle_old_jsonrpc)
1✔
365
        rpc_app.router.add_post('/lbryapi', self.handle_old_jsonrpc)
1✔
366
        rpc_app.router.add_post('/', self.handle_old_jsonrpc)
1✔
367
        rpc_app.router.add_options('/', self.add_cors_headers)
1✔
368
        self.rpc_runner = web.AppRunner(rpc_app)
1✔
369

370
        streaming_app = web.Application()
1✔
371
        streaming_app.router.add_get('/get/{claim_name}', self.handle_stream_get_request)
1✔
372
        streaming_app.router.add_get('/get/{claim_name}/{claim_id}', self.handle_stream_get_request)
1✔
373
        streaming_app.router.add_get('/stream/{sd_hash}', self.handle_stream_range_request)
1✔
374
        self.streaming_runner = web.AppRunner(streaming_app)
1✔
375

376
        prom_app = web.Application()
1✔
377
        prom_app.router.add_get('/metrics', self.handle_metrics_get_request)
1✔
378
        self.metrics_runner = web.AppRunner(prom_app)
1✔
379

380
    @property
1✔
381
    def dht_node(self) -> typing.Optional['Node']:
1✔
382
        return self.component_manager.get_component(DHT_COMPONENT)
×
383

384
    @property
1✔
385
    def wallet_manager(self) -> typing.Optional['WalletManager']:
1✔
386
        return self.component_manager.get_component(WALLET_COMPONENT)
×
387

388
    @property
1✔
389
    def storage(self) -> typing.Optional['SQLiteStorage']:
1✔
390
        return self.component_manager.get_component(DATABASE_COMPONENT)
×
391

392
    @property
1✔
393
    def file_manager(self) -> typing.Optional['FileManager']:
1✔
394
        return self.component_manager.get_component(FILE_MANAGER_COMPONENT)
×
395

396
    @property
1✔
397
    def exchange_rate_manager(self) -> typing.Optional['ExchangeRateManager']:
1✔
398
        return self.component_manager.get_component(EXCHANGE_RATE_MANAGER_COMPONENT)
×
399

400
    @property
1✔
401
    def blob_manager(self) -> typing.Optional['BlobManager']:
1✔
402
        return self.component_manager.get_component(BLOB_COMPONENT)
×
403

404
    @property
1✔
405
    def disk_space_manager(self) -> typing.Optional['DiskSpaceManager']:
1✔
406
        return self.component_manager.get_component(DISK_SPACE_COMPONENT)
×
407

408
    @property
1✔
409
    def upnp(self) -> typing.Optional['UPnPComponent']:
1✔
410
        return self.component_manager.get_component(UPNP_COMPONENT)
×
411

412
    @classmethod
1✔
413
    def get_api_definitions(cls):
1✔
414
        prefix = 'jsonrpc_'
1✔
415
        not_grouped = ['routing_table_get', 'ffmpeg_find']
1✔
416
        api = {
1✔
417
            'groups': {
418
                group_name[:-len('_DOC')].lower(): getattr(cls, group_name).strip()
419
                for group_name in dir(cls) if group_name.endswith('_DOC')
420
            },
421
            'commands': {}
422
        }
423
        for jsonrpc_method in dir(cls):
1✔
424
            if jsonrpc_method.startswith(prefix):
1✔
425
                full_name = jsonrpc_method[len(prefix):]
1✔
426
                method = getattr(cls, jsonrpc_method)
1✔
427
                if full_name in not_grouped:
1✔
428
                    name_parts = [full_name]
1✔
429
                else:
430
                    name_parts = full_name.split('_', 1)
1✔
431
                if len(name_parts) == 1:
1✔
432
                    group = None
1✔
433
                    name, = name_parts
1✔
434
                elif len(name_parts) == 2:
1!
435
                    group, name = name_parts
1✔
436
                    assert group in api['groups'], \
1✔
437
                        f"Group {group} does not have doc string for command {full_name}."
438
                else:
439
                    raise NameError(f'Could not parse method name: {jsonrpc_method}')
×
440
                api['commands'][full_name] = {
1✔
441
                    'api_method_name': full_name,
442
                    'name': name,
443
                    'group': group,
444
                    'doc': method.__doc__,
445
                    'method': method,
446
                }
447
                if hasattr(method, '_deprecated'):
1✔
448
                    api['commands'][full_name]['replaced_by'] = method.new_command
1✔
449

450
        for command in api['commands'].values():
1✔
451
            if 'replaced_by' in command:
1✔
452
                command['replaced_by'] = api['commands'][command['replaced_by']]
1✔
453

454
        return api
1✔
455

456
    @property
1✔
457
    def db_revision_file_path(self):
1✔
458
        return os.path.join(self.conf.data_dir, 'db_revision')
×
459

460
    @property
1✔
461
    def installation_id(self):
1✔
462
        install_id_filename = os.path.join(self.conf.data_dir, "install_id")
1✔
463
        if not self._installation_id:
1✔
464
            if os.path.isfile(install_id_filename):
1!
465
                with open(install_id_filename, "r") as install_id_file:
×
466
                    self._installation_id = str(install_id_file.read()).strip()
×
467
        if not self._installation_id:
1✔
468
            self._installation_id = base58.b58encode(utils.generate_id()).decode()
1✔
469
            with open(install_id_filename, "w") as install_id_file:
1✔
470
                install_id_file.write(self._installation_id)
1✔
471
        return self._installation_id
1✔
472

473
    def ensure_data_dir(self):
1✔
474
        if not os.path.isdir(self.conf.data_dir):
1!
475
            os.makedirs(self.conf.data_dir)
×
476
        if not os.path.isdir(os.path.join(self.conf.data_dir, "blobfiles")):
1!
477
            os.makedirs(os.path.join(self.conf.data_dir, "blobfiles"))
1✔
478
        return self.conf.data_dir
1✔
479

480
    def ensure_wallet_dir(self):
1✔
481
        if not os.path.isdir(self.conf.wallet_dir):
1!
482
            os.makedirs(self.conf.wallet_dir)
1✔
483

484
    def ensure_download_dir(self):
1✔
485
        if not os.path.isdir(self.conf.download_dir):
1!
486
            os.makedirs(self.conf.download_dir)
1✔
487

488
    async def start(self):
1✔
489
        log.info("Starting LBRYNet Daemon")
1✔
490
        log.debug("Settings: %s", json.dumps(self.conf.settings_dict, indent=2))
1✔
491
        log.info("Platform: %s", json.dumps(self.platform_info, indent=2))
1✔
492

493
        await self.analytics_manager.send_server_startup()
1✔
494
        await self.rpc_runner.setup()
1✔
495
        await self.streaming_runner.setup()
1✔
496
        await self.metrics_runner.setup()
1✔
497

498
        try:
1✔
499
            rpc_site = web.TCPSite(self.rpc_runner, self.conf.api_host, self.conf.api_port, shutdown_timeout=.5)
1✔
500
            await rpc_site.start()
1✔
501
            log.info('RPC server listening on TCP %s:%i', *rpc_site._server.sockets[0].getsockname()[:2])
1✔
502
        except OSError as e:
×
503
            log.error('RPC server failed to bind TCP %s:%i', self.conf.api_host, self.conf.api_port)
×
504
            await self.analytics_manager.send_server_startup_error(str(e))
×
505
            raise SystemExit()
×
506

507
        try:
1✔
508
            streaming_site = web.TCPSite(self.streaming_runner, self.conf.streaming_host, self.conf.streaming_port,
1✔
509
                                         shutdown_timeout=.5)
510
            await streaming_site.start()
1✔
511
            log.info('media server listening on TCP %s:%i', *streaming_site._server.sockets[0].getsockname()[:2])
1✔
512

513
        except OSError as e:
×
514
            log.error('media server failed to bind TCP %s:%i', self.conf.streaming_host, self.conf.streaming_port)
×
515
            await self.analytics_manager.send_server_startup_error(str(e))
×
516
            raise SystemExit()
×
517

518
        if self.conf.prometheus_port:
1!
519
            try:
×
520
                prom_site = web.TCPSite(self.metrics_runner, "0.0.0.0", self.conf.prometheus_port, shutdown_timeout=.5)
×
521
                await prom_site.start()
×
522
                log.info('metrics server listening on TCP %s:%i', *prom_site._server.sockets[0].getsockname()[:2])
×
523
            except OSError as e:
×
524
                log.error('metrics server failed to bind TCP :%i', self.conf.prometheus_port)
×
525
                await self.analytics_manager.send_server_startup_error(str(e))
×
526
                raise SystemExit()
×
527

528
        try:
1✔
529
            await self.initialize()
1✔
530
        except asyncio.CancelledError:
×
531
            log.info("shutting down before finished starting")
×
532
            await self.analytics_manager.send_server_startup_error("shutting down before finished starting")
×
533
            raise
×
534
        except Exception as e:
×
535
            await self.analytics_manager.send_server_startup_error(str(e))
×
536
            log.exception('Failed to start lbrynet')
×
537
            raise SystemExit()
×
538

539
        await self.analytics_manager.send_server_startup_success()
1✔
540

541
    async def initialize(self):
1✔
542
        self.ensure_data_dir()
1✔
543
        self.ensure_wallet_dir()
1✔
544
        self.ensure_download_dir()
1✔
545
        if not self.analytics_manager.is_started:
1!
546
            await self.analytics_manager.start()
1✔
547
        self.component_startup_task = asyncio.create_task(self.component_manager.start())
1✔
548
        await self.component_startup_task
1✔
549

550
    async def stop(self):
1✔
551
        if self.component_startup_task is not None:
1✔
552
            if self.component_startup_task.done():
1!
553
                await self.component_manager.stop()
1✔
554
            else:
555
                self.component_startup_task.cancel()
×
556
                # the wallet component might have not started
557
                try:
×
558
                    wallet_component = self.component_manager.get_actual_component('wallet')
×
559
                except NameError:
×
560
                    pass
×
561
                else:
562
                    await wallet_component.stop()
×
563
                await self.component_manager.stop()
×
564
        log.info("stopped api components")
1✔
565
        await self.rpc_runner.cleanup()
1✔
566
        await self.streaming_runner.cleanup()
1✔
567
        await self.metrics_runner.cleanup()
1✔
568
        log.info("stopped api server")
1✔
569
        if self.analytics_manager.is_started:
1✔
570
            self.analytics_manager.stop()
1✔
571
        log.info("finished shutting down")
1✔
572

573
    async def add_cors_headers(self, request):
1✔
574
        if self.conf.allowed_origin:
1!
575
            return web.Response(
1✔
576
                headers={
577
                    'Access-Control-Allow-Origin': self.conf.allowed_origin,
578
                    'Access-Control-Allow-Methods': self.conf.allowed_origin,
579
                    'Access-Control-Allow-Headers': self.conf.allowed_origin,
580
                }
581
            )
582
        return None
×
583

584
    async def handle_old_jsonrpc(self, request):
1✔
585
        ensure_request_allowed(request, self.conf)
1✔
586
        data = await request.json()
1✔
587
        params = data.get('params', {})
1✔
588
        include_protobuf = params.pop('include_protobuf', False) if isinstance(params, dict) else False
1✔
589
        result = await self._process_rpc_call(data)
1✔
590
        ledger = None
1✔
591
        if 'wallet' in self.component_manager.get_components_status():
1!
592
            # self.ledger only available if wallet component is not skipped
593
            ledger = self.ledger
×
594
        try:
1✔
595
            encoded_result = jsonrpc_dumps_pretty(
1✔
596
                result, ledger=ledger, include_protobuf=include_protobuf)
597
        except Exception:
×
598
            log.exception('Failed to encode JSON RPC result:')
×
599
            encoded_result = jsonrpc_dumps_pretty(JSONRPCError(
×
600
                JSONRPCError.CODE_APPLICATION_ERROR,
601
                'After successfully executing the command, failed to encode result for JSON RPC response.',
602
                {'traceback': format_exc()}
603
            ), ledger=ledger)
604
        headers = {}
1✔
605
        if self.conf.allowed_origin:
1!
606
            headers.update({
1✔
607
                'Access-Control-Allow-Origin': self.conf.allowed_origin,
608
                'Access-Control-Allow-Methods': self.conf.allowed_origin,
609
                'Access-Control-Allow-Headers': self.conf.allowed_origin,
610
            })
611
        return web.Response(
1✔
612
            text=encoded_result,
613
            headers=headers,
614
            content_type='application/json'
615
        )
616

617
    @staticmethod
1✔
618
    async def handle_metrics_get_request(request: web.Request):
1✔
619
        try:
×
620
            return web.Response(
×
621
                text=prom_generate_latest().decode(),
622
                content_type='text/plain; version=0.0.4'
623
            )
624
        except Exception:
×
625
            log.exception('could not generate prometheus data')
×
626
            raise
×
627

628
    async def handle_stream_get_request(self, request: web.Request):
1✔
629
        if not self.conf.streaming_get:
×
630
            log.warning("streaming_get is disabled, rejecting request")
×
631
            raise web.HTTPForbidden()
×
632
        name_and_claim_id = request.path.split("/get/")[1]
×
633
        if "/" not in name_and_claim_id:
×
634
            uri = f"lbry://{name_and_claim_id}"
×
635
        else:
636
            name, claim_id = name_and_claim_id.split("/")
×
637
            uri = f"lbry://{name}#{claim_id}"
×
638
        if not self.file_manager.started.is_set():
×
639
            await self.file_manager.started.wait()
×
640
        stream = await self.jsonrpc_get(uri)
×
641
        if isinstance(stream, dict):
×
642
            raise web.HTTPServerError(text=stream['error'])
×
643
        raise web.HTTPFound(f"/stream/{stream.sd_hash}")
×
644

645
    async def handle_stream_range_request(self, request: web.Request):
1✔
646
        try:
×
647
            return await self._handle_stream_range_request(request)
×
648
        except web.HTTPException as err:
×
649
            log.warning("http code during /stream range request: %s", err)
×
650
            raise err
×
651
        except asyncio.CancelledError:
×
652
            # if not excepted here, it would bubble up the error to the console. every time you closed
653
            # a running tab, you'd get this error in the console
654
            log.debug("/stream range request cancelled")
×
655
        except Exception:
×
656
            log.exception("error handling /stream range request")
×
657
            raise
×
658
        finally:
659
            log.debug("finished handling /stream range request")
×
660

661
    async def _handle_stream_range_request(self, request: web.Request):
1✔
662
        sd_hash = request.path.split("/stream/")[1]
×
663
        if not self.file_manager.started.is_set():
×
664
            await self.file_manager.started.wait()
×
665
        if sd_hash not in self.file_manager.streams:
×
666
            return web.HTTPNotFound()
×
667
        return await self.file_manager.stream_partial_content(request, sd_hash)
×
668

669
    async def _process_rpc_call(self, data):
1✔
670
        args = data.get('params', {})
1✔
671

672
        try:
1✔
673
            function_name = data['method']
1✔
674
        except KeyError:
×
675
            return JSONRPCError(
×
676
                JSONRPCError.CODE_METHOD_NOT_FOUND,
677
                "Missing 'method' value in request."
678
            )
679

680
        try:
1✔
681
            method = self._get_jsonrpc_method(function_name)
1✔
682
        except UnknownAPIMethodError:
×
683
            return JSONRPCError(
×
684
                JSONRPCError.CODE_METHOD_NOT_FOUND,
685
                str(CommandDoesNotExistError(function_name))
686
            )
687

688
        if args in ([{}], []):
1!
689
            _args, _kwargs = (), {}
1✔
690
        elif isinstance(args, dict):
×
691
            _args, _kwargs = (), args
×
692
        elif isinstance(args, list) and len(args) == 1 and isinstance(args[0], dict):
×
693
            # TODO: this is for backwards compatibility. Remove this once API and UI are updated
694
            # TODO: also delete EMPTY_PARAMS then
695
            _args, _kwargs = (), args[0]
×
696
        elif isinstance(args, list) and len(args) == 2 and \
×
697
                isinstance(args[0], list) and isinstance(args[1], dict):
698
            _args, _kwargs = args
×
699
        else:
700
            return JSONRPCError(
×
701
                JSONRPCError.CODE_INVALID_PARAMS,
702
                f"Invalid parameters format: {args}"
703
            )
704

705
        if is_transactional_function(function_name):
1!
706
            log.info("%s %s %s", function_name, _args, _kwargs)
×
707

708
        params_error, erroneous_params = self._check_params(method, _args, _kwargs)
1✔
709
        if params_error is not None:
1!
710
            params_error_message = '{} for {} command: {}'.format(
×
711
                params_error, function_name, ', '.join(erroneous_params)
712
            )
713
            log.warning(params_error_message)
×
714
            return JSONRPCError(
×
715
                JSONRPCError.CODE_INVALID_PARAMS,
716
                params_error_message,
717
            )
718
        self.pending_requests_metric.labels(method=function_name).inc()
1✔
719
        self.requests_count_metric.labels(method=function_name).inc()
1✔
720
        start = time.perf_counter()
1✔
721
        try:
1✔
722
            result = method(self, *_args, **_kwargs)
1✔
723
            if asyncio.iscoroutine(result):
1!
724
                result = await result
1✔
725
            return result
1✔
726
        except asyncio.CancelledError:
×
727
            self.cancelled_request_metric.labels(method=function_name).inc()
×
728
            log.info("cancelled API call for: %s", function_name)
×
729
            raise
×
730
        except Exception as e:  # pylint: disable=broad-except
×
731
            self.failed_request_metric.labels(method=function_name).inc()
×
732
            if not isinstance(e, BaseError):
×
733
                log.exception("error handling api request")
×
734
            else:
735
                log.error("error handling api request: %s", e)
×
736
            return JSONRPCError.create_command_exception(
×
737
                command=function_name, args=_args, kwargs=_kwargs, exception=e, traceback=format_exc()
738
            )
739
        finally:
740
            self.pending_requests_metric.labels(method=function_name).dec()
1✔
741
            self.response_time_metric.labels(method=function_name).observe(time.perf_counter() - start)
1!
742

743
    def _verify_method_is_callable(self, function_path):
1✔
744
        if function_path not in self.callable_methods:
1!
745
            raise UnknownAPIMethodError(function_path)
×
746

747
    def _get_jsonrpc_method(self, function_path):
1✔
748
        if function_path in self.deprecated_methods:
1!
749
            new_command = self.deprecated_methods[function_path].new_command
×
750
            log.warning('API function \"%s\" is deprecated, please update to use \"%s\"',
×
751
                        function_path, new_command)
752
            function_path = new_command
×
753
        self._verify_method_is_callable(function_path)
1✔
754
        return self.callable_methods.get(function_path)
1✔
755

756
    @staticmethod
1✔
757
    def _check_params(function, args_tup, args_dict):
1✔
758
        argspec = inspect.getfullargspec(undecorated(function))
1✔
759
        num_optional_params = 0 if argspec.defaults is None else len(argspec.defaults)
1✔
760

761
        duplicate_params = [
1✔
762
            duplicate_param
763
            for duplicate_param in argspec.args[1:len(args_tup) + 1]
764
            if duplicate_param in args_dict
765
        ]
766

767
        if duplicate_params:
1!
768
            return 'Duplicate parameters', duplicate_params
×
769

770
        missing_required_params = [
1✔
771
            required_param
772
            for required_param in argspec.args[len(args_tup) + 1:-num_optional_params]
773
            if required_param not in args_dict
774
        ]
775
        if len(missing_required_params) > 0:
1!
776
            return 'Missing required parameters', missing_required_params
×
777

778
        extraneous_params = [] if argspec.varkw is not None else [
1✔
779
            extra_param
780
            for extra_param in args_dict
781
            if extra_param not in argspec.args[1:]
782
        ]
783
        if len(extraneous_params) > 0:
1!
784
            return 'Extraneous parameters', extraneous_params
×
785

786
        return None, None
1✔
787

788
    @property
1✔
789
    def ledger(self) -> Optional['Ledger']:
1✔
790
        try:
×
791
            return self.wallet_manager.default_account.ledger
×
792
        except AttributeError:
×
793
            return None
×
794

795
    async def get_est_cost_from_uri(self, uri: str) -> typing.Optional[float]:
1✔
796
        """
797
        Resolve a name and return the estimated stream cost
798
        """
799

800
        resolved = await self.resolve([], uri)
×
801
        if resolved:
×
802
            claim_response = resolved[uri]
×
803
        else:
804
            claim_response = None
×
805

806
        if claim_response and 'claim' in claim_response:
×
807
            if 'value' in claim_response['claim'] and claim_response['claim']['value'] is not None:
×
808
                claim_value = Claim.from_bytes(claim_response['claim']['value'])
×
809
                if not claim_value.stream.has_fee:
×
810
                    return 0.0
×
811
                return round(
×
812
                    self.exchange_rate_manager.convert_currency(
813
                        claim_value.stream.fee.currency, "LBC", claim_value.stream.fee.amount
814
                    ), 5
815
                )
816
            else:
817
                log.warning("Failed to estimate cost for %s", uri)
×
818

819
    ############################################################################
820
    #                                                                          #
821
    #                JSON-RPC API methods start here                           #
822
    #                                                                          #
823
    ############################################################################
824

825
    def jsonrpc_stop(self):  # pylint: disable=no-self-use
1✔
826
        """
827
        Stop lbrynet API server.
828

829
        Usage:
830
            stop
831

832
        Options:
833
            None
834

835
        Returns:
836
            (string) Shutdown message
837
        """
838

839
        def shutdown():
×
840
            raise web.GracefulExit()
×
841

842
        log.info("Shutting down lbrynet daemon")
×
843
        asyncio.get_event_loop().call_later(0, shutdown)
×
844
        return "Shutting down"
×
845

846
    async def jsonrpc_ffmpeg_find(self):
1✔
847
        """
848
        Get ffmpeg installation information
849

850
        Usage:
851
            ffmpeg_find
852

853
        Options:
854
            None
855

856
        Returns:
857
            (dict) Dictionary of ffmpeg information
858
            {
859
                'available': (bool) found ffmpeg,
860
                'which': (str) path to ffmpeg,
861
                'analyze_audio_volume': (bool) should ffmpeg analyze audio
862
            }
863
        """
864
        return await self._video_file_analyzer.status(reset=True, recheck=True)
×
865

866
    async def jsonrpc_status(self):
1✔
867
        """
868
        Get daemon status
869

870
        Usage:
871
            status
872

873
        Options:
874
            None
875

876
        Returns:
877
            (dict) lbrynet-daemon status
878
            {
879
                'installation_id': (str) installation id - base58,
880
                'is_running': (bool),
881
                'skipped_components': (list) [names of skipped components (str)],
882
                'startup_status': { Does not include components which have been skipped
883
                    'blob_manager': (bool),
884
                    'blockchain_headers': (bool),
885
                    'database': (bool),
886
                    'dht': (bool),
887
                    'exchange_rate_manager': (bool),
888
                    'hash_announcer': (bool),
889
                    'peer_protocol_server': (bool),
890
                    'file_manager': (bool),
891
                    'libtorrent_component': (bool),
892
                    'upnp': (bool),
893
                    'wallet': (bool),
894
                },
895
                'connection_status': {
896
                    'code': (str) connection status code,
897
                    'message': (str) connection status message
898
                },
899
                'blockchain_headers': {
900
                    'downloading_headers': (bool),
901
                    'download_progress': (float) 0-100.0
902
                },
903
                'wallet': {
904
                    'connected': (str) host and port of the connected spv server,
905
                    'blocks': (int) local blockchain height,
906
                    'blocks_behind': (int) remote_height - local_height,
907
                    'best_blockhash': (str) block hash of most recent block,
908
                    'is_encrypted': (bool),
909
                    'is_locked': (bool),
910
                    'connected_servers': (list) [
911
                        {
912
                            'host': (str) server hostname,
913
                            'port': (int) server port,
914
                            'latency': (int) milliseconds
915
                        }
916
                    ],
917
                },
918
                'libtorrent_component': {
919
                    'running': (bool) libtorrent was detected and started successfully,
920
                },
921
                'dht': {
922
                    'node_id': (str) lbry dht node id - hex encoded,
923
                    'peers_in_routing_table': (int) the number of peers in the routing table,
924
                },
925
                'blob_manager': {
926
                    'finished_blobs': (int) number of finished blobs in the blob manager,
927
                    'connections': {
928
                        'incoming_bps': {
929
                            <source ip and tcp port>: (int) bytes per second received,
930
                        },
931
                        'outgoing_bps': {
932
                            <destination ip and tcp port>: (int) bytes per second sent,
933
                        },
934
                        'total_outgoing_mps': (float) megabytes per second sent,
935
                        'total_incoming_mps': (float) megabytes per second received,
936
                        'max_outgoing_mbs': (float) maximum bandwidth (megabytes per second) sent, since the
937
                                            daemon was started
938
                        'max_incoming_mbs': (float) maximum bandwidth (megabytes per second) received, since the
939
                                            daemon was started
940
                        'total_sent' : (int) total number of bytes sent since the daemon was started
941
                        'total_received' : (int) total number of bytes received since the daemon was started
942
                    }
943
                },
944
                'hash_announcer': {
945
                    'announce_queue_size': (int) number of blobs currently queued to be announced
946
                },
947
                'file_manager': {
948
                    'managed_files': (int) count of files in the stream manager,
949
                },
950
                'upnp': {
951
                    'aioupnp_version': (str),
952
                    'redirects': {
953
                        <TCP | UDP>: (int) external_port,
954
                    },
955
                    'gateway': (str) manufacturer and model,
956
                    'dht_redirect_set': (bool),
957
                    'peer_redirect_set': (bool),
958
                    'external_ip': (str) external ip address,
959
                }
960
            }
961
        """
962
        ffmpeg_status = await self._video_file_analyzer.status()
1✔
963
        running_components = self.component_manager.get_components_status()
1✔
964
        response = {
1✔
965
            'installation_id': self.installation_id,
966
            'is_running': all(running_components.values()),
967
            'skipped_components': self.component_manager.skip_components,
968
            'startup_status': running_components,
969
            'ffmpeg_status': ffmpeg_status
970
        }
971
        for component in self.component_manager.components:
1!
972
            status = await component.get_status()
×
973
            if status:
×
974
                response[component.component_name] = status
×
975
        return response
1✔
976

977
    def jsonrpc_version(self):  # pylint: disable=no-self-use
1✔
978
        """
979
        Get lbrynet API server version information
980

981
        Usage:
982
            version
983

984
        Options:
985
            None
986

987
        Returns:
988
            (dict) Dictionary of lbry version information
989
            {
990
                'processor': (str) processor type,
991
                'python_version': (str) python version,
992
                'platform': (str) platform string,
993
                'os_release': (str) os release string,
994
                'os_system': (str) os name,
995
                'version': (str) lbrynet version,
996
                'build': (str) "dev" | "qa" | "rc" | "release",
997
            }
998
        """
999
        return self.platform_info
×
1000

1001
    @requires(WALLET_COMPONENT)
1✔
1002
    async def jsonrpc_resolve(self, urls: typing.Union[str, list], wallet_id=None, **kwargs):
1✔
1003
        """
1004
        Get the claim that a URL refers to.
1005

1006
        Usage:
1007
            resolve <urls>... [--wallet_id=<wallet_id>]
1008
                    [--include_purchase_receipt]
1009
                    [--include_is_my_output]
1010
                    [--include_sent_supports]
1011
                    [--include_sent_tips]
1012
                    [--include_received_tips]
1013
                    [--new_sdk_server=<new_sdk_server>]
1014

1015
        Options:
1016
            --urls=<urls>              : (str, list) one or more urls to resolve
1017
            --wallet_id=<wallet_id>    : (str) wallet to check for claim purchase receipts
1018
           --new_sdk_server=<new_sdk_server> : (str) URL of the new SDK server (EXPERIMENTAL)
1019
           --include_purchase_receipt  : (bool) lookup and include a receipt if this wallet
1020
                                                has purchased the claim being resolved
1021
            --include_is_my_output     : (bool) lookup and include a boolean indicating
1022
                                                if claim being resolved is yours
1023
            --include_sent_supports    : (bool) lookup and sum the total amount
1024
                                                of supports you've made to this claim
1025
            --include_sent_tips        : (bool) lookup and sum the total amount
1026
                                                of tips you've made to this claim
1027
                                                (only makes sense when claim is not yours)
1028
            --include_received_tips    : (bool) lookup and sum the total amount
1029
                                                of tips you've received to this claim
1030
                                                (only makes sense when claim is yours)
1031

1032
        Returns:
1033
            Dictionary of results, keyed by url
1034
            '<url>': {
1035
                    If a resolution error occurs:
1036
                    'error': Error message
1037

1038
                    If the url resolves to a channel or a claim in a channel:
1039
                    'certificate': {
1040
                        'address': (str) claim address,
1041
                        'amount': (float) claim amount,
1042
                        'effective_amount': (float) claim amount including supports,
1043
                        'claim_id': (str) claim id,
1044
                        'claim_sequence': (int) claim sequence number (or -1 if unknown),
1045
                        'decoded_claim': (bool) whether or not the claim value was decoded,
1046
                        'height': (int) claim height,
1047
                        'confirmations': (int) claim depth,
1048
                        'timestamp': (int) timestamp of the block that included this claim tx,
1049
                        'has_signature': (bool) included if decoded_claim
1050
                        'name': (str) claim name,
1051
                        'permanent_url': (str) permanent url of the certificate claim,
1052
                        'supports: (list) list of supports [{'txid': (str) txid,
1053
                                                             'nout': (int) nout,
1054
                                                             'amount': (float) amount}],
1055
                        'txid': (str) claim txid,
1056
                        'nout': (str) claim nout,
1057
                        'signature_is_valid': (bool), included if has_signature,
1058
                        'value': ClaimDict if decoded, otherwise hex string
1059
                    }
1060

1061
                    If the url resolves to a channel:
1062
                    'claims_in_channel': (int) number of claims in the channel,
1063

1064
                    If the url resolves to a claim:
1065
                    'claim': {
1066
                        'address': (str) claim address,
1067
                        'amount': (float) claim amount,
1068
                        'effective_amount': (float) claim amount including supports,
1069
                        'claim_id': (str) claim id,
1070
                        'claim_sequence': (int) claim sequence number (or -1 if unknown),
1071
                        'decoded_claim': (bool) whether or not the claim value was decoded,
1072
                        'height': (int) claim height,
1073
                        'depth': (int) claim depth,
1074
                        'has_signature': (bool) included if decoded_claim
1075
                        'name': (str) claim name,
1076
                        'permanent_url': (str) permanent url of the claim,
1077
                        'channel_name': (str) channel name if claim is in a channel
1078
                        'supports: (list) list of supports [{'txid': (str) txid,
1079
                                                             'nout': (int) nout,
1080
                                                             'amount': (float) amount}]
1081
                        'txid': (str) claim txid,
1082
                        'nout': (str) claim nout,
1083
                        'signature_is_valid': (bool), included if has_signature,
1084
                        'value': ClaimDict if decoded, otherwise hex string
1085
                    }
1086
            }
1087
        """
1088
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1089

1090
        if isinstance(urls, str):
×
1091
            urls = [urls]
×
1092

1093
        results = {}
×
1094

1095
        valid_urls = set()
×
1096
        for url in urls:
×
1097
            try:
×
1098
                URL.parse(url)
×
1099
                valid_urls.add(url)
×
1100
            except ValueError:
×
1101
                results[url] = {"error": f"{url} is not a valid url"}
×
1102

1103
        resolved = await self.resolve(wallet.accounts, list(valid_urls), **kwargs)
×
1104

1105
        for resolved_uri in resolved:
×
1106
            results[resolved_uri] = resolved[resolved_uri] if resolved[resolved_uri] is not None else \
×
1107
                {"error": f"{resolved_uri} did not resolve to a claim"}
1108

1109
        return results
×
1110

1111
    @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT,
1✔
1112
              FILE_MANAGER_COMPONENT)
1113
    async def jsonrpc_get(
1✔
1114
            self, uri, file_name=None, download_directory=None, timeout=None, save_file=None, wallet_id=None):
1115
        """
1116
        Download stream from a LBRY name.
1117

1118
        Usage:
1119
            get <uri> [<file_name> | --file_name=<file_name>]
1120
             [<download_directory> | --download_directory=<download_directory>] [<timeout> | --timeout=<timeout>]
1121
             [--save_file=<save_file>] [--wallet_id=<wallet_id>]
1122

1123

1124
        Options:
1125
            --uri=<uri>              : (str) uri of the content to download
1126
            --file_name=<file_name>  : (str) specified name for the downloaded file, overrides the stream file name
1127
            --download_directory=<download_directory>  : (str) full path to the directory to download into
1128
            --timeout=<timeout>      : (int) download timeout in number of seconds
1129
            --save_file=<save_file>  : (bool) save the file to the downloads directory
1130
            --wallet_id=<wallet_id>  : (str) wallet to check for claim purchase receipts
1131

1132
        Returns: {File}
1133
        """
1134
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1135
        if download_directory and not os.path.isdir(download_directory):
×
1136
            return {"error": f"specified download directory \"{download_directory}\" does not exist"}
×
1137
        try:
×
1138
            stream = await self.file_manager.download_from_uri(
×
1139
                uri, self.exchange_rate_manager, timeout, file_name, download_directory,
1140
                save_file=save_file, wallet=wallet
1141
            )
1142
            if not stream:
×
1143
                raise DownloadSDTimeoutError(uri)
×
1144
        except Exception as e:
×
1145
            # TODO: use error from lbry.error
1146
            log.warning("Error downloading %s: %s", uri, str(e))
×
1147
            return {"error": str(e)}
×
1148
        return stream
×
1149

1150
    SETTINGS_DOC = """
1✔
1151
    Settings management.
1152
    """
1153

1154
    def jsonrpc_settings_get(self):
1✔
1155
        """
1156
        Get daemon settings
1157

1158
        Usage:
1159
            settings_get
1160

1161
        Options:
1162
            None
1163

1164
        Returns:
1165
            (dict) Dictionary of daemon settings
1166
            See ADJUSTABLE_SETTINGS in lbry/conf.py for full list of settings
1167
        """
1168
        return self.conf.settings_dict
×
1169

1170
    def jsonrpc_settings_set(self, key, value):
1✔
1171
        """
1172
        Set daemon settings
1173

1174
        Usage:
1175
            settings_set (<key>) (<value>)
1176

1177
        Options:
1178
            None
1179

1180
        Returns:
1181
            (dict) Updated dictionary of daemon settings
1182
        """
1183
        with self.conf.update_config() as c:
×
1184
            if value and isinstance(value, str) and value[0] in ('[', '{'):
×
1185
                value = json.loads(value)
×
1186
            attr: Setting = getattr(type(c), key)
×
1187
            cleaned = attr.deserialize(value)
×
1188
            setattr(c, key, cleaned)
×
1189
        return {key: cleaned}
×
1190

1191
    def jsonrpc_settings_clear(self, key):
1✔
1192
        """
1193
        Clear daemon settings
1194

1195
        Usage:
1196
            settings_clear (<key>)
1197

1198
        Options:
1199
            None
1200

1201
        Returns:
1202
            (dict) Updated dictionary of daemon settings
1203
        """
1204
        with self.conf.update_config() as c:
×
1205
            setattr(c, key, NOT_SET)
×
1206
        return {key: self.conf.settings_dict[key]}
×
1207

1208
    PREFERENCE_DOC = """
1✔
1209
    Preferences management.
1210
    """
1211

1212
    def jsonrpc_preference_get(self, key=None, wallet_id=None):
1✔
1213
        """
1214
        Get preference value for key or all values if not key is passed in.
1215

1216
        Usage:
1217
            preference_get [<key>] [--wallet_id=<wallet_id>]
1218

1219
        Options:
1220
            --key=<key> : (str) key associated with value
1221
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
1222

1223
        Returns:
1224
            (dict) Dictionary of preference(s)
1225
        """
1226
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1227
        if key:
×
1228
            if key in wallet.preferences:
×
1229
                return {key: wallet.preferences[key]}
×
1230
            return
×
1231
        return wallet.preferences.to_dict_without_ts()
×
1232

1233
    def jsonrpc_preference_set(self, key, value, wallet_id=None):
1✔
1234
        """
1235
        Set preferences
1236

1237
        Usage:
1238
            preference_set (<key>) (<value>) [--wallet_id=<wallet_id>]
1239

1240
        Options:
1241
            --key=<key> : (str) key associated with value
1242
            --value=<key> : (str) key associated with value
1243
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
1244

1245
        Returns:
1246
            (dict) Dictionary with key/value of new preference
1247
        """
1248
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1249
        if value and isinstance(value, str) and value[0] in ('[', '{'):
×
1250
            value = json.loads(value)
×
1251
        wallet.preferences[key] = value
×
1252
        wallet.save()
×
1253
        return {key: value}
×
1254

1255
    WALLET_DOC = """
1✔
1256
    Create, modify and inspect wallets.
1257
    """
1258

1259
    @requires("wallet")
1✔
1260
    def jsonrpc_wallet_list(self, wallet_id=None, page=None, page_size=None):
1✔
1261
        """
1262
        List wallets.
1263

1264
        Usage:
1265
            wallet_list [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]
1266

1267
        Options:
1268
            --wallet_id=<wallet_id>  : (str) show specific wallet only
1269
            --page=<page>            : (int) page to return during paginating
1270
            --page_size=<page_size>  : (int) number of items on page during pagination
1271

1272
        Returns: {Paginated[Wallet]}
1273
        """
1274
        if wallet_id:
×
1275
            return paginate_list([self.wallet_manager.get_wallet_or_error(wallet_id)], 1, 1)
×
1276
        return paginate_list(self.wallet_manager.wallets, page, page_size)
×
1277

1278
    def jsonrpc_wallet_reconnect(self):
1✔
1279
        """
1280
        Reconnects ledger network client, applying new configurations.
1281

1282
        Usage:
1283
            wallet_reconnect
1284

1285
        Options:
1286

1287
        Returns: None
1288
        """
1289
        return self.wallet_manager.reset()
×
1290

1291
    @requires("wallet")
1✔
1292
    async def jsonrpc_wallet_create(
1✔
1293
            self, wallet_id, skip_on_startup=False, create_account=False, single_key=False):
1294
        """
1295
        Create a new wallet.
1296

1297
        Usage:
1298
            wallet_create (<wallet_id> | --wallet_id=<wallet_id>) [--skip_on_startup]
1299
                          [--create_account] [--single_key]
1300

1301
        Options:
1302
            --wallet_id=<wallet_id>  : (str) wallet file name
1303
            --skip_on_startup        : (bool) don't add wallet to daemon_settings.yml
1304
            --create_account         : (bool) generates the default account
1305
            --single_key             : (bool) used with --create_account, creates single-key account
1306

1307
        Returns: {Wallet}
1308
        """
1309
        wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id)
×
1310
        for wallet in self.wallet_manager.wallets:
×
1311
            if wallet.id == wallet_id:
×
1312
                raise WalletAlreadyLoadedError(wallet_path)
×
1313
        if os.path.exists(wallet_path):
×
1314
            raise WalletAlreadyExistsError(wallet_path)
×
1315

1316
        wallet = self.wallet_manager.import_wallet(wallet_path)
×
1317
        if not wallet.accounts and create_account:
×
1318
            account = Account.generate(
×
1319
                self.ledger, wallet, address_generator={
1320
                    'name': SingleKey.name if single_key else HierarchicalDeterministic.name
1321
                }
1322
            )
1323
            if self.ledger.network.is_connected:
×
1324
                await self.ledger.subscribe_account(account)
×
1325
        wallet.save()
×
1326
        if not skip_on_startup:
×
1327
            with self.conf.update_config() as c:
×
1328
                c.wallets += [wallet_id]
×
1329
        return wallet
×
1330

1331
    @requires("wallet")
1✔
1332
    async def jsonrpc_wallet_export(self, password=None, wallet_id=None):
1✔
1333
        """
1334
        Exports encrypted wallet data if password is supplied; otherwise plain JSON.
1335

1336
        Wallet must be unlocked to perform this operation.
1337

1338
        Usage:
1339
            wallet_export [--password=<password>] [--wallet_id=<wallet_id>]
1340

1341
        Options:
1342
            --password=<password>         : (str) password to encrypt outgoing data
1343
            --wallet_id=<wallet_id>       : (str) wallet being exported
1344

1345
        Returns:
1346
            (str) data: base64-encoded encrypted wallet, or cleartext JSON
1347

1348
        """
1349
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1350
        if password is None:
×
1351
            return wallet.to_json()
×
1352
        return wallet.pack(password).decode()
×
1353

1354
    @requires("wallet")
1✔
1355
    async def jsonrpc_wallet_import(self, data, password=None, wallet_id=None, blocking=False):
1✔
1356
        """
1357
        Import wallet data and merge accounts and preferences. Data is expected to be JSON if
1358
        password is not supplied.
1359

1360
        Wallet must be unlocked to perform this operation.
1361

1362
        Usage:
1363
            wallet_import (<data> | --data=<data>) [<password> | --password=<password>]
1364
                          [--wallet_id=<wallet_id>] [--blocking]
1365

1366
        Options:
1367
            --data=<data>                 : (str) incoming wallet data
1368
            --password=<password>         : (str) password to decrypt incoming data
1369
            --wallet_id=<wallet_id>       : (str) wallet being merged into
1370
            --blocking                    : (bool) wait until any new accounts have merged
1371

1372
        Returns:
1373
            (str) base64-encoded encrypted wallet, or cleartext JSON
1374
        """
1375
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1376
        added_accounts, merged_accounts = wallet.merge(self.wallet_manager, password, data)
×
1377
        for new_account in itertools.chain(added_accounts, merged_accounts):
×
1378
            await new_account.maybe_migrate_certificates()
×
1379
        if added_accounts and self.ledger.network.is_connected:
×
1380
            if blocking:
×
1381
                await asyncio.wait([
×
1382
                    a.ledger.subscribe_account(a) for a in added_accounts
1383
                ])
1384
            else:
1385
                for new_account in added_accounts:
×
1386
                    asyncio.create_task(self.ledger.subscribe_account(new_account))
×
1387
        wallet.save()
×
1388
        return await self.jsonrpc_wallet_export(password=password, wallet_id=wallet_id)
×
1389

1390
    @requires("wallet")
1✔
1391
    async def jsonrpc_wallet_add(self, wallet_id):
1✔
1392
        """
1393
        Add existing wallet.
1394

1395
        Usage:
1396
            wallet_add (<wallet_id> | --wallet_id=<wallet_id>)
1397

1398
        Options:
1399
            --wallet_id=<wallet_id>  : (str) wallet file name
1400

1401
        Returns: {Wallet}
1402
        """
1403
        wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id)
×
1404
        for wallet in self.wallet_manager.wallets:
×
1405
            if wallet.id == wallet_id:
×
1406
                raise WalletAlreadyLoadedError(wallet_path)
×
1407
        if not os.path.exists(wallet_path):
×
1408
            raise WalletNotFoundError(wallet_path)
×
1409
        wallet = self.wallet_manager.import_wallet(wallet_path)
×
1410
        if self.ledger.network.is_connected:
×
1411
            for account in wallet.accounts:
×
1412
                await self.ledger.subscribe_account(account)
×
1413
        return wallet
×
1414

1415
    @requires("wallet")
1✔
1416
    async def jsonrpc_wallet_remove(self, wallet_id):
1✔
1417
        """
1418
        Remove an existing wallet.
1419

1420
        Usage:
1421
            wallet_remove (<wallet_id> | --wallet_id=<wallet_id>)
1422

1423
        Options:
1424
            --wallet_id=<wallet_id>    : (str) name of wallet to remove
1425

1426
        Returns: {Wallet}
1427
        """
1428
        wallet = self.wallet_manager.get_wallet_or_error(wallet_id)
×
1429
        self.wallet_manager.wallets.remove(wallet)
×
1430
        for account in wallet.accounts:
×
1431
            await self.ledger.unsubscribe_account(account)
×
1432
        return wallet
×
1433

1434
    @requires("wallet")
1✔
1435
    async def jsonrpc_wallet_balance(self, wallet_id=None, confirmations=0):
1✔
1436
        """
1437
        Return the balance of a wallet
1438

1439
        Usage:
1440
            wallet_balance [--wallet_id=<wallet_id>] [--confirmations=<confirmations>]
1441

1442
        Options:
1443
            --wallet_id=<wallet_id>         : (str) balance for specific wallet
1444
            --confirmations=<confirmations> : (int) Only include transactions with this many
1445
                                              confirmed blocks.
1446

1447
        Returns:
1448
            (decimal) amount of lbry credits in wallet
1449
        """
1450
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1451
        balance = await self.ledger.get_detailed_balance(
×
1452
            accounts=wallet.accounts, confirmations=confirmations
1453
        )
1454
        return dict_values_to_lbc(balance)
×
1455

1456
    def jsonrpc_wallet_status(self, wallet_id=None):
1✔
1457
        """
1458
        Status of wallet including encryption/lock state.
1459

1460
        Usage:
1461
            wallet_status [<wallet_id> | --wallet_id=<wallet_id>]
1462

1463
        Options:
1464
            --wallet_id=<wallet_id>    : (str) status of specific wallet
1465

1466
        Returns:
1467
            Dictionary of wallet status information.
1468
        """
1469
        if self.wallet_manager is None:
×
1470
            return {'is_encrypted': None, 'is_syncing': None, 'is_locked': None}
×
1471
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1472
        return {
×
1473
            'is_encrypted': wallet.is_encrypted,
1474
            'is_syncing': len(self.ledger._update_tasks) > 0,
1475
            'is_locked': wallet.is_locked
1476
        }
1477

1478
    @requires(WALLET_COMPONENT)
1✔
1479
    def jsonrpc_wallet_unlock(self, password, wallet_id=None):
1✔
1480
        """
1481
        Unlock an encrypted wallet
1482

1483
        Usage:
1484
            wallet_unlock (<password> | --password=<password>) [--wallet_id=<wallet_id>]
1485

1486
        Options:
1487
            --password=<password>      : (str) password to use for unlocking
1488
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
1489

1490
        Returns:
1491
            (bool) true if wallet is unlocked, otherwise false
1492
        """
1493
        return self.wallet_manager.get_wallet_or_default(wallet_id).unlock(password)
×
1494

1495
    @requires(WALLET_COMPONENT)
1✔
1496
    def jsonrpc_wallet_lock(self, wallet_id=None):
1✔
1497
        """
1498
        Lock an unlocked wallet
1499

1500
        Usage:
1501
            wallet_lock [--wallet_id=<wallet_id>]
1502

1503
        Options:
1504
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
1505

1506
        Returns:
1507
            (bool) true if wallet is locked, otherwise false
1508
        """
1509
        return self.wallet_manager.get_wallet_or_default(wallet_id).lock()
×
1510

1511
    @requires(WALLET_COMPONENT)
1✔
1512
    def jsonrpc_wallet_decrypt(self, wallet_id=None):
1✔
1513
        """
1514
        Decrypt an encrypted wallet, this will remove the wallet password. The wallet must be unlocked to decrypt it
1515

1516
        Usage:
1517
            wallet_decrypt [--wallet_id=<wallet_id>]
1518

1519
        Options:
1520
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
1521

1522
        Returns:
1523
            (bool) true if wallet is decrypted, otherwise false
1524
        """
1525
        return self.wallet_manager.get_wallet_or_default(wallet_id).decrypt()
×
1526

1527
    @requires(WALLET_COMPONENT)
1✔
1528
    def jsonrpc_wallet_encrypt(self, new_password, wallet_id=None):
1✔
1529
        """
1530
        Encrypt an unencrypted wallet with a password
1531

1532
        Usage:
1533
            wallet_encrypt (<new_password> | --new_password=<new_password>)
1534
                            [--wallet_id=<wallet_id>]
1535

1536
        Options:
1537
            --new_password=<new_password>  : (str) password to encrypt account
1538
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
1539

1540
        Returns:
1541
            (bool) true if wallet is decrypted, otherwise false
1542
        """
1543
        return self.wallet_manager.get_wallet_or_default(wallet_id).encrypt(new_password)
×
1544

1545
    @requires(WALLET_COMPONENT)
1✔
1546
    async def jsonrpc_wallet_send(
1✔
1547
            self, amount, addresses, wallet_id=None,
1548
            change_account_id=None, funding_account_ids=None, preview=False, blocking=True):
1549
        """
1550
        Send the same number of credits to multiple addresses using all accounts in wallet to
1551
        fund the transaction and the default account to receive any change.
1552

1553
        Usage:
1554
            wallet_send <amount> <addresses>... [--wallet_id=<wallet_id>] [--preview]
1555
                        [--change_account_id=None] [--funding_account_ids=<funding_account_ids>...]
1556
                        [--blocking]
1557

1558
        Options:
1559
            --wallet_id=<wallet_id>         : (str) restrict operation to specific wallet
1560
            --change_account_id=<wallet_id> : (str) account where change will go
1561
            --funding_account_ids=<funding_account_ids> : (str) accounts to fund the transaction
1562
            --preview                       : (bool) do not broadcast the transaction
1563
            --blocking                      : (bool) wait until tx has synced
1564

1565
        Returns: {Transaction}
1566
        """
1567
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1568
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
1569
        account = wallet.get_account_or_default(change_account_id)
×
1570
        accounts = wallet.get_accounts_or_all(funding_account_ids)
×
1571

1572
        amount = self.get_dewies_or_error("amount", amount)
×
1573

1574
        if addresses and not isinstance(addresses, list):
×
1575
            addresses = [addresses]
×
1576

1577
        outputs = []
×
1578
        for address in addresses:
×
1579
            self.valid_address_or_error(address, allow_script_address=True)
×
1580
            if self.ledger.is_pubkey_address(address):
×
1581
                outputs.append(
×
1582
                    Output.pay_pubkey_hash(
1583
                        amount, self.ledger.address_to_hash160(address)
1584
                    )
1585
                )
1586
            elif self.ledger.is_script_address(address):
×
1587
                outputs.append(
×
1588
                    Output.pay_script_hash(
1589
                        amount, self.ledger.address_to_hash160(address)
1590
                    )
1591
                )
1592
            else:
1593
                raise ValueError(f"Unsupported address: '{address}'")  # TODO: use error from lbry.error
×
1594

1595
        tx = await Transaction.create(
×
1596
            [], outputs, accounts, account
1597
        )
1598
        if not preview:
×
1599
            await self.broadcast_or_release(tx, blocking)
×
1600
            self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())
×
1601
        else:
1602
            await self.ledger.release_tx(tx)
×
1603
        return tx
×
1604

1605
    ACCOUNT_DOC = """
1✔
1606
    Create, modify and inspect wallet accounts.
1607
    """
1608

1609
    @requires("wallet")
1✔
1610
    async def jsonrpc_account_list(
1✔
1611
            self, account_id=None, wallet_id=None, confirmations=0,
1612
            include_claims=False, show_seed=False, page=None, page_size=None):
1613
        """
1614
        List details of all of the accounts or a specific account.
1615

1616
        Usage:
1617
            account_list [<account_id>] [--wallet_id=<wallet_id>]
1618
                         [--confirmations=<confirmations>]
1619
                         [--include_claims] [--show_seed]
1620
                         [--page=<page>] [--page_size=<page_size>]
1621

1622
        Options:
1623
            --account_id=<account_id>       : (str) If provided only the balance for this
1624
                                                    account will be given
1625
            --wallet_id=<wallet_id>         : (str) accounts in specific wallet
1626
            --confirmations=<confirmations> : (int) required confirmations (default: 0)
1627
            --include_claims                : (bool) include claims, requires than a
1628
                                                     LBC account is specified (default: false)
1629
            --show_seed                     : (bool) show the seed for the account
1630
            --page=<page>                   : (int) page to return during paginating
1631
            --page_size=<page_size>         : (int) number of items on page during pagination
1632

1633
        Returns: {Paginated[Account]}
1634
        """
1635
        kwargs = {
×
1636
            'confirmations': confirmations,
1637
            'show_seed': show_seed
1638
        }
1639
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1640
        if account_id:
×
1641
            return paginate_list([await wallet.get_account_or_error(account_id).get_details(**kwargs)], 1, 1)
×
1642
        else:
1643
            return paginate_list(await wallet.get_detailed_accounts(**kwargs), page, page_size)
×
1644

1645
    @requires("wallet")
1✔
1646
    async def jsonrpc_account_balance(self, account_id=None, wallet_id=None, confirmations=0):
1✔
1647
        """
1648
        Return the balance of an account
1649

1650
        Usage:
1651
            account_balance [<account_id>] [<address> | --address=<address>] [--wallet_id=<wallet_id>]
1652
                            [<confirmations> | --confirmations=<confirmations>]
1653

1654
        Options:
1655
            --account_id=<account_id>       : (str) If provided only the balance for this
1656
                                              account will be given. Otherwise default account.
1657
            --wallet_id=<wallet_id>         : (str) balance for specific wallet
1658
            --confirmations=<confirmations> : (int) Only include transactions with this many
1659
                                              confirmed blocks.
1660

1661
        Returns:
1662
            (decimal) amount of lbry credits in wallet
1663
        """
1664
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1665
        account = wallet.get_account_or_default(account_id)
×
1666
        balance = await account.get_detailed_balance(
×
1667
            confirmations=confirmations, read_only=True
1668
        )
1669
        return dict_values_to_lbc(balance)
×
1670

1671
    @requires("wallet")
1✔
1672
    async def jsonrpc_account_add(
1✔
1673
            self, account_name, wallet_id=None, single_key=False,
1674
            seed=None, private_key=None, public_key=None):
1675
        """
1676
        Add a previously created account from a seed, private key or public key (read-only).
1677
        Specify --single_key for single address or vanity address accounts.
1678

1679
        Usage:
1680
            account_add (<account_name> | --account_name=<account_name>)
1681
                 (--seed=<seed> | --private_key=<private_key> | --public_key=<public_key>)
1682
                 [--single_key] [--wallet_id=<wallet_id>]
1683

1684
        Options:
1685
            --account_name=<account_name>  : (str) name of the account to add
1686
            --seed=<seed>                  : (str) seed to generate new account from
1687
            --private_key=<private_key>    : (str) private key for new account
1688
            --public_key=<public_key>      : (str) public key for new account
1689
            --single_key                   : (bool) create single key account, default is multi-key
1690
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
1691

1692
        Returns: {Account}
1693
        """
1694
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1695
        account = Account.from_dict(
×
1696
            self.ledger, wallet, {
1697
                'name': account_name,
1698
                'seed': seed,
1699
                'private_key': private_key,
1700
                'public_key': public_key,
1701
                'address_generator': {
1702
                    'name': SingleKey.name if single_key else HierarchicalDeterministic.name
1703
                }
1704
            }
1705
        )
1706
        wallet.save()
×
1707
        if self.ledger.network.is_connected:
×
1708
            await self.ledger.subscribe_account(account)
×
1709
        return account
×
1710

1711
    @requires("wallet")
1✔
1712
    async def jsonrpc_account_create(self, account_name, single_key=False, wallet_id=None):
1✔
1713
        """
1714
        Create a new account. Specify --single_key if you want to use
1715
        the same address for all transactions (not recommended).
1716

1717
        Usage:
1718
            account_create (<account_name> | --account_name=<account_name>)
1719
                           [--single_key] [--wallet_id=<wallet_id>]
1720

1721
        Options:
1722
            --account_name=<account_name>  : (str) name of the account to create
1723
            --single_key                   : (bool) create single key account, default is multi-key
1724
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
1725

1726
        Returns: {Account}
1727
        """
1728
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1729
        account = Account.generate(
×
1730
            self.ledger, wallet, account_name, {
1731
                'name': SingleKey.name if single_key else HierarchicalDeterministic.name
1732
            }
1733
        )
1734
        wallet.save()
×
1735
        if self.ledger.network.is_connected:
×
1736
            await self.ledger.subscribe_account(account)
×
1737
        return account
×
1738

1739
    @requires("wallet")
1✔
1740
    def jsonrpc_account_remove(self, account_id, wallet_id=None):
1✔
1741
        """
1742
        Remove an existing account.
1743

1744
        Usage:
1745
            account_remove (<account_id> | --account_id=<account_id>) [--wallet_id=<wallet_id>]
1746

1747
        Options:
1748
            --account_id=<account_id>  : (str) id of the account to remove
1749
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
1750

1751
        Returns: {Account}
1752
        """
1753
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1754
        account = wallet.get_account_or_error(account_id)
×
1755
        wallet.accounts.remove(account)
×
1756
        wallet.save()
×
1757
        return account
×
1758

1759
    @requires("wallet")
1✔
1760
    def jsonrpc_account_set(
1✔
1761
            self, account_id, wallet_id=None, default=False, new_name=None,
1762
            change_gap=None, change_max_uses=None, receiving_gap=None, receiving_max_uses=None):
1763
        """
1764
        Change various settings on an account.
1765

1766
        Usage:
1767
            account_set (<account_id> | --account_id=<account_id>) [--wallet_id=<wallet_id>]
1768
                [--default] [--new_name=<new_name>]
1769
                [--change_gap=<change_gap>] [--change_max_uses=<change_max_uses>]
1770
                [--receiving_gap=<receiving_gap>] [--receiving_max_uses=<receiving_max_uses>]
1771

1772
        Options:
1773
            --account_id=<account_id>       : (str) id of the account to change
1774
            --wallet_id=<wallet_id>         : (str) restrict operation to specific wallet
1775
            --default                       : (bool) make this account the default
1776
            --new_name=<new_name>           : (str) new name for the account
1777
            --receiving_gap=<receiving_gap> : (int) set the gap for receiving addresses
1778
            --receiving_max_uses=<receiving_max_uses> : (int) set the maximum number of times to
1779
                                                              use a receiving address
1780
            --change_gap=<change_gap>           : (int) set the gap for change addresses
1781
            --change_max_uses=<change_max_uses> : (int) set the maximum number of times to
1782
                                                        use a change address
1783

1784
        Returns: {Account}
1785
        """
1786
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1787
        account = wallet.get_account_or_error(account_id)
×
1788
        change_made = False
×
1789

1790
        if account.receiving.name == HierarchicalDeterministic.name:
×
1791
            address_changes = {
×
1792
                'change': {'gap': change_gap, 'maximum_uses_per_address': change_max_uses},
1793
                'receiving': {'gap': receiving_gap, 'maximum_uses_per_address': receiving_max_uses},
1794
            }
1795
            for chain_name, changes in address_changes.items():
×
1796
                chain = getattr(account, chain_name)
×
1797
                for attr, value in changes.items():
×
1798
                    if value is not None:
×
1799
                        setattr(chain, attr, value)
×
1800
                        change_made = True
×
1801

1802
        if new_name is not None:
×
1803
            account.name = new_name
×
1804
            change_made = True
×
1805

1806
        if default and wallet.default_account != account:
×
1807
            wallet.accounts.remove(account)
×
1808
            wallet.accounts.insert(0, account)
×
1809
            change_made = True
×
1810

1811
        if change_made:
×
1812
            account.modified_on = int(time.time())
×
1813
            wallet.save()
×
1814

1815
        return account
×
1816

1817
    @requires("wallet")
1✔
1818
    def jsonrpc_account_max_address_gap(self, account_id, wallet_id=None):
1✔
1819
        """
1820
        Finds ranges of consecutive addresses that are unused and returns the length
1821
        of the longest such range: for change and receiving address chains. This is
1822
        useful to figure out ideal values to set for 'receiving_gap' and 'change_gap'
1823
        account settings.
1824

1825
        Usage:
1826
            account_max_address_gap (<account_id> | --account_id=<account_id>)
1827
                                    [--wallet_id=<wallet_id>]
1828

1829
        Options:
1830
            --account_id=<account_id>  : (str) account for which to get max gaps
1831
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
1832

1833
        Returns:
1834
            (map) maximum gap for change and receiving addresses
1835
        """
1836
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1837
        return wallet.get_account_or_error(account_id).get_max_gap()
×
1838

1839
    @requires("wallet")
1✔
1840
    def jsonrpc_account_fund(self, to_account=None, from_account=None, amount='0.0',
1✔
1841
                             everything=False, outputs=1, broadcast=False, wallet_id=None):
1842
        """
1843
        Transfer some amount (or --everything) to an account from another
1844
        account (can be the same account). Amounts are interpreted as LBC.
1845
        You can also spread the transfer across a number of --outputs (cannot
1846
        be used together with --everything).
1847

1848
        Usage:
1849
            account_fund [<to_account> | --to_account=<to_account>]
1850
                [<from_account> | --from_account=<from_account>]
1851
                (<amount> | --amount=<amount> | --everything)
1852
                [<outputs> | --outputs=<outputs>] [--wallet_id=<wallet_id>]
1853
                [--broadcast]
1854

1855
        Options:
1856
            --to_account=<to_account>     : (str) send to this account
1857
            --from_account=<from_account> : (str) spend from this account
1858
            --amount=<amount>             : (decimal) the amount to transfer lbc
1859
            --everything                  : (bool) transfer everything (excluding claims), default: false.
1860
            --outputs=<outputs>           : (int) split payment across many outputs, default: 1.
1861
            --wallet_id=<wallet_id>       : (str) limit operation to specific wallet.
1862
            --broadcast                   : (bool) actually broadcast the transaction, default: false.
1863

1864
        Returns: {Transaction}
1865
        """
1866
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1867
        to_account = wallet.get_account_or_default(to_account)
×
1868
        from_account = wallet.get_account_or_default(from_account)
×
1869
        amount = self.get_dewies_or_error('amount', amount) if amount else None
×
1870
        if not isinstance(outputs, int):
×
1871
            # TODO: use error from lbry.error
1872
            raise ValueError("--outputs must be an integer.")
×
1873
        if everything and outputs > 1:
×
1874
            # TODO: use error from lbry.error
1875
            raise ValueError("Using --everything along with --outputs is not supported.")
×
1876
        return from_account.fund(
×
1877
            to_account=to_account, amount=amount, everything=everything,
1878
            outputs=outputs, broadcast=broadcast
1879
        )
1880

1881
    @requires("wallet")
1✔
1882
    async def jsonrpc_account_deposit(
1✔
1883
        self, txid, nout, redeem_script, private_key,
1884
        to_account=None, wallet_id=None, preview=False, blocking=False
1885
    ):
1886
        """
1887
        Spend a time locked transaction into your account.
1888

1889
        Usage:
1890
            account_deposit <txid> <nout> <redeem_script> <private_key>
1891
                [<to_account> | --to_account=<to_account>]
1892
                [--wallet_id=<wallet_id>] [--preview] [--blocking]
1893

1894
        Options:
1895
            --txid=<txid>                   : (str) id of the transaction
1896
            --nout=<nout>                   : (int) output number in the transaction
1897
            --redeem_script=<redeem_script> : (str) redeem script for output
1898
            --private_key=<private_key>     : (str) private key to sign transaction
1899
            --to_account=<to_account>       : (str) deposit to this account
1900
            --wallet_id=<wallet_id>         : (str) limit operation to specific wallet.
1901
            --preview                       : (bool) do not broadcast the transaction
1902
            --blocking                      : (bool) wait until tx has synced
1903

1904
        Returns: {Transaction}
1905
        """
1906
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1907
        account = wallet.get_account_or_default(to_account)
×
1908
        other_tx = await self.wallet_manager.get_transaction(txid)
×
1909
        tx = await Transaction.spend_time_lock(
×
1910
            other_tx.outputs[nout], unhexlify(redeem_script), account
1911
        )
1912
        pk = PrivateKey.from_bytes(
×
1913
            account.ledger, Base58.decode_check(private_key)[1:-1]
1914
        )
1915
        await tx.sign([account], {pk.address: pk})
×
1916
        if not preview:
×
1917
            await self.broadcast_or_release(tx, blocking)
×
1918
            self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())
×
1919
        else:
1920
            await self.ledger.release_tx(tx)
×
1921
        return tx
×
1922

1923
    @requires(WALLET_COMPONENT)
1✔
1924
    def jsonrpc_account_send(self, amount, addresses, account_id=None, wallet_id=None, preview=False, blocking=False):
1✔
1925
        """
1926
        Send the same number of credits to multiple addresses from a specific account (or default account).
1927

1928
        Usage:
1929
            account_send <amount> <addresses>... [--account_id=<account_id>] [--wallet_id=<wallet_id>] [--preview]
1930
                                                 [--blocking]
1931

1932
        Options:
1933
            --account_id=<account_id>  : (str) account to fund the transaction
1934
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
1935
            --preview                  : (bool) do not broadcast the transaction
1936
            --blocking                 : (bool) wait until tx has synced
1937

1938
        Returns: {Transaction}
1939
        """
1940
        return self.jsonrpc_wallet_send(
×
1941
            amount=amount, addresses=addresses, wallet_id=wallet_id,
1942
            change_account_id=account_id, funding_account_ids=[account_id] if account_id else [],
1943
            preview=preview, blocking=blocking
1944
        )
1945

1946
    SYNC_DOC = """
1✔
1947
    Wallet synchronization.
1948
    """
1949

1950
    @requires("wallet")
1✔
1951
    def jsonrpc_sync_hash(self, wallet_id=None):
1✔
1952
        """
1953
        Deterministic hash of the wallet.
1954

1955
        Usage:
1956
            sync_hash [<wallet_id> | --wallet_id=<wallet_id>]
1957

1958
        Options:
1959
            --wallet_id=<wallet_id>   : (str) wallet for which to generate hash
1960

1961
        Returns:
1962
            (str) sha256 hash of wallet
1963
        """
1964
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1965
        return hexlify(wallet.hash).decode()
×
1966

1967
    @requires("wallet")
1✔
1968
    async def jsonrpc_sync_apply(self, password, data=None, wallet_id=None, blocking=False):
1✔
1969
        """
1970
        Apply incoming synchronization data, if provided, and return a sync hash and update wallet data.
1971

1972
        Wallet must be unlocked to perform this operation.
1973

1974
        If "encrypt-on-disk" preference is True and supplied password is different from local password,
1975
        or there is no local password (because local wallet was not encrypted), then the supplied password
1976
        will be used for local encryption (overwriting previous local encryption password).
1977

1978
        Usage:
1979
            sync_apply <password> [--data=<data>] [--wallet_id=<wallet_id>] [--blocking]
1980

1981
        Options:
1982
            --password=<password>         : (str) password to decrypt incoming and encrypt outgoing data
1983
            --data=<data>                 : (str) incoming sync data, if any
1984
            --wallet_id=<wallet_id>       : (str) wallet being sync'ed
1985
            --blocking                    : (bool) wait until any new accounts have sync'ed
1986

1987
        Returns:
1988
            (map) sync hash and data
1989

1990
        """
1991
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
1992
        wallet_changed = False
×
1993
        if data is not None:
×
1994
            added_accounts, merged_accounts = wallet.merge(self.wallet_manager, password, data)
×
1995
            for new_account in itertools.chain(added_accounts, merged_accounts):
×
1996
                await new_account.maybe_migrate_certificates()
×
1997
            if added_accounts and self.ledger.network.is_connected:
×
1998
                if blocking:
×
1999
                    await asyncio.wait([
×
2000
                        a.ledger.subscribe_account(a) for a in added_accounts
2001
                    ])
2002
                else:
2003
                    for new_account in added_accounts:
×
2004
                        asyncio.create_task(self.ledger.subscribe_account(new_account))
×
2005
            wallet_changed = True
×
2006
        if wallet.preferences.get(ENCRYPT_ON_DISK, False) and password != wallet.encryption_password:
×
2007
            wallet.encryption_password = password
×
2008
            wallet_changed = True
×
2009
        if wallet_changed:
×
2010
            wallet.save()
×
2011
        encrypted = wallet.pack(password)
×
2012
        return {
×
2013
            'hash': self.jsonrpc_sync_hash(wallet_id),
2014
            'data': encrypted.decode()
2015
        }
2016

2017
    ADDRESS_DOC = """
1✔
2018
    List, generate and verify addresses.
2019
    """
2020

2021
    @requires(WALLET_COMPONENT)
1✔
2022
    async def jsonrpc_address_is_mine(self, address, account_id=None, wallet_id=None):
1✔
2023
        """
2024
        Checks if an address is associated with the current wallet.
2025

2026
        Usage:
2027
            address_is_mine (<address> | --address=<address>)
2028
                            [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
2029

2030
        Options:
2031
            --address=<address>       : (str) address to check
2032
            --account_id=<account_id> : (str) id of the account to use
2033
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
2034

2035
        Returns:
2036
            (bool) true, if address is associated with current wallet
2037
        """
2038
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2039
        account = wallet.get_account_or_default(account_id)
×
2040
        match = await self.ledger.db.get_address(read_only=True, address=address, accounts=[account])
×
2041
        if match is not None:
×
2042
            return True
×
2043
        return False
×
2044

2045
    @requires(WALLET_COMPONENT)
1✔
2046
    def jsonrpc_address_list(self, address=None, account_id=None, wallet_id=None, page=None, page_size=None):
1✔
2047
        """
2048
        List account addresses or details of single address.
2049

2050
        Usage:
2051
            address_list [--address=<address>] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2052
                         [--page=<page>] [--page_size=<page_size>]
2053

2054
        Options:
2055
            --address=<address>        : (str) just show details for single address
2056
            --account_id=<account_id>  : (str) id of the account to use
2057
            --wallet_id=<wallet_id>    : (str) restrict operation to specific wallet
2058
            --page=<page>              : (int) page to return during paginating
2059
            --page_size=<page_size>    : (int) number of items on page during pagination
2060

2061
        Returns: {Paginated[Address]}
2062
        """
2063
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2064
        constraints = {
×
2065
            'cols': ('address', 'account', 'used_times', 'pubkey', 'chain_code', 'n', 'depth')
2066
        }
2067
        if address:
×
2068
            constraints['address'] = address
×
2069
        if account_id:
×
2070
            constraints['accounts'] = [wallet.get_account_or_error(account_id)]
×
2071
        else:
2072
            constraints['accounts'] = wallet.accounts
×
2073
        return paginate_rows(
×
2074
            self.ledger.get_addresses,
2075
            self.ledger.get_address_count,
2076
            page, page_size, read_only=True, **constraints
2077
        )
2078

2079
    @requires(WALLET_COMPONENT)
1✔
2080
    def jsonrpc_address_unused(self, account_id=None, wallet_id=None):
1✔
2081
        """
2082
        Return an address containing no balance, will create
2083
        a new address if there is none.
2084

2085
        Usage:
2086
            address_unused [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2087

2088
        Options:
2089
            --account_id=<account_id> : (str) id of the account to use
2090
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
2091

2092
        Returns: {Address}
2093
        """
2094
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2095
        return wallet.get_account_or_default(account_id).receiving.get_or_create_usable_address()
×
2096

2097
    FILE_DOC = """
1✔
2098
    File management.
2099
    """
2100

2101
    @requires(FILE_MANAGER_COMPONENT)
1✔
2102
    async def jsonrpc_file_list(self, sort=None, reverse=False, comparison=None, wallet_id=None, page=None,
1✔
2103
                                page_size=None, **kwargs):
2104
        """
2105
        List files limited by optional filters
2106

2107
        Usage:
2108
            file_list [--sd_hash=<sd_hash>] [--file_name=<file_name>] [--stream_hash=<stream_hash>]
2109
                      [--rowid=<rowid>] [--added_on=<added_on>] [--claim_id=<claim_id>]
2110
                      [--outpoint=<outpoint>] [--txid=<txid>] [--nout=<nout>]
2111
                      [--channel_claim_id=<channel_claim_id>] [--channel_name=<channel_name>]
2112
                      [--claim_name=<claim_name>] [--blobs_in_stream=<blobs_in_stream>]
2113
                      [--download_path=<download_path>] [--blobs_remaining=<blobs_remaining>]
2114
                      [--uploading_to_reflector=<uploading_to_reflector>] [--is_fully_reflected=<is_fully_reflected>]
2115
                      [--status=<status>] [--completed=<completed>] [--sort=<sort_by>] [--comparison=<comparison>]
2116
                      [--full_status=<full_status>] [--reverse] [--page=<page>] [--page_size=<page_size>]
2117
                      [--wallet_id=<wallet_id>]
2118

2119
        Options:
2120
            --sd_hash=<sd_hash>                    : (str) get file with matching sd hash
2121
            --file_name=<file_name>                : (str) get file with matching file name in the
2122
                                                     downloads folder
2123
            --stream_hash=<stream_hash>            : (str) get file with matching stream hash
2124
            --rowid=<rowid>                        : (int) get file with matching row id
2125
            --added_on=<added_on>                  : (int) get file with matching time of insertion
2126
            --claim_id=<claim_id>                  : (str) get file with matching claim id(s)
2127
            --outpoint=<outpoint>                  : (str) get file with matching claim outpoint(s)
2128
            --txid=<txid>                          : (str) get file with matching claim txid
2129
            --nout=<nout>                          : (int) get file with matching claim nout
2130
            --channel_claim_id=<channel_claim_id>  : (str) get file with matching channel claim id(s)
2131
            --channel_name=<channel_name>          : (str) get file with matching channel name
2132
            --claim_name=<claim_name>              : (str) get file with matching claim name
2133
            --blobs_in_stream=<blobs_in_stream>    : (int) get file with matching blobs in stream
2134
            --download_path=<download_path>        : (str) get file with matching download path
2135
            --uploading_to_reflector=<uploading_to_reflector> : (bool) get files currently uploading to reflector
2136
            --is_fully_reflected=<is_fully_reflected>         : (bool) get files that have been uploaded to reflector
2137
            --status=<status>                      : (str) match by status, ( running | finished | stopped )
2138
            --completed=<completed>                : (bool) match only completed
2139
            --blobs_remaining=<blobs_remaining>    : (int) amount of remaining blobs to download
2140
            --sort=<sort_by>                       : (str) field to sort by (one of the above filter fields)
2141
            --comparison=<comparison>              : (str) logical comparison, (eq | ne | g | ge | l | le | in)
2142
            --page=<page>                          : (int) page to return during paginating
2143
            --page_size=<page_size>                : (int) number of items on page during pagination
2144
            --wallet_id=<wallet_id>                : (str) add purchase receipts from this wallet
2145

2146
        Returns: {Paginated[File]}
2147
        """
2148
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2149
        sort = sort or 'rowid'
×
2150
        comparison = comparison or 'eq'
×
2151

2152
        paginated = paginate_list(
×
2153
            self.file_manager.get_filtered(sort, reverse, comparison, **kwargs), page, page_size
2154
        )
2155
        if paginated['items']:
×
2156
            receipts = {
×
2157
                txo.purchased_claim_id: txo for txo in
2158
                await self.ledger.db.get_purchases(
2159
                    accounts=wallet.accounts,
2160
                    purchased_claim_id__in=[s.claim_id for s in paginated['items']]
2161
                )
2162
            }
2163
            for stream in paginated['items']:
×
2164
                stream.purchase_receipt = receipts.get(stream.claim_id)
×
2165
        return paginated
×
2166

2167
    @requires(FILE_MANAGER_COMPONENT)
1✔
2168
    async def jsonrpc_file_set_status(self, status, **kwargs):
1✔
2169
        """
2170
        Start or stop downloading a file
2171

2172
        Usage:
2173
            file_set_status (<status> | --status=<status>) [--sd_hash=<sd_hash>]
2174
                      [--file_name=<file_name>] [--stream_hash=<stream_hash>] [--rowid=<rowid>]
2175

2176
        Options:
2177
            --status=<status>            : (str) one of "start" or "stop"
2178
            --sd_hash=<sd_hash>          : (str) set status of file with matching sd hash
2179
            --file_name=<file_name>      : (str) set status of file with matching file name in the
2180
                                           downloads folder
2181
            --stream_hash=<stream_hash>  : (str) set status of file with matching stream hash
2182
            --rowid=<rowid>              : (int) set status of file with matching row id
2183

2184
        Returns:
2185
            (str) Confirmation message
2186
        """
2187

2188
        if status not in ['start', 'stop']:
×
2189
            # TODO: use error from lbry.error
2190
            raise Exception('Status must be "start" or "stop".')
×
2191

2192
        streams = self.file_manager.get_filtered(**kwargs)
×
2193
        if not streams:
×
2194
            # TODO: use error from lbry.error
2195
            raise Exception(f'Unable to find a file for {kwargs}')
×
2196
        stream = streams[0]
×
2197
        if status == 'start' and not stream.running:
×
2198
            if not hasattr(stream, 'bt_infohash') and 'dht' not in self.conf.components_to_skip:
×
2199
                stream.downloader.node = self.dht_node
×
2200
            await stream.save_file()
×
2201
            msg = "Resumed download"
×
2202
        elif status == 'stop' and stream.running:
×
2203
            await stream.stop()
×
2204
            msg = "Stopped download"
×
2205
        else:
2206
            msg = (
×
2207
                "File was already being downloaded" if status == 'start'
2208
                else "File was already stopped"
2209
            )
2210
        return msg
×
2211

2212
    @requires(FILE_MANAGER_COMPONENT)
1✔
2213
    async def jsonrpc_file_delete(self, delete_from_download_dir=False, delete_all=False, **kwargs):
1✔
2214
        """
2215
        Delete a LBRY file
2216

2217
        Usage:
2218
            file_delete [--delete_from_download_dir] [--delete_all] [--sd_hash=<sd_hash>] [--file_name=<file_name>]
2219
                        [--stream_hash=<stream_hash>] [--rowid=<rowid>] [--claim_id=<claim_id>] [--txid=<txid>]
2220
                        [--nout=<nout>] [--claim_name=<claim_name>] [--channel_claim_id=<channel_claim_id>]
2221
                        [--channel_name=<channel_name>]
2222

2223
        Options:
2224
            --delete_from_download_dir             : (bool) delete file from download directory,
2225
                                                    instead of just deleting blobs
2226
            --delete_all                           : (bool) if there are multiple matching files,
2227
                                                     allow the deletion of multiple files.
2228
                                                     Otherwise do not delete anything.
2229
            --sd_hash=<sd_hash>                    : (str) delete by file sd hash
2230
            --file_name=<file_name>                 : (str) delete by file name in downloads folder
2231
            --stream_hash=<stream_hash>            : (str) delete by file stream hash
2232
            --rowid=<rowid>                        : (int) delete by file row id
2233
            --claim_id=<claim_id>                  : (str) delete by file claim id
2234
            --txid=<txid>                          : (str) delete by file claim txid
2235
            --nout=<nout>                          : (int) delete by file claim nout
2236
            --claim_name=<claim_name>              : (str) delete by file claim name
2237
            --channel_claim_id=<channel_claim_id>  : (str) delete by file channel claim id
2238
            --channel_name=<channel_name>                 : (str) delete by file channel claim name
2239

2240
        Returns:
2241
            (bool) true if deletion was successful
2242
        """
2243

2244
        streams = self.file_manager.get_filtered(**kwargs)
×
2245

2246
        if len(streams) > 1:
×
2247
            if not delete_all:
×
2248
                log.warning("There are %i files to delete, use narrower filters to select one",
×
2249
                            len(streams))
2250
                return False
×
2251
            else:
2252
                log.warning("Deleting %i files",
×
2253
                            len(streams))
2254

2255
        if not streams:
×
2256
            log.warning("There is no file to delete")
×
2257
            return False
×
2258
        else:
2259
            for stream in streams:
×
2260
                message = f"Deleted file {stream.file_name}"
×
2261
                await self.file_manager.delete(stream, delete_file=delete_from_download_dir)
×
2262
                log.info(message)
×
2263
            result = True
×
2264
        return result
×
2265

2266
    @requires(FILE_MANAGER_COMPONENT)
1✔
2267
    async def jsonrpc_file_save(self, file_name=None, download_directory=None, **kwargs):
1✔
2268
        """
2269
        Start saving a file to disk.
2270

2271
        Usage:
2272
            file_save [--file_name=<file_name>] [--download_directory=<download_directory>] [--sd_hash=<sd_hash>]
2273
                      [--stream_hash=<stream_hash>] [--rowid=<rowid>] [--claim_id=<claim_id>] [--txid=<txid>]
2274
                      [--nout=<nout>] [--claim_name=<claim_name>] [--channel_claim_id=<channel_claim_id>]
2275
                      [--channel_name=<channel_name>]
2276

2277
        Options:
2278
            --file_name=<file_name>                      : (str) file name to save to
2279
            --download_directory=<download_directory>    : (str) directory to save into
2280
            --sd_hash=<sd_hash>                          : (str) save file with matching sd hash
2281
            --stream_hash=<stream_hash>                  : (str) save file with matching stream hash
2282
            --rowid=<rowid>                              : (int) save file with matching row id
2283
            --claim_id=<claim_id>                        : (str) save file with matching claim id
2284
            --txid=<txid>                                : (str) save file with matching claim txid
2285
            --nout=<nout>                                : (int) save file with matching claim nout
2286
            --claim_name=<claim_name>                    : (str) save file with matching claim name
2287
            --channel_claim_id=<channel_claim_id>        : (str) save file with matching channel claim id
2288
            --channel_name=<channel_name>                : (str) save file with matching channel claim name
2289

2290
        Returns: {File}
2291
        """
2292

2293
        streams = self.file_manager.get_filtered(**kwargs)
×
2294

2295
        if len(streams) > 1:
×
2296
            log.warning("There are %i matching files, use narrower filters to select one", len(streams))
×
2297
            return False
×
2298
        if not streams:
×
2299
            log.warning("There is no file to save")
×
2300
            return False
×
2301
        stream = streams[0]
×
2302
        if not hasattr(stream, 'bt_infohash') and 'dht' not in self.conf.components_to_skip:
×
2303
            stream.downloader.node = self.dht_node
×
2304
        await stream.save_file(file_name, download_directory)
×
2305
        return stream
×
2306

2307
    PURCHASE_DOC = """
1✔
2308
    List and make purchases of claims.
2309
    """
2310

2311
    @requires(WALLET_COMPONENT)
1✔
2312
    def jsonrpc_purchase_list(
1✔
2313
            self, claim_id=None, resolve=False, account_id=None, wallet_id=None, page=None, page_size=None):
2314
        """
2315
        List my claim purchases.
2316

2317
        Usage:
2318
            purchase_list [<claim_id> | --claim_id=<claim_id>] [--resolve]
2319
                          [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2320
                          [--page=<page>] [--page_size=<page_size>]
2321

2322
        Options:
2323
            --claim_id=<claim_id>      : (str) purchases for specific claim
2324
            --resolve                  : (str) include resolved claim information
2325
            --account_id=<account_id>  : (str) id of the account to query
2326
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
2327
            --page=<page>              : (int) page to return during paginating
2328
            --page_size=<page_size>    : (int) number of items on page during pagination
2329

2330
        Returns: {Paginated[Output]}
2331
        """
2332
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2333
        constraints = {
×
2334
            "wallet": wallet,
2335
            "accounts": [wallet.get_account_or_error(account_id)] if account_id else wallet.accounts,
2336
            "resolve": resolve,
2337
        }
2338
        if claim_id:
×
2339
            constraints["purchased_claim_id"] = claim_id
×
2340
        return paginate_rows(
×
2341
            self.ledger.get_purchases,
2342
            self.ledger.get_purchase_count,
2343
            page, page_size, **constraints
2344
        )
2345

2346
    @requires(WALLET_COMPONENT)
1✔
2347
    async def jsonrpc_purchase_create(
1✔
2348
            self, claim_id=None, url=None, wallet_id=None, funding_account_ids=None,
2349
            allow_duplicate_purchase=False, override_max_key_fee=False, preview=False, blocking=False):
2350
        """
2351
        Purchase a claim.
2352

2353
        Usage:
2354
            purchase_create (--claim_id=<claim_id> | --url=<url>) [--wallet_id=<wallet_id>]
2355
                    [--funding_account_ids=<funding_account_ids>...]
2356
                    [--allow_duplicate_purchase] [--override_max_key_fee] [--preview] [--blocking]
2357

2358
        Options:
2359
            --claim_id=<claim_id>          : (str) claim id of claim to purchase
2360
            --url=<url>                    : (str) lookup claim to purchase by url
2361
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
2362
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
2363
            --allow_duplicate_purchase     : (bool) allow purchasing claim_id you already own
2364
            --override_max_key_fee         : (bool) ignore max key fee for this purchase
2365
            --preview                      : (bool) do not broadcast the transaction
2366
            --blocking                     : (bool) wait until transaction is in mempool
2367

2368
        Returns: {Transaction}
2369
        """
2370
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2371
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
2372
        accounts = wallet.get_accounts_or_all(funding_account_ids)
×
2373
        txo = None
×
2374
        if claim_id:
×
2375
            txo = await self.ledger.get_claim_by_claim_id(claim_id, accounts, include_purchase_receipt=True)
×
2376
            if not isinstance(txo, Output) or not txo.is_claim:
×
2377
                # TODO: use error from lbry.error
2378
                raise Exception(f"Could not find claim with claim_id '{claim_id}'.")
×
2379
        elif url:
×
2380
            txo = (await self.ledger.resolve(accounts, [url], include_purchase_receipt=True))[url]
×
2381
            if not isinstance(txo, Output) or not txo.is_claim:
×
2382
                # TODO: use error from lbry.error
2383
                raise Exception(f"Could not find claim with url '{url}'.")
×
2384
        else:
2385
            # TODO: use error from lbry.error
2386
            raise Exception("Missing argument claim_id or url.")
×
2387
        if not allow_duplicate_purchase and txo.purchase_receipt:
×
2388
            raise AlreadyPurchasedError(claim_id)
×
2389
        claim = txo.claim
×
2390
        if not claim.is_stream or not claim.stream.has_fee:
×
2391
            # TODO: use error from lbry.error
2392
            raise Exception(f"Claim '{claim_id}' does not have a purchase price.")
×
2393
        tx = await self.wallet_manager.create_purchase_transaction(
×
2394
            accounts, txo, self.exchange_rate_manager, override_max_key_fee
2395
        )
2396
        if not preview:
×
2397
            await self.broadcast_or_release(tx, blocking)
×
2398
        else:
2399
            await self.ledger.release_tx(tx)
×
2400
        return tx
×
2401

2402
    CLAIM_DOC = """
1✔
2403
    List and search all types of claims.
2404
    """
2405

2406
    @requires(WALLET_COMPONENT)
1✔
2407
    def jsonrpc_claim_list(self, claim_type=None, **kwargs):
1✔
2408
        """
2409
        List my stream and channel claims.
2410

2411
        Usage:
2412
            claim_list [--claim_type=<claim_type>...] [--claim_id=<claim_id>...] [--name=<name>...] [--is_spent]
2413
                       [--reposted_claim_id=<reposted_claim_id>...]
2414
                       [--channel_id=<channel_id>...] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2415
                       [--has_source | --has_no_source] [--page=<page>] [--page_size=<page_size>]
2416
                       [--resolve] [--order_by=<order_by>] [--no_totals] [--include_received_tips]
2417

2418
        Options:
2419
            --claim_type=<claim_type>  : (str or list) claim type: channel, stream, repost, collection
2420
            --claim_id=<claim_id>      : (str or list) claim id
2421
            --channel_id=<channel_id>  : (str or list) streams in this channel
2422
            --name=<name>              : (str or list) claim name
2423
            --is_spent                 : (bool) shows previous claim updates and abandons
2424
            --reposted_claim_id=<reposted_claim_id> : (str or list) reposted claim id
2425
            --account_id=<account_id>  : (str) id of the account to query
2426
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
2427
            --has_source               : (bool) list claims containing a source field
2428
            --has_no_source            : (bool) list claims not containing a source field
2429
            --page=<page>              : (int) page to return during paginating
2430
            --page_size=<page_size>    : (int) number of items on page during pagination
2431
            --resolve                  : (bool) resolves each claim to provide additional metadata
2432
            --order_by=<order_by>      : (str) field to order by: 'name', 'height', 'amount'
2433
            --no_totals                : (bool) do not calculate the total number of pages and items in result set
2434
                                                (significant performance boost)
2435
            --include_received_tips    : (bool) calculate the amount of tips received for claim outputs
2436

2437
        Returns: {Paginated[Output]}
2438
        """
2439
        kwargs['type'] = claim_type or CLAIM_TYPE_NAMES
×
2440
        if not kwargs.get('is_spent', False):
×
2441
            kwargs['is_not_spent'] = True
×
2442
        return self.jsonrpc_txo_list(**kwargs)
×
2443

2444
    async def jsonrpc_support_sum(self, claim_id, new_sdk_server, include_channel_content=False, **kwargs):
1✔
2445
        """
2446
        List total staked supports for a claim, grouped by the channel that signed the support.
2447

2448
        If claim_id is a channel claim, you can use --include_channel_content to also include supports for
2449
        content claims in the channel.
2450

2451
        !!!! NOTE: PAGINATION DOES NOT DO ANYTHING AT THE MOMENT !!!!!
2452

2453
        Usage:
2454
            support_sum <claim_id> <new_sdk_server>
2455
                         [--include_channel_content]
2456
                         [--page=<page>] [--page_size=<page_size>]
2457

2458
        Options:
2459
            --claim_id=<claim_id>             : (str)  claim id
2460
            --new_sdk_server=<new_sdk_server> : (str)  URL of the new SDK server (EXPERIMENTAL)
2461
            --include_channel_content         : (bool) if claim_id is for a channel, include supports for claims in
2462
                                                       that channel
2463
            --page=<page>                     : (int)  page to return during paginating
2464
            --page_size=<page_size>           : (int)  number of items on page during pagination
2465

2466
        Returns: {Paginated[Dict]}
2467
        """
2468
        page_num, page_size = abs(kwargs.pop('page', 1)), min(abs(kwargs.pop('page_size', DEFAULT_PAGE_SIZE)), 50)
×
2469
        kwargs.update({'offset': page_size * (page_num - 1), 'limit': page_size})
×
2470
        support_sums = await self.ledger.sum_supports(
×
2471
            new_sdk_server, claim_id=claim_id, include_channel_content=include_channel_content, **kwargs
2472
        )
2473
        return {
×
2474
            "items": support_sums,
2475
            "page": page_num,
2476
            "page_size": page_size
2477
        }
2478

2479
    @requires(WALLET_COMPONENT)
1✔
2480
    async def jsonrpc_claim_search(self, **kwargs):
1✔
2481
        """
2482
        Search for stream and channel claims on the blockchain.
2483

2484
        Arguments marked with "supports equality constraints" allow prepending the
2485
        value with an equality constraint such as '>', '>=', '<' and '<='
2486
        eg. --height=">400000" would limit results to only claims above 400k block height.
2487

2488
        They also support multiple constraints passed as a list of the args described above.
2489
        eg. --release_time=[">1000000", "<2000000"]
2490

2491
        Usage:
2492
            claim_search [<name> | --name=<name>] [--text=<text>] [--txid=<txid>] [--nout=<nout>]
2493
                         [--claim_id=<claim_id> | --claim_ids=<claim_ids>...]
2494
                         [--channel=<channel> |
2495
                             [[--channel_ids=<channel_ids>...] [--not_channel_ids=<not_channel_ids>...]]]
2496
                         [--has_channel_signature] [--valid_channel_signature | --invalid_channel_signature]
2497
                         [--limit_claims_per_channel=<limit_claims_per_channel>]
2498
                         [--is_controlling] [--release_time=<release_time>] [--public_key_id=<public_key_id>]
2499
                         [--timestamp=<timestamp>] [--creation_timestamp=<creation_timestamp>]
2500
                         [--height=<height>] [--creation_height=<creation_height>]
2501
                         [--activation_height=<activation_height>] [--expiration_height=<expiration_height>]
2502
                         [--amount=<amount>] [--effective_amount=<effective_amount>]
2503
                         [--support_amount=<support_amount>] [--trending_group=<trending_group>]
2504
                         [--trending_mixed=<trending_mixed>] [--trending_local=<trending_local>]
2505
                         [--trending_global=<trending_global] [--trending_score=<trending_score]
2506
                         [--reposted_claim_id=<reposted_claim_id>] [--reposted=<reposted>]
2507
                         [--claim_type=<claim_type>] [--stream_types=<stream_types>...] [--media_types=<media_types>...]
2508
                         [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>]
2509
                         [--duration=<duration>]
2510
                         [--any_tags=<any_tags>...] [--all_tags=<all_tags>...] [--not_tags=<not_tags>...]
2511
                         [--any_languages=<any_languages>...] [--all_languages=<all_languages>...]
2512
                         [--not_languages=<not_languages>...]
2513
                         [--any_locations=<any_locations>...] [--all_locations=<all_locations>...]
2514
                         [--not_locations=<not_locations>...]
2515
                         [--order_by=<order_by>...] [--no_totals] [--page=<page>] [--page_size=<page_size>]
2516
                         [--wallet_id=<wallet_id>] [--include_purchase_receipt] [--include_is_my_output]
2517
                         [--remove_duplicates] [--has_source | --has_no_source] [--sd_hash=<sd_hash>]
2518
                         [--new_sdk_server=<new_sdk_server>]
2519

2520
        Options:
2521
            --name=<name>                   : (str) claim name (normalized)
2522
            --text=<text>                   : (str) full text search
2523
            --claim_id=<claim_id>           : (str) full or partial claim id
2524
            --claim_ids=<claim_ids>         : (list) list of full claim ids
2525
            --txid=<txid>                   : (str) transaction id
2526
            --nout=<nout>                   : (str) position in the transaction
2527
            --channel=<channel>             : (str) claims signed by this channel (argument is
2528
                                                    a URL which automatically gets resolved),
2529
                                                    see --channel_ids if you need to filter by
2530
                                                    multiple channels at the same time,
2531
                                                    includes claims with invalid signatures,
2532
                                                    use in conjunction with --valid_channel_signature
2533
            --channel_ids=<channel_ids>     : (list) claims signed by any of these channels
2534
                                                    (arguments must be claim ids of the channels),
2535
                                                    includes claims with invalid signatures,
2536
                                                    implies --has_channel_signature,
2537
                                                    use in conjunction with --valid_channel_signature
2538
            --not_channel_ids=<not_channel_ids>: (list) exclude claims signed by any of these channels
2539
                                                    (arguments must be claim ids of the channels)
2540
            --has_channel_signature         : (bool) claims with a channel signature (valid or invalid)
2541
            --valid_channel_signature       : (bool) claims with a valid channel signature or no signature,
2542
                                                     use in conjunction with --has_channel_signature to
2543
                                                     only get claims with valid signatures
2544
            --invalid_channel_signature     : (bool) claims with invalid channel signature or no signature,
2545
                                                     use in conjunction with --has_channel_signature to
2546
                                                     only get claims with invalid signatures
2547
            --limit_claims_per_channel=<limit_claims_per_channel>: (int) only return up to the specified
2548
                                                                         number of claims per channel
2549
            --is_controlling                : (bool) winning claims of their respective name
2550
            --public_key_id=<public_key_id> : (str) only return channels having this public key id, this is
2551
                                                    the same key as used in the wallet file to map
2552
                                                    channel certificate private keys: {'public_key_id': 'private key'}
2553
            --height=<height>               : (int) last updated block height (supports equality constraints)
2554
            --timestamp=<timestamp>         : (int) last updated timestamp (supports equality constraints)
2555
            --creation_height=<creation_height>      : (int) created at block height (supports equality constraints)
2556
            --creation_timestamp=<creation_timestamp>: (int) created at timestamp (supports equality constraints)
2557
            --activation_height=<activation_height>  : (int) height at which claim starts competing for name
2558
                                                             (supports equality constraints)
2559
            --expiration_height=<expiration_height>  : (int) height at which claim will expire
2560
                                                             (supports equality constraints)
2561
            --release_time=<release_time>   : (int) limit to claims self-described as having been
2562
                                                    released to the public on or after this UTC
2563
                                                    timestamp, when claim does not provide
2564
                                                    a release time the publish time is used instead
2565
                                                    (supports equality constraints)
2566
            --amount=<amount>               : (int) limit by claim value (supports equality constraints)
2567
            --support_amount=<support_amount>: (int) limit by supports and tips received (supports
2568
                                                    equality constraints)
2569
            --effective_amount=<effective_amount>: (int) limit by total value (initial claim value plus
2570
                                                     all tips and supports received), this amount is
2571
                                                     blank until claim has reached activation height
2572
                                                     (supports equality constraints)
2573
            --trending_score=<trending_score>: (int) limit by trending score (supports equality constraints)
2574
            --trending_group=<trending_group>: (int) DEPRECATED - instead please use trending_score
2575
            --trending_mixed=<trending_mixed>: (int) DEPRECATED - instead please use trending_score
2576
            --trending_local=<trending_local>: (int) DEPRECATED - instead please use trending_score
2577
            --trending_global=<trending_global>: (int) DEPRECATED - instead please use trending_score
2578
            --reposted_claim_id=<reposted_claim_id>: (str) all reposts of the specified original claim id
2579
            --reposted=<reposted>           : (int) claims reposted this many times (supports
2580
                                                    equality constraints)
2581
            --claim_type=<claim_type>       : (str) filter by 'channel', 'stream', 'repost' or 'collection'
2582
            --stream_types=<stream_types>   : (list) filter by 'video', 'image', 'document', etc
2583
            --media_types=<media_types>     : (list) filter by 'video/mp4', 'image/png', etc
2584
            --fee_currency=<fee_currency>   : (string) specify fee currency: LBC, BTC, USD
2585
            --fee_amount=<fee_amount>       : (decimal) content download fee (supports equality constraints)
2586
            --duration=<duration>           : (int) duration of video or audio in seconds
2587
                                                     (supports equality constraints)
2588
            --any_tags=<any_tags>           : (list) find claims containing any of the tags
2589
            --all_tags=<all_tags>           : (list) find claims containing every tag
2590
            --not_tags=<not_tags>           : (list) find claims not containing any of these tags
2591
            --any_languages=<any_languages> : (list) find claims containing any of the languages
2592
            --all_languages=<all_languages> : (list) find claims containing every language
2593
            --not_languages=<not_languages> : (list) find claims not containing any of these languages
2594
            --any_locations=<any_locations> : (list) find claims containing any of the locations
2595
            --all_locations=<all_locations> : (list) find claims containing every location
2596
            --not_locations=<not_locations> : (list) find claims not containing any of these locations
2597
            --page=<page>                   : (int) page to return during paginating
2598
            --page_size=<page_size>         : (int) number of items on page during pagination
2599
            --order_by=<order_by>           : (list) field to order by, default is descending order, to do an
2600
                                                    ascending order prepend ^ to the field name, eg. '^amount'
2601
                                                    available fields: 'name', 'height', 'release_time',
2602
                                                    'publish_time', 'amount', 'effective_amount',
2603
                                                    'support_amount', 'trending_group', 'trending_mixed',
2604
                                                    'trending_local', 'trending_global', 'activation_height'
2605
            --no_totals                     : (bool) do not calculate the total number of pages and items in result set
2606
                                                     (significant performance boost)
2607
            --wallet_id=<wallet_id>         : (str) wallet to check for claim purchase receipts
2608
            --include_purchase_receipt      : (bool) lookup and include a receipt if this wallet
2609
                                                     has purchased the claim
2610
            --include_is_my_output          : (bool) lookup and include a boolean indicating
2611
                                                     if claim being resolved is yours
2612
            --remove_duplicates             : (bool) removes duplicated content from search by picking either the
2613
                                                     original claim or the oldest matching repost
2614
            --has_source                    : (bool) find claims containing a source field
2615
            --sd_hash=<sd_hash>             : (str)  find claims where the source stream descriptor hash matches
2616
                                                     (partially or completely) the given hexadecimal string
2617
            --has_no_source                 : (bool) find claims not containing a source field
2618
           --new_sdk_server=<new_sdk_server> : (str) URL of the new SDK server (EXPERIMENTAL)
2619

2620
        Returns: {Paginated[Output]}
2621
        """
2622
        if "claim_ids" in kwargs and not kwargs["claim_ids"]:
×
2623
            kwargs.pop("claim_ids")
×
2624
        if {'claim_id', 'claim_ids'}.issubset(kwargs):
×
2625
            raise ConflictingInputValueError('claim_id', 'claim_ids')
×
2626
        if kwargs.pop('valid_channel_signature', False):
×
2627
            kwargs['signature_valid'] = 1
×
2628
        if kwargs.pop('invalid_channel_signature', False):
×
2629
            kwargs['signature_valid'] = 0
×
2630
        if 'has_no_source' in kwargs:
×
2631
            kwargs['has_source'] = not kwargs.pop('has_no_source')
×
2632
        if 'order_by' in kwargs:  # TODO: remove this after removing support for old trending args from the api
×
2633
            value = kwargs.pop('order_by')
×
2634
            value = value if isinstance(value, list) else [value]
×
2635
            new_value = []
×
2636
            for new_v in value:
×
2637
                migrated = new_v if new_v not in (
×
2638
                    'trending_mixed', 'trending_local', 'trending_global', 'trending_group'
2639
                ) else 'trending_score'
2640
                if migrated not in new_value:
×
2641
                    new_value.append(migrated)
×
2642
            kwargs['order_by'] = new_value
×
2643
        page_num, page_size = abs(kwargs.pop('page', 1)), min(abs(kwargs.pop('page_size', DEFAULT_PAGE_SIZE)), 50)
×
2644
        wallet = self.wallet_manager.get_wallet_or_default(kwargs.pop('wallet_id', None))
×
2645
        kwargs.update({'offset': page_size * (page_num - 1), 'limit': page_size})
×
2646
        txos, blocked, _, total = await self.ledger.claim_search(wallet.accounts, **kwargs)
×
2647
        result = {
×
2648
            "items": txos,
2649
            "blocked": blocked,
2650
            "page": page_num,
2651
            "page_size": page_size
2652
        }
2653
        if not kwargs.pop('no_totals', False):
×
2654
            result['total_pages'] = int((total + (page_size - 1)) / page_size)
×
2655
            result['total_items'] = total
×
2656
        return result
×
2657

2658
    CHANNEL_DOC = """
1✔
2659
    Create, update, abandon and list your channel claims.
2660
    """
2661

2662
    @deprecated('channel_create')
1✔
2663
    def jsonrpc_channel_new(self):
1✔
2664
        """ deprecated """
2665

2666
    @requires(WALLET_COMPONENT)
1✔
2667
    async def jsonrpc_channel_create(
1✔
2668
            self, name, bid, allow_duplicate_name=False, account_id=None, wallet_id=None,
2669
            claim_address=None, funding_account_ids=None, preview=False, blocking=False, **kwargs):
2670
        """
2671
        Create a new channel by generating a channel private key and establishing an '@' prefixed claim.
2672

2673
        Usage:
2674
            channel_create (<name> | --name=<name>) (<bid> | --bid=<bid>)
2675
                           [--allow_duplicate_name=<allow_duplicate_name>]
2676
                           [--title=<title>] [--description=<description>] [--email=<email>]
2677
                           [--website_url=<website_url>] [--featured=<featured>...]
2678
                           [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
2679
                           [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]
2680
                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2681
                           [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]
2682
                           [--preview] [--blocking]
2683

2684
        Options:
2685
            --name=<name>                  : (str) name of the channel prefixed with '@'
2686
            --bid=<bid>                    : (decimal) amount to back the claim
2687
        --allow_duplicate_name=<allow_duplicate_name> : (bool) create new channel even if one already exists with
2688
                                              given name. default: false.
2689
            --title=<title>                : (str) title of the publication
2690
            --description=<description>    : (str) description of the publication
2691
            --email=<email>                : (str) email of channel owner
2692
            --website_url=<website_url>    : (str) website url
2693
            --featured=<featured>          : (list) claim_ids of featured content in channel
2694
            --tags=<tags>                  : (list) content tags
2695
            --languages=<languages>        : (list) languages used by the channel,
2696
                                                    using RFC 5646 format, eg:
2697
                                                    for English `--languages=en`
2698
                                                    for Spanish (Spain) `--languages=es-ES`
2699
                                                    for Spanish (Mexican) `--languages=es-MX`
2700
                                                    for Chinese (Simplified) `--languages=zh-Hans`
2701
                                                    for Chinese (Traditional) `--languages=zh-Hant`
2702
            --locations=<locations>        : (list) locations of the channel, consisting of 2 letter
2703
                                                    `country` code and a `state`, `city` and a postal
2704
                                                    `code` along with a `latitude` and `longitude`.
2705
                                                    for JSON RPC: pass a dictionary with aforementioned
2706
                                                        attributes as keys, eg:
2707
                                                        ...
2708
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
2709
                                                        ...
2710
                                                    for command line: pass a colon delimited list
2711
                                                        with values in the following order:
2712

2713
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
2714

2715
                                                        making sure to include colon for blank values, for
2716
                                                        example to provide only the city:
2717

2718
                                                          ... --locations="::Manchester"
2719

2720
                                                        with all values set:
2721

2722
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
2723

2724
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
2725

2726
                                                          ... --locations="42.990605:-71.460989"
2727

2728
                                                        finally, you can also pass JSON string of dictionary
2729
                                                        on the command line as you would via JSON RPC
2730

2731
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
2732

2733
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
2734
            --cover_url=<cover_url>        : (str) url of cover image
2735
            --account_id=<account_id>      : (str) account to use for holding the transaction
2736
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
2737
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
2738
            --claim_address=<claim_address>: (str) address where the channel is sent to, if not specified
2739
                                                   it will be determined automatically from the account
2740
            --preview                      : (bool) do not broadcast the transaction
2741
            --blocking                     : (bool) wait until transaction is in mempool
2742

2743
        Returns: {Transaction}
2744
        """
2745
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2746
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
2747
        account = wallet.get_account_or_default(account_id)
×
2748
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
2749
        self.valid_channel_name_or_error(name)
×
2750
        amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
2751
        claim_address = await self.get_receiving_address(claim_address, account)
×
2752

2753
        existing_channels = await self.ledger.get_channels(accounts=wallet.accounts, claim_name=name)
×
2754
        if len(existing_channels) > 0:
×
2755
            if not allow_duplicate_name:
×
2756
                # TODO: use error from lbry.error
2757
                raise Exception(
×
2758
                    f"You already have a channel under the name '{name}'. "
2759
                    f"Use --allow-duplicate-name flag to override."
2760
                )
2761

2762
        claim = Claim()
×
2763
        claim.channel.update(**kwargs)
×
2764
        tx = await Transaction.claim_create(
×
2765
            name, claim, amount, claim_address, funding_accounts, funding_accounts[0]
2766
        )
2767
        txo = tx.outputs[0]
×
2768
        txo.set_channel_private_key(
×
2769
            await funding_accounts[0].generate_channel_private_key()
2770
        )
2771

2772
        await tx.sign(funding_accounts)
×
2773

2774
        if not preview:
×
2775
            wallet.save()
×
2776
            await self.broadcast_or_release(tx, blocking)
×
2777
            self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info(
×
2778
                tx, txo, claim_address, claim, name
2779
            )]))
2780
            self.component_manager.loop.create_task(self.analytics_manager.send_new_channel())
×
2781
        else:
2782
            await account.ledger.release_tx(tx)
×
2783

2784
        return tx
×
2785

2786
    @requires(WALLET_COMPONENT)
1✔
2787
    async def jsonrpc_channel_update(
1✔
2788
            self, claim_id, bid=None, account_id=None, wallet_id=None, claim_address=None,
2789
            funding_account_ids=None, new_signing_key=False, preview=False,
2790
            blocking=False, replace=False, **kwargs):
2791
        """
2792
        Update an existing channel claim.
2793

2794
        Usage:
2795
            channel_update (<claim_id> | --claim_id=<claim_id>) [<bid> | --bid=<bid>]
2796
                           [--title=<title>] [--description=<description>] [--email=<email>]
2797
                           [--website_url=<website_url>]
2798
                           [--featured=<featured>...] [--clear_featured]
2799
                           [--tags=<tags>...] [--clear_tags]
2800
                           [--languages=<languages>...] [--clear_languages]
2801
                           [--locations=<locations>...] [--clear_locations]
2802
                           [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]
2803
                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2804
                           [--claim_address=<claim_address>] [--new_signing_key]
2805
                           [--funding_account_ids=<funding_account_ids>...]
2806
                           [--preview] [--blocking] [--replace]
2807

2808
        Options:
2809
            --claim_id=<claim_id>          : (str) claim_id of the channel to update
2810
            --bid=<bid>                    : (decimal) amount to back the claim
2811
            --title=<title>                : (str) title of the publication
2812
            --description=<description>    : (str) description of the publication
2813
            --email=<email>                : (str) email of channel owner
2814
            --website_url=<website_url>    : (str) website url
2815
            --featured=<featured>          : (list) claim_ids of featured content in channel
2816
            --clear_featured               : (bool) clear existing featured content (prior to adding new ones)
2817
            --tags=<tags>                  : (list) add content tags
2818
            --clear_tags                   : (bool) clear existing tags (prior to adding new ones)
2819
            --languages=<languages>        : (list) languages used by the channel,
2820
                                                    using RFC 5646 format, eg:
2821
                                                    for English `--languages=en`
2822
                                                    for Spanish (Spain) `--languages=es-ES`
2823
                                                    for Spanish (Mexican) `--languages=es-MX`
2824
                                                    for Chinese (Simplified) `--languages=zh-Hans`
2825
                                                    for Chinese (Traditional) `--languages=zh-Hant`
2826
            --clear_languages              : (bool) clear existing languages (prior to adding new ones)
2827
            --locations=<locations>        : (list) locations of the channel, consisting of 2 letter
2828
                                                    `country` code and a `state`, `city` and a postal
2829
                                                    `code` along with a `latitude` and `longitude`.
2830
                                                    for JSON RPC: pass a dictionary with aforementioned
2831
                                                        attributes as keys, eg:
2832
                                                        ...
2833
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
2834
                                                        ...
2835
                                                    for command line: pass a colon delimited list
2836
                                                        with values in the following order:
2837

2838
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
2839

2840
                                                        making sure to include colon for blank values, for
2841
                                                        example to provide only the city:
2842

2843
                                                          ... --locations="::Manchester"
2844

2845
                                                        with all values set:
2846

2847
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
2848

2849
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
2850

2851
                                                          ... --locations="42.990605:-71.460989"
2852

2853
                                                        finally, you can also pass JSON string of dictionary
2854
                                                        on the command line as you would via JSON RPC
2855

2856
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
2857

2858
            --clear_locations              : (bool) clear existing locations (prior to adding new ones)
2859
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
2860
            --cover_url=<cover_url>        : (str) url of cover image
2861
            --account_id=<account_id>      : (str) account in which to look for channel (default: all)
2862
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
2863
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
2864
            --claim_address=<claim_address>: (str) address where the channel is sent
2865
            --new_signing_key              : (bool) generate a new signing key, will invalidate all previous publishes
2866
            --preview                      : (bool) do not broadcast the transaction
2867
            --blocking                     : (bool) wait until transaction is in mempool
2868
            --replace                      : (bool) instead of modifying specific values on
2869
                                                    the channel, this will clear all existing values
2870
                                                    and only save passed in values, useful for form
2871
                                                    submissions where all values are always set
2872

2873
        Returns: {Transaction}
2874
        """
2875
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2876
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
2877
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
2878
        if account_id:
×
2879
            account = wallet.get_account_or_error(account_id)
×
2880
            accounts = [account]
×
2881
        else:
2882
            account = wallet.default_account
×
2883
            accounts = wallet.accounts
×
2884

2885
        existing_channels = await self.ledger.get_claims(
×
2886
            wallet=wallet, accounts=accounts, claim_id=claim_id
2887
        )
2888
        if len(existing_channels) != 1:
×
2889
            account_ids = ', '.join(f"'{account.id}'" for account in accounts)
×
2890
            # TODO: use error from lbry.error
2891
            raise Exception(
×
2892
                f"Can't find the channel '{claim_id}' in account(s) {account_ids}."
2893
            )
2894
        old_txo = existing_channels[0]
×
2895
        if not old_txo.claim.is_channel:
×
2896
            # TODO: use error from lbry.error
2897
            raise Exception(
×
2898
                f"A claim with id '{claim_id}' was found but it is not a channel."
2899
            )
2900

2901
        if bid is not None:
×
2902
            amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
2903
        else:
2904
            amount = old_txo.amount
×
2905

2906
        if claim_address is not None:
×
2907
            self.valid_address_or_error(claim_address)
×
2908
        else:
2909
            claim_address = old_txo.get_address(account.ledger)
×
2910

2911
        if replace:
×
2912
            claim = Claim()
×
2913
            claim.channel.public_key_bytes = old_txo.claim.channel.public_key_bytes
×
2914
        else:
2915
            claim = Claim.from_bytes(old_txo.claim.to_bytes())
×
2916
        claim.channel.update(**kwargs)
×
2917
        tx = await Transaction.claim_update(
×
2918
            old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0]
2919
        )
2920
        new_txo = tx.outputs[0]
×
2921

2922
        if new_signing_key:
×
2923
            new_txo.set_channel_private_key(
×
2924
                await funding_accounts[0].generate_channel_private_key()
2925
            )
2926
        else:
2927
            new_txo.private_key = old_txo.private_key
×
2928

2929
        new_txo.script.generate()
×
2930

2931
        await tx.sign(funding_accounts)
×
2932

2933
        if not preview:
×
2934
            wallet.save()
×
2935
            await self.broadcast_or_release(tx, blocking)
×
2936
            self.component_manager.loop.create_task(self.storage.save_claims([self._old_get_temp_claim_info(
×
2937
                tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name
2938
            )]))
2939
            self.component_manager.loop.create_task(self.analytics_manager.send_new_channel())
×
2940
        else:
2941
            await account.ledger.release_tx(tx)
×
2942

2943
        return tx
×
2944

2945
    @requires(WALLET_COMPONENT)
1✔
2946
    async def jsonrpc_channel_sign(
1✔
2947
            self, channel_name=None, channel_id=None, hexdata=None, salt=None,
2948
            channel_account_id=None, wallet_id=None):
2949
        """
2950
        Signs data using the specified channel signing key.
2951

2952
        Usage:
2953
            channel_sign [<channel_name> | --channel_name=<channel_name>] [<channel_id> | --channel_id=<channel_id>]
2954
                         [<hexdata> | --hexdata=<hexdata>] [<salt> | --salt=<salt>]
2955
                         [--channel_account_id=<channel_account_id>...] [--wallet_id=<wallet_id>]
2956

2957
        Options:
2958
            --channel_name=<channel_name>            : (str) name of channel used to sign (or use channel id)
2959
            --channel_id=<channel_id>                : (str) claim id of channel used to sign (or use channel name)
2960
            --hexdata=<hexdata>                      : (str) data to sign, encoded as hexadecimal
2961
            --salt=<salt>                            : (str) salt to use for signing, default is to use timestamp
2962
            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
2963
                                                             for channel certificates, defaults to all accounts.
2964
            --wallet_id=<wallet_id>                  : (str) restrict operation to specific wallet
2965

2966
        Returns:
2967
            (dict) Signature if successfully made, (None) or an error otherwise
2968
            {
2969
                "signature":    (str) The signature of the comment,
2970
                "signing_ts":   (str) The timestamp used to sign the comment,
2971
            }
2972
        """
2973
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
2974
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
2975
        signing_channel = await self.get_channel_or_error(
×
2976
            wallet, channel_account_id, channel_id, channel_name, for_signing=True
2977
        )
2978
        if salt is None:
×
2979
            salt = str(int(time.time()))
×
2980
        signature = signing_channel.sign_data(unhexlify(str(hexdata)), salt)
×
2981
        return {
×
2982
            'signature': signature,
2983
            'signing_ts': salt,  # DEPRECATED
2984
            'salt': salt,
2985
        }
2986

2987
    @requires(WALLET_COMPONENT)
1✔
2988
    async def jsonrpc_channel_abandon(
1✔
2989
            self, claim_id=None, txid=None, nout=None, account_id=None, wallet_id=None,
2990
            preview=False, blocking=True):
2991
        """
2992
        Abandon one of my channel claims.
2993

2994
        Usage:
2995
            channel_abandon [<claim_id> | --claim_id=<claim_id>]
2996
                            [<txid> | --txid=<txid>] [<nout> | --nout=<nout>]
2997
                            [--account_id=<account_id>] [--wallet_id=<wallet_id>]
2998
                            [--preview] [--blocking]
2999

3000
        Options:
3001
            --claim_id=<claim_id>     : (str) claim_id of the claim to abandon
3002
            --txid=<txid>             : (str) txid of the claim to abandon
3003
            --nout=<nout>             : (int) nout of the claim to abandon
3004
            --account_id=<account_id> : (str) id of the account to use
3005
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
3006
            --preview                 : (bool) do not broadcast the transaction
3007
            --blocking                : (bool) wait until abandon is in mempool
3008

3009
        Returns: {Transaction}
3010
        """
3011
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3012
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
3013
        if account_id:
×
3014
            account = wallet.get_account_or_error(account_id)
×
3015
            accounts = [account]
×
3016
        else:
3017
            account = wallet.default_account
×
3018
            accounts = wallet.accounts
×
3019

3020
        if txid is not None and nout is not None:
×
3021
            claims = await self.ledger.get_claims(
×
3022
                wallet=wallet, accounts=accounts, **{'txo.txid': txid, 'txo.position': nout}
3023
            )
3024
        elif claim_id is not None:
×
3025
            claims = await self.ledger.get_claims(
×
3026
                wallet=wallet, accounts=accounts, claim_id=claim_id
3027
            )
3028
        else:
3029
            # TODO: use error from lbry.error
3030
            raise Exception('Must specify claim_id, or txid and nout')
×
3031

3032
        if not claims:
×
3033
            # TODO: use error from lbry.error
3034
            raise Exception('No claim found for the specified claim_id or txid:nout')
×
3035

3036
        tx = await Transaction.create(
×
3037
            [Input.spend(txo) for txo in claims], [], [account], account
3038
        )
3039

3040
        if not preview:
×
3041
            await self.broadcast_or_release(tx, blocking)
×
3042
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('abandon'))
×
3043
        else:
3044
            await account.ledger.release_tx(tx)
×
3045

3046
        return tx
×
3047

3048
    @requires(WALLET_COMPONENT)
1✔
3049
    def jsonrpc_channel_list(self, *args, **kwargs):
1✔
3050
        """
3051
        List my channel claims.
3052

3053
        Usage:
3054
            channel_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
3055
                         [--name=<name>...] [--claim_id=<claim_id>...] [--is_spent]
3056
                         [--page=<page>] [--page_size=<page_size>] [--resolve] [--no_totals]
3057

3058
        Options:
3059
            --name=<name>              : (str or list) channel name
3060
            --claim_id=<claim_id>      : (str or list) channel id
3061
            --is_spent                 : (bool) shows previous channel updates and abandons
3062
            --account_id=<account_id>  : (str) id of the account to use
3063
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
3064
            --page=<page>              : (int) page to return during paginating
3065
            --page_size=<page_size>    : (int) number of items on page during pagination
3066
            --resolve                  : (bool) resolves each channel to provide additional metadata
3067
            --no_totals                : (bool) do not calculate the total number of pages and items in result set
3068
                                                (significant performance boost)
3069

3070
        Returns: {Paginated[Output]}
3071
        """
3072
        kwargs['type'] = 'channel'
×
3073
        if 'is_spent' not in kwargs or not kwargs['is_spent']:
×
3074
            kwargs['is_not_spent'] = True
×
3075
        return self.jsonrpc_txo_list(*args, **kwargs)
×
3076

3077
    @requires(WALLET_COMPONENT)
1✔
3078
    async def jsonrpc_channel_export(self, channel_id=None, channel_name=None, account_id=None, wallet_id=None):
1✔
3079
        """
3080
        Export channel private key.
3081

3082
        Usage:
3083
            channel_export (<channel_id> | --channel_id=<channel_id> | --channel_name=<channel_name>)
3084
                           [--account_id=<account_id>...] [--wallet_id=<wallet_id>]
3085

3086
        Options:
3087
            --channel_id=<channel_id>     : (str) claim id of channel to export
3088
            --channel_name=<channel_name> : (str) name of channel to export
3089
            --account_id=<account_id>     : (str) one or more account ids for accounts
3090
                                                  to look in for channels, defaults to
3091
                                                  all accounts.
3092
            --wallet_id=<wallet_id>       : (str) restrict operation to specific wallet
3093

3094
        Returns:
3095
            (str) serialized channel private key
3096
        """
3097
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3098
        channel = await self.get_channel_or_error(wallet, account_id, channel_id, channel_name, for_signing=True)
×
3099
        address = channel.get_address(self.ledger)
×
3100
        public_key = await self.ledger.get_public_key_for_address(wallet, address)
×
3101
        if not public_key:
×
3102
            # TODO: use error from lbry.error
3103
            raise Exception("Can't find public key for address holding the channel.")
×
3104
        export = {
×
3105
            'name': channel.claim_name,
3106
            'channel_id': channel.claim_id,
3107
            'holding_address': address,
3108
            'holding_public_key': public_key.extended_key_string(),
3109
            'signing_private_key': channel.private_key.signing_key.to_pem().decode()
3110
        }
3111
        return base58.b58encode(json.dumps(export, separators=(',', ':')))
×
3112

3113
    @requires(WALLET_COMPONENT)
1✔
3114
    async def jsonrpc_channel_import(self, channel_data, wallet_id=None):
1✔
3115
        """
3116
        Import serialized channel private key (to allow signing new streams to the channel)
3117

3118
        Usage:
3119
            channel_import (<channel_data> | --channel_data=<channel_data>) [--wallet_id=<wallet_id>]
3120

3121
        Options:
3122
            --channel_data=<channel_data> : (str) serialized channel, as exported by channel export
3123
            --wallet_id=<wallet_id>       : (str) import into specific wallet
3124

3125
        Returns:
3126
            (dict) Result dictionary
3127
        """
3128
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3129

3130
        decoded = base58.b58decode(channel_data)
×
3131
        data = json.loads(decoded)
×
3132
        channel_private_key = PrivateKey.from_pem(
×
3133
            self.ledger, data['signing_private_key']
3134
        )
3135

3136
        # check that the holding_address hasn't changed since the export was made
3137
        holding_address = data['holding_address']
×
3138
        channels, _, _, _ = await self.ledger.claim_search(
×
3139
            wallet.accounts, public_key_id=channel_private_key.address
3140
        )
3141
        if channels and channels[0].get_address(self.ledger) != holding_address:
×
3142
            holding_address = channels[0].get_address(self.ledger)
×
3143

3144
        account = await self.ledger.get_account_for_address(wallet, holding_address)
×
3145
        if account:
×
3146
            # Case 1: channel holding address is in one of the accounts we already have
3147
            #         simply add the certificate to existing account
3148
            pass
×
3149
        else:
3150
            # Case 2: channel holding address hasn't changed and thus is in the bundled read-only account
3151
            #         create a single-address holding account to manage the channel
3152
            if holding_address == data['holding_address']:
×
3153
                account = Account.from_dict(self.ledger, wallet, {
×
3154
                    'name': f"Holding Account For Channel {data['name']}",
3155
                    'public_key': data['holding_public_key'],
3156
                    'address_generator': {'name': 'single-address'}
3157
                })
3158
                if self.ledger.network.is_connected:
×
3159
                    await self.ledger.subscribe_account(account)
×
3160
                    await self.ledger._update_tasks.done.wait()
×
3161
            # Case 3: the holding address has changed and we can't create or find an account for it
3162
            else:
3163
                # TODO: use error from lbry.error
3164
                raise Exception(
×
3165
                    "Channel owning account has changed since the channel was exported and "
3166
                    "it is not an account to which you have access."
3167
                )
3168
        account.add_channel_private_key(channel_private_key)
×
3169
        wallet.save()
×
3170
        return f"Added channel signing key for {data['name']}."
×
3171

3172
    STREAM_DOC = """
1✔
3173
    Create, update, abandon, list and inspect your stream claims.
3174
    """
3175

3176
    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)
1✔
3177
    async def jsonrpc_publish(self, name, **kwargs):
1✔
3178
        """
3179
        Create or replace a stream claim at a given name (use 'stream create/update' for more control).
3180

3181
        Usage:
3182
            publish (<name> | --name=<name>) [--bid=<bid>] [--file_path=<file_path>]
3183
                    [--file_name=<file_name>] [--file_hash=<file_hash>] [--validate_file] [--optimize_file]
3184
                    [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
3185
                    [--title=<title>] [--description=<description>] [--author=<author>]
3186
                    [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
3187
                    [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
3188
                    [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]
3189
                    [--sd_hash=<sd_hash>] [--channel_id=<channel_id> | --channel_name=<channel_name>]
3190
                    [--channel_account_id=<channel_account_id>...]
3191
                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]
3192
                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]
3193
                    [--preview] [--blocking]
3194

3195
        Options:
3196
            --name=<name>                  : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash))
3197
            --bid=<bid>                    : (decimal) amount to back the claim
3198
            --file_path=<file_path>        : (str) path to file to be associated with name.
3199
            --file_name=<file_name>        : (str) name of file to be associated with stream.
3200
            --file_hash=<file_hash>        : (str) hash of file to be associated with stream.
3201
            --validate_file                : (bool) validate that the video container and encodings match
3202
                                             common web browser support or that optimization succeeds if specified.
3203
                                             FFmpeg is required
3204
            --optimize_file                : (bool) transcode the video & audio if necessary to ensure
3205
                                             common web browser support. FFmpeg is required
3206
            --fee_currency=<fee_currency>  : (string) specify fee currency
3207
            --fee_amount=<fee_amount>      : (decimal) content download fee
3208
            --fee_address=<fee_address>    : (str) address where to send fee payments, will use
3209
                                                   value from --claim_address if not provided
3210
            --title=<title>                : (str) title of the publication
3211
            --description=<description>    : (str) description of the publication
3212
            --author=<author>              : (str) author of the publication. The usage for this field is not
3213
                                             the same as for channels. The author field is used to credit an author
3214
                                             who is not the publisher and is not represented by the channel. For
3215
                                             example, a pdf file of 'The Odyssey' has an author of 'Homer' but may
3216
                                             by published to a channel such as '@classics', or to no channel at all
3217
            --tags=<tags>                  : (list) add content tags
3218
            --languages=<languages>        : (list) languages used by the channel,
3219
                                                    using RFC 5646 format, eg:
3220
                                                    for English `--languages=en`
3221
                                                    for Spanish (Spain) `--languages=es-ES`
3222
                                                    for Spanish (Mexican) `--languages=es-MX`
3223
                                                    for Chinese (Simplified) `--languages=zh-Hans`
3224
                                                    for Chinese (Traditional) `--languages=zh-Hant`
3225
            --locations=<locations>        : (list) locations relevant to the stream, consisting of 2 letter
3226
                                                    `country` code and a `state`, `city` and a postal
3227
                                                    `code` along with a `latitude` and `longitude`.
3228
                                                    for JSON RPC: pass a dictionary with aforementioned
3229
                                                        attributes as keys, eg:
3230
                                                        ...
3231
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
3232
                                                        ...
3233
                                                    for command line: pass a colon delimited list
3234
                                                        with values in the following order:
3235

3236
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
3237

3238
                                                        making sure to include colon for blank values, for
3239
                                                        example to provide only the city:
3240

3241
                                                          ... --locations="::Manchester"
3242

3243
                                                        with all values set:
3244

3245
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
3246

3247
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
3248

3249
                                                          ... --locations="42.990605:-71.460989"
3250

3251
                                                        finally, you can also pass JSON string of dictionary
3252
                                                        on the command line as you would via JSON RPC
3253

3254
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
3255

3256
            --license=<license>            : (str) publication license
3257
            --license_url=<license_url>    : (str) publication license url
3258
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
3259
            --release_time=<release_time>  : (int) original public release of content, seconds since UNIX epoch
3260
            --width=<width>                : (int) image/video width, automatically calculated from media file
3261
            --height=<height>              : (int) image/video height, automatically calculated from media file
3262
            --duration=<duration>          : (int) audio/video duration in seconds, automatically calculated
3263
            --sd_hash=<sd_hash>            : (str) sd_hash of stream
3264
            --channel_id=<channel_id>      : (str) claim id of the publisher channel
3265
            --channel_name=<channel_name>  : (str) name of publisher channel
3266
          --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
3267
                                                   for channel certificates, defaults to all accounts.
3268
            --account_id=<account_id>      : (str) account to use for holding the transaction
3269
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
3270
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
3271
            --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified
3272
                                                   it will be determined automatically from the account
3273
            --preview                      : (bool) do not broadcast the transaction
3274
            --blocking                     : (bool) wait until transaction is in mempool
3275

3276
        Returns: {Transaction}
3277
        """
3278
        self.valid_stream_name_or_error(name)
×
3279
        wallet = self.wallet_manager.get_wallet_or_default(kwargs.get('wallet_id'))
×
3280
        if kwargs.get('account_id'):
×
3281
            accounts = [wallet.get_account_or_error(kwargs.get('account_id'))]
×
3282
        else:
3283
            accounts = wallet.accounts
×
3284
        claims = await self.ledger.get_claims(
×
3285
            wallet=wallet, accounts=accounts, claim_name=name
3286
        )
3287
        if len(claims) == 0:
×
3288
            if 'bid' not in kwargs:
×
3289
                # TODO: use error from lbry.error
3290
                raise Exception("'bid' is a required argument for new publishes.")
×
3291
            return await self.jsonrpc_stream_create(name, **kwargs)
×
3292
        elif len(claims) == 1:
×
3293
            assert claims[0].claim.is_stream, f"Claim at name '{name}' is not a stream claim."
×
3294
            return await self.jsonrpc_stream_update(claims[0].claim_id, replace=True, **kwargs)
×
3295
        # TODO: use error from lbry.error
3296
        raise Exception(
×
3297
            f"There are {len(claims)} claims for '{name}', please use 'stream update' command "
3298
            f"to update a specific stream claim."
3299
        )
3300

3301
    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)
1✔
3302
    async def jsonrpc_stream_repost(
1✔
3303
            self, name, bid, claim_id, allow_duplicate_name=False, channel_id=None,
3304
            channel_name=None, channel_account_id=None, account_id=None, wallet_id=None,
3305
            claim_address=None, funding_account_ids=None, preview=False, blocking=False, **kwargs):
3306
        """
3307
            Creates a claim that references an existing stream by its claim id.
3308

3309
            Usage:
3310
                stream_repost (<name> | --name=<name>) (<bid> | --bid=<bid>) (<claim_id> | --claim_id=<claim_id>)
3311
                        [--allow_duplicate_name=<allow_duplicate_name>]
3312
                        [--title=<title>] [--description=<description>] [--tags=<tags>...]
3313
                        [--channel_id=<channel_id> | --channel_name=<channel_name>]
3314
                        [--channel_account_id=<channel_account_id>...]
3315
                        [--account_id=<account_id>] [--wallet_id=<wallet_id>]
3316
                        [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]
3317
                        [--preview] [--blocking]
3318

3319
            Options:
3320
                --name=<name>                  : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash))
3321
                --bid=<bid>                    : (decimal) amount to back the claim
3322
                --claim_id=<claim_id>          : (str) id of the claim being reposted
3323
                --allow_duplicate_name=<allow_duplicate_name> : (bool) create new claim even if one already exists with
3324
                                                                       given name. default: false.
3325
                --title=<title>                : (str) title of the repost
3326
                --description=<description>    : (str) description of the repost
3327
                --tags=<tags>                  : (list) add repost tags
3328
                --channel_id=<channel_id>      : (str) claim id of the publisher channel
3329
                --channel_name=<channel_name>  : (str) name of the publisher channel
3330
                --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
3331
                                                                 for channel certificates, defaults to all accounts.
3332
                --account_id=<account_id>      : (str) account to use for holding the transaction
3333
                --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
3334
                --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
3335
                --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified
3336
                                                       it will be determined automatically from the account
3337
                --preview                      : (bool) do not broadcast the transaction
3338
                --blocking                     : (bool) wait until transaction is in mempool
3339

3340
            Returns: {Transaction}
3341
            """
3342
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3343
        self.valid_stream_name_or_error(name)
×
3344
        account = wallet.get_account_or_default(account_id)
×
3345
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
3346
        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)
×
3347
        amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
3348
        claim_address = await self.get_receiving_address(claim_address, account)
×
3349
        claims = await account.get_claims(claim_name=name)
×
3350
        if len(claims) > 0:
×
3351
            if not allow_duplicate_name:
×
3352
                # TODO: use error from lbry.error
3353
                raise Exception(
×
3354
                    f"You already have a stream claim published under the name '{name}'. "
3355
                    f"Use --allow-duplicate-name flag to override."
3356
                )
3357
        if not VALID_FULL_CLAIM_ID.fullmatch(claim_id):
×
3358
            # TODO: use error from lbry.error
3359
            raise Exception('Invalid claim id. It is expected to be a 40 characters long hexadecimal string.')
×
3360

3361
        claim = Claim()
×
3362
        claim.repost.update(**kwargs)
×
3363
        claim.repost.reference.claim_id = claim_id
×
3364
        tx = await Transaction.claim_create(
×
3365
            name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel
3366
        )
3367
        new_txo = tx.outputs[0]
×
3368

3369
        if channel:
×
3370
            new_txo.sign(channel)
×
3371
        await tx.sign(funding_accounts)
×
3372

3373
        if not preview:
×
3374
            await self.broadcast_or_release(tx, blocking)
×
3375
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))
×
3376
        else:
3377
            await account.ledger.release_tx(tx)
×
3378

3379
        return tx
×
3380

3381
    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)
1✔
3382
    async def jsonrpc_stream_create(
1✔
3383
            self, name, bid, file_path=None, allow_duplicate_name=False,
3384
            channel_id=None, channel_name=None, channel_account_id=None,
3385
            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,
3386
            preview=False, blocking=False, validate_file=False, optimize_file=False, **kwargs):
3387
        """
3388
        Make a new stream claim and announce the associated file to lbrynet.
3389

3390
        Usage:
3391
            stream_create (<name> | --name=<name>) (<bid> | --bid=<bid>) [<file_path> | --file_path=<file_path>]
3392
                    [--file_name=<file_name>] [--file_hash=<file_hash>] [--validate_file] [--optimize_file]
3393
                    [--allow_duplicate_name=<allow_duplicate_name>]
3394
                    [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>] [--fee_address=<fee_address>]
3395
                    [--title=<title>] [--description=<description>] [--author=<author>]
3396
                    [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
3397
                    [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
3398
                    [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]
3399
                    [--sd_hash=<sd_hash>] [--channel_id=<channel_id> | --channel_name=<channel_name>]
3400
                    [--channel_account_id=<channel_account_id>...]
3401
                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]
3402
                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]
3403
                    [--preview] [--blocking]
3404

3405
        Options:
3406
            --name=<name>                  : (str) name of the content (can only consist of a-z A-Z 0-9 and -(dash))
3407
            --bid=<bid>                    : (decimal) amount to back the claim
3408
            --file_path=<file_path>        : (str) path to file to be associated with name.
3409
            --file_name=<file_name>        : (str) name of file to be associated with stream.
3410
            --file_hash=<file_hash>        : (str) hash of file to be associated with stream.
3411
            --validate_file                : (bool) validate that the video container and encodings match
3412
                                             common web browser support or that optimization succeeds if specified.
3413
                                             FFmpeg is required
3414
            --optimize_file                : (bool) transcode the video & audio if necessary to ensure
3415
                                             common web browser support. FFmpeg is required
3416
        --allow_duplicate_name=<allow_duplicate_name> : (bool) create new claim even if one already exists with
3417
                                              given name. default: false.
3418
            --fee_currency=<fee_currency>  : (string) specify fee currency
3419
            --fee_amount=<fee_amount>      : (decimal) content download fee
3420
            --fee_address=<fee_address>    : (str) address where to send fee payments, will use
3421
                                                   value from --claim_address if not provided
3422
            --title=<title>                : (str) title of the publication
3423
            --description=<description>    : (str) description of the publication
3424
            --author=<author>              : (str) author of the publication. The usage for this field is not
3425
                                             the same as for channels. The author field is used to credit an author
3426
                                             who is not the publisher and is not represented by the channel. For
3427
                                             example, a pdf file of 'The Odyssey' has an author of 'Homer' but may
3428
                                             by published to a channel such as '@classics', or to no channel at all
3429
            --tags=<tags>                  : (list) add content tags
3430
            --languages=<languages>        : (list) languages used by the channel,
3431
                                                    using RFC 5646 format, eg:
3432
                                                    for English `--languages=en`
3433
                                                    for Spanish (Spain) `--languages=es-ES`
3434
                                                    for Spanish (Mexican) `--languages=es-MX`
3435
                                                    for Chinese (Simplified) `--languages=zh-Hans`
3436
                                                    for Chinese (Traditional) `--languages=zh-Hant`
3437
            --locations=<locations>        : (list) locations relevant to the stream, consisting of 2 letter
3438
                                                    `country` code and a `state`, `city` and a postal
3439
                                                    `code` along with a `latitude` and `longitude`.
3440
                                                    for JSON RPC: pass a dictionary with aforementioned
3441
                                                        attributes as keys, eg:
3442
                                                        ...
3443
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
3444
                                                        ...
3445
                                                    for command line: pass a colon delimited list
3446
                                                        with values in the following order:
3447

3448
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
3449

3450
                                                        making sure to include colon for blank values, for
3451
                                                        example to provide only the city:
3452

3453
                                                          ... --locations="::Manchester"
3454

3455
                                                        with all values set:
3456

3457
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
3458

3459
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
3460

3461
                                                          ... --locations="42.990605:-71.460989"
3462

3463
                                                        finally, you can also pass JSON string of dictionary
3464
                                                        on the command line as you would via JSON RPC
3465

3466
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
3467

3468
            --license=<license>            : (str) publication license
3469
            --license_url=<license_url>    : (str) publication license url
3470
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
3471
            --release_time=<release_time>  : (int) original public release of content, seconds since UNIX epoch
3472
            --width=<width>                : (int) image/video width, automatically calculated from media file
3473
            --height=<height>              : (int) image/video height, automatically calculated from media file
3474
            --duration=<duration>          : (int) audio/video duration in seconds, automatically calculated
3475
            --sd_hash=<sd_hash>            : (str) sd_hash of stream
3476
            --channel_id=<channel_id>      : (str) claim id of the publisher channel
3477
            --channel_name=<channel_name>  : (str) name of the publisher channel
3478
            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
3479
                                                   for channel certificates, defaults to all accounts.
3480
            --account_id=<account_id>      : (str) account to use for holding the transaction
3481
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
3482
            --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
3483
            --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified
3484
                                                   it will be determined automatically from the account
3485
            --preview                      : (bool) do not broadcast the transaction
3486
            --blocking                     : (bool) wait until transaction is in mempool
3487

3488
        Returns: {Transaction}
3489
        """
3490
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3491
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
3492
        self.valid_stream_name_or_error(name)
×
3493
        account = wallet.get_account_or_default(account_id)
×
3494
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
3495
        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)
×
3496
        amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
3497
        claim_address = await self.get_receiving_address(claim_address, account)
×
3498
        kwargs['fee_address'] = self.get_fee_address(kwargs, claim_address)
×
3499

3500
        claims = await account.get_claims(claim_name=name)
×
3501
        if len(claims) > 0:
×
3502
            if not allow_duplicate_name:
×
3503
                # TODO: use error from lbry.error
3504
                raise Exception(
×
3505
                    f"You already have a stream claim published under the name '{name}'. "
3506
                    f"Use --allow-duplicate-name flag to override."
3507
                )
3508

3509
        if file_path is not None:
×
3510
            file_path, spec = await self._video_file_analyzer.verify_or_repair(
×
3511
                validate_file, optimize_file, file_path, ignore_non_video=True
3512
            )
3513
            kwargs.update(spec)
×
3514

3515
        claim = Claim()
×
3516
        if file_path is not None:
×
3517
            claim.stream.update(file_path=file_path, sd_hash='0' * 96, **kwargs)
×
3518
        else:
3519
            claim.stream.update(**kwargs)
×
3520
        tx = await Transaction.claim_create(
×
3521
            name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel
3522
        )
3523
        new_txo = tx.outputs[0]
×
3524

3525
        file_stream = None
×
3526
        if not preview and file_path is not None:
×
3527
            file_stream = await self.file_manager.create_stream(file_path)
×
3528
            claim.stream.source.sd_hash = file_stream.sd_hash
×
3529
            new_txo.script.generate()
×
3530

3531
        if channel:
×
3532
            new_txo.sign(channel)
×
3533
        await tx.sign(funding_accounts)
×
3534

3535
        if not preview:
×
3536
            await self.broadcast_or_release(tx, blocking)
×
3537

3538
            async def save_claims():
×
3539
                await self.storage.save_claims([self._old_get_temp_claim_info(
×
3540
                    tx, new_txo, claim_address, claim, name
3541
                )])
3542
                if file_path is not None:
×
3543
                    await self.storage.save_content_claim(file_stream.stream_hash, new_txo.id)
×
3544

3545
            self.component_manager.loop.create_task(save_claims())
×
3546
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))
×
3547
        else:
3548
            await account.ledger.release_tx(tx)
×
3549

3550
        return tx
×
3551

3552
    @requires(WALLET_COMPONENT, FILE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT)
1✔
3553
    async def jsonrpc_stream_update(
1✔
3554
            self, claim_id, bid=None, file_path=None,
3555
            channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False,
3556
            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,
3557
            preview=False, blocking=False, replace=False, validate_file=False, optimize_file=False, **kwargs):
3558
        """
3559
        Update an existing stream claim and if a new file is provided announce it to lbrynet.
3560

3561
        Usage:
3562
            stream_update (<claim_id> | --claim_id=<claim_id>) [--bid=<bid>] [--file_path=<file_path>]
3563
                    [--validate_file] [--optimize_file]
3564
                    [--file_name=<file_name>] [--file_size=<file_size>] [--file_hash=<file_hash>]
3565
                    [--fee_currency=<fee_currency>] [--fee_amount=<fee_amount>]
3566
                    [--fee_address=<fee_address>] [--clear_fee]
3567
                    [--title=<title>] [--description=<description>] [--author=<author>]
3568
                    [--tags=<tags>...] [--clear_tags]
3569
                    [--languages=<languages>...] [--clear_languages]
3570
                    [--locations=<locations>...] [--clear_locations]
3571
                    [--license=<license>] [--license_url=<license_url>] [--thumbnail_url=<thumbnail_url>]
3572
                    [--release_time=<release_time>] [--width=<width>] [--height=<height>] [--duration=<duration>]
3573
                    [--sd_hash=<sd_hash>] [--channel_id=<channel_id> | --channel_name=<channel_name> | --clear_channel]
3574
                    [--channel_account_id=<channel_account_id>...]
3575
                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]
3576
                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]
3577
                    [--preview] [--blocking] [--replace]
3578

3579
        Options:
3580
            --claim_id=<claim_id>          : (str) id of the stream claim to update
3581
            --bid=<bid>                    : (decimal) amount to back the claim
3582
            --file_path=<file_path>        : (str) path to file to be associated with name.
3583
            --validate_file                : (bool) validate that the video container and encodings match
3584
                                             common web browser support or that optimization succeeds if specified.
3585
                                             FFmpeg is required and file_path must be specified.
3586
            --optimize_file                : (bool) transcode the video & audio if necessary to ensure common
3587
                                             web browser support. FFmpeg is required and file_path must be specified.
3588
            --file_name=<file_name>        : (str) override file name, defaults to name from file_path.
3589
            --file_size=<file_size>        : (str) override file size, otherwise automatically computed.
3590
            --file_hash=<file_hash>        : (str) override file hash, otherwise automatically computed.
3591
            --fee_currency=<fee_currency>  : (string) specify fee currency
3592
            --fee_amount=<fee_amount>      : (decimal) content download fee
3593
            --fee_address=<fee_address>    : (str) address where to send fee payments, will use
3594
                                                   value from --claim_address if not provided
3595
            --clear_fee                    : (bool) clear previously set fee
3596
            --title=<title>                : (str) title of the publication
3597
            --description=<description>    : (str) description of the publication
3598
            --author=<author>              : (str) author of the publication. The usage for this field is not
3599
                                             the same as for channels. The author field is used to credit an author
3600
                                             who is not the publisher and is not represented by the channel. For
3601
                                             example, a pdf file of 'The Odyssey' has an author of 'Homer' but may
3602
                                             by published to a channel such as '@classics', or to no channel at all
3603
            --tags=<tags>                  : (list) add content tags
3604
            --clear_tags                   : (bool) clear existing tags (prior to adding new ones)
3605
            --languages=<languages>        : (list) languages used by the channel,
3606
                                                    using RFC 5646 format, eg:
3607
                                                    for English `--languages=en`
3608
                                                    for Spanish (Spain) `--languages=es-ES`
3609
                                                    for Spanish (Mexican) `--languages=es-MX`
3610
                                                    for Chinese (Simplified) `--languages=zh-Hans`
3611
                                                    for Chinese (Traditional) `--languages=zh-Hant`
3612
            --clear_languages              : (bool) clear existing languages (prior to adding new ones)
3613
            --locations=<locations>        : (list) locations relevant to the stream, consisting of 2 letter
3614
                                                    `country` code and a `state`, `city` and a postal
3615
                                                    `code` along with a `latitude` and `longitude`.
3616
                                                    for JSON RPC: pass a dictionary with aforementioned
3617
                                                        attributes as keys, eg:
3618
                                                        ...
3619
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
3620
                                                        ...
3621
                                                    for command line: pass a colon delimited list
3622
                                                        with values in the following order:
3623

3624
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
3625

3626
                                                        making sure to include colon for blank values, for
3627
                                                        example to provide only the city:
3628

3629
                                                          ... --locations="::Manchester"
3630

3631
                                                        with all values set:
3632

3633
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
3634

3635
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
3636

3637
                                                          ... --locations="42.990605:-71.460989"
3638

3639
                                                        finally, you can also pass JSON string of dictionary
3640
                                                        on the command line as you would via JSON RPC
3641

3642
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
3643

3644
            --clear_locations              : (bool) clear existing locations (prior to adding new ones)
3645
            --license=<license>            : (str) publication license
3646
            --license_url=<license_url>    : (str) publication license url
3647
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
3648
            --release_time=<release_time>  : (int) original public release of content, seconds since UNIX epoch
3649
            --width=<width>                : (int) image/video width, automatically calculated from media file
3650
            --height=<height>              : (int) image/video height, automatically calculated from media file
3651
            --duration=<duration>          : (int) audio/video duration in seconds, automatically calculated
3652
            --sd_hash=<sd_hash>            : (str) sd_hash of stream
3653
            --channel_id=<channel_id>      : (str) claim id of the publisher channel
3654
            --channel_name=<channel_name>  : (str) name of the publisher channel
3655
            --clear_channel                : (bool) remove channel signature
3656
          --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
3657
                                                   for channel certificates, defaults to all accounts.
3658
            --account_id=<account_id>      : (str) account in which to look for stream (default: all)
3659
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
3660
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
3661
            --claim_address=<claim_address>: (str) address where the claim is sent to, if not specified
3662
                                                   it will be determined automatically from the account
3663
            --preview                      : (bool) do not broadcast the transaction
3664
            --blocking                     : (bool) wait until transaction is in mempool
3665
            --replace                      : (bool) instead of modifying specific values on
3666
                                                    the stream, this will clear all existing values
3667
                                                    and only save passed in values, useful for form
3668
                                                    submissions where all values are always set
3669

3670
        Returns: {Transaction}
3671
        """
3672
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3673
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
3674
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
3675
        if account_id:
×
3676
            account = wallet.get_account_or_error(account_id)
×
3677
            accounts = [account]
×
3678
        else:
3679
            account = wallet.default_account
×
3680
            accounts = wallet.accounts
×
3681

3682
        existing_claims = await self.ledger.get_claims(
×
3683
            wallet=wallet, accounts=accounts, claim_id=claim_id
3684
        )
3685
        if len(existing_claims) != 1:
×
3686
            account_ids = ', '.join(f"'{account.id}'" for account in accounts)
×
3687
            raise InputValueError(
×
3688
                f"Can't find the stream '{claim_id}' in account(s) {account_ids}."
3689
            )
3690

3691
        old_txo = existing_claims[0]
×
3692
        if not old_txo.claim.is_stream and not old_txo.claim.is_repost:
×
3693
            # in principle it should work with any type of claim, but its safer to
3694
            # limit it to ones we know won't be broken. in the future we can expand
3695
            # this if we have a test case for e.g. channel or support claims
3696
            raise InputValueError(
×
3697
                f"A claim with id '{claim_id}' was found but it is not a stream or repost claim."
3698
            )
3699

3700
        if bid is not None:
×
3701
            amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
3702
        else:
3703
            amount = old_txo.amount
×
3704

3705
        if claim_address is not None:
×
3706
            self.valid_address_or_error(claim_address)
×
3707
        else:
3708
            claim_address = old_txo.get_address(account.ledger)
×
3709

3710
        channel = None
×
3711
        if not clear_channel and (channel_id or channel_name):
×
3712
            channel = await self.get_channel_or_error(
×
3713
                wallet, channel_account_id, channel_id, channel_name, for_signing=True)
3714
        elif old_txo.claim.is_signed and not clear_channel and not replace:
×
3715
            channel = old_txo.channel
×
3716

3717
        fee_address = self.get_fee_address(kwargs, claim_address)
×
3718
        if fee_address:
×
3719
            kwargs['fee_address'] = fee_address
×
3720

3721
        file_path, spec = await self._video_file_analyzer.verify_or_repair(
×
3722
            validate_file, optimize_file, file_path, ignore_non_video=True
3723
        )
3724
        kwargs.update(spec)
×
3725

3726
        if replace:
×
3727
            claim = Claim()
×
3728
            if old_txo.claim.is_stream:
×
3729
                if old_txo.claim.stream.has_source:
×
3730
                    claim.stream.message.source.CopyFrom(
×
3731
                        old_txo.claim.stream.message.source
3732
                    )
3733
                stream_type = old_txo.claim.stream.stream_type
×
3734
                if stream_type:
×
3735
                    old_stream_type = getattr(old_txo.claim.stream.message, stream_type)
×
3736
                    new_stream_type = getattr(claim.stream.message, stream_type)
×
3737
                    new_stream_type.CopyFrom(old_stream_type)
×
3738
        else:
3739
            claim = Claim.from_bytes(old_txo.claim.to_bytes())
×
3740

3741
        if old_txo.claim.is_stream:
×
3742
            claim.stream.update(file_path=file_path, **kwargs)
×
3743
        elif old_txo.claim.is_repost:
×
3744
            claim.repost.update(**kwargs)
×
3745

3746
        if clear_channel:
×
3747
            claim.clear_signature()
×
3748
        tx = await Transaction.claim_update(
×
3749
            old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0],
3750
            channel if not clear_channel else None
3751
        )
3752

3753
        new_txo = tx.outputs[0]
×
3754
        stream_hash = None
×
3755
        if not preview and old_txo.claim.is_stream:
×
3756
            old_stream = self.file_manager.get_filtered(sd_hash=old_txo.claim.stream.source.sd_hash)
×
3757
            old_stream = old_stream[0] if old_stream else None
×
3758
            if file_path is not None:
×
3759
                if old_stream:
×
3760
                    await self.file_manager.delete(old_stream, delete_file=False)
×
3761
                file_stream = await self.file_manager.create_stream(file_path)
×
3762
                new_txo.claim.stream.source.sd_hash = file_stream.sd_hash
×
3763
                new_txo.script.generate()
×
3764
                stream_hash = file_stream.stream_hash
×
3765
            elif old_stream:
×
3766
                stream_hash = old_stream.stream_hash
×
3767

3768
        if channel:
×
3769
            new_txo.sign(channel)
×
3770
        await tx.sign(funding_accounts)
×
3771

3772
        if not preview:
×
3773
            await self.broadcast_or_release(tx, blocking)
×
3774

3775
            async def save_claims():
×
3776
                await self.storage.save_claims([self._old_get_temp_claim_info(
×
3777
                    tx, new_txo, claim_address, new_txo.claim, new_txo.claim_name
3778
                )])
3779
                if stream_hash:
×
3780
                    await self.storage.save_content_claim(stream_hash, new_txo.id)
×
3781

3782
            self.component_manager.loop.create_task(save_claims())
×
3783
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))
×
3784
        else:
3785
            await account.ledger.release_tx(tx)
×
3786

3787
        return tx
×
3788

3789
    @requires(WALLET_COMPONENT)
1✔
3790
    async def jsonrpc_stream_abandon(
1✔
3791
            self, claim_id=None, txid=None, nout=None, account_id=None, wallet_id=None,
3792
            preview=False, blocking=False):
3793
        """
3794
        Abandon one of my stream claims.
3795

3796
        Usage:
3797
            stream_abandon [<claim_id> | --claim_id=<claim_id>]
3798
                           [<txid> | --txid=<txid>] [<nout> | --nout=<nout>]
3799
                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]
3800
                           [--preview] [--blocking]
3801

3802
        Options:
3803
            --claim_id=<claim_id>     : (str) claim_id of the claim to abandon
3804
            --txid=<txid>             : (str) txid of the claim to abandon
3805
            --nout=<nout>             : (int) nout of the claim to abandon
3806
            --account_id=<account_id> : (str) id of the account to use
3807
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
3808
            --preview                 : (bool) do not broadcast the transaction
3809
            --blocking                : (bool) wait until abandon is in mempool
3810

3811
        Returns: {Transaction}
3812
        """
3813
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3814
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
3815
        if account_id:
×
3816
            account = wallet.get_account_or_error(account_id)
×
3817
            accounts = [account]
×
3818
        else:
3819
            account = wallet.default_account
×
3820
            accounts = wallet.accounts
×
3821

3822
        if txid is not None and nout is not None:
×
3823
            claims = await self.ledger.get_claims(
×
3824
                wallet=wallet, accounts=accounts, **{'txo.txid': txid, 'txo.position': nout}
3825
            )
3826
        elif claim_id is not None:
×
3827
            claims = await self.ledger.get_claims(
×
3828
                wallet=wallet, accounts=accounts, claim_id=claim_id
3829
            )
3830
        else:
3831
            # TODO: use error from lbry.error
3832
            raise Exception('Must specify claim_id, or txid and nout')
×
3833

3834
        if not claims:
×
3835
            # TODO: use error from lbry.error
3836
            raise Exception('No claim found for the specified claim_id or txid:nout')
×
3837

3838
        tx = await Transaction.create(
×
3839
            [Input.spend(txo) for txo in claims], [], accounts, account
3840
        )
3841

3842
        if not preview:
×
3843
            await self.broadcast_or_release(tx, blocking)
×
3844
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('abandon'))
×
3845
        else:
3846
            await self.ledger.release_tx(tx)
×
3847

3848
        return tx
×
3849

3850
    @requires(WALLET_COMPONENT)
1✔
3851
    def jsonrpc_stream_list(self, *args, **kwargs):
1✔
3852
        """
3853
        List my stream claims.
3854

3855
        Usage:
3856
            stream_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
3857
                        [--name=<name>...] [--claim_id=<claim_id>...] [--is_spent]
3858
                        [--page=<page>] [--page_size=<page_size>] [--resolve] [--no_totals]
3859

3860
        Options:
3861
            --name=<name>              : (str or list) stream name
3862
            --claim_id=<claim_id>      : (str or list) stream id
3863
            --is_spent                 : (bool) shows previous stream updates and abandons
3864
            --account_id=<account_id>  : (str) id of the account to query
3865
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
3866
            --page=<page>              : (int) page to return during paginating
3867
            --page_size=<page_size>    : (int) number of items on page during pagination
3868
            --resolve                  : (bool) resolves each stream to provide additional metadata
3869
            --no_totals                : (bool) do not calculate the total number of pages and items in result set
3870
                                                (significant performance boost)
3871

3872
        Returns: {Paginated[Output]}
3873
        """
3874
        kwargs['type'] = 'stream'
×
3875
        if 'is_spent' not in kwargs:
×
3876
            kwargs['is_not_spent'] = True
×
3877
        return self.jsonrpc_txo_list(*args, **kwargs)
×
3878

3879
    @requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT,
1✔
3880
              DHT_COMPONENT, DATABASE_COMPONENT)
3881
    def jsonrpc_stream_cost_estimate(self, uri):
1✔
3882
        """
3883
        Get estimated cost for a lbry stream
3884

3885
        Usage:
3886
            stream_cost_estimate (<uri> | --uri=<uri>)
3887

3888
        Options:
3889
            --uri=<uri>      : (str) uri to use
3890

3891
        Returns:
3892
            (float) Estimated cost in lbry credits, returns None if uri is not
3893
                resolvable
3894
        """
3895
        return self.get_est_cost_from_uri(uri)
×
3896

3897
    COLLECTION_DOC = """
1✔
3898
    Create, update, list, resolve, and abandon collections.
3899
    """
3900

3901
    @requires(WALLET_COMPONENT)
1✔
3902
    async def jsonrpc_collection_create(
1✔
3903
            self, name, bid, claims, allow_duplicate_name=False,
3904
            channel_id=None, channel_name=None, channel_account_id=None,
3905
            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,
3906
            preview=False, blocking=False, **kwargs):
3907
        """
3908
        Create a new collection.
3909

3910
        Usage:
3911
            collection_create (<name> | --name=<name>) (<bid> | --bid=<bid>)
3912
                    (--claims=<claims>...)
3913
                    [--allow_duplicate_name]
3914
                    [--title=<title>] [--description=<description>]
3915
                    [--tags=<tags>...] [--languages=<languages>...] [--locations=<locations>...]
3916
                    [--thumbnail_url=<thumbnail_url>]
3917
                    [--channel_id=<channel_id> | --channel_name=<channel_name>]
3918
                    [--channel_account_id=<channel_account_id>...]
3919
                    [--account_id=<account_id>] [--wallet_id=<wallet_id>]
3920
                    [--claim_address=<claim_address>] [--funding_account_ids=<funding_account_ids>...]
3921
                    [--preview] [--blocking]
3922

3923
        Options:
3924
            --name=<name>                  : (str) name of the collection
3925
            --bid=<bid>                    : (decimal) amount to back the claim
3926
            --claims=<claims>              : (list) claim ids to be included in the collection
3927
            --allow_duplicate_name         : (bool) create new collection even if one already exists with
3928
                                                    given name. default: false.
3929
            --title=<title>                : (str) title of the collection
3930
            --description=<description>    : (str) description of the collection
3931
            --tags=<tags>                  : (list) content tags
3932
            --clear_languages              : (bool) clear existing languages (prior to adding new ones)
3933
            --languages=<languages>        : (list) languages used by the collection,
3934
                                                    using RFC 5646 format, eg:
3935
                                                    for English `--languages=en`
3936
                                                    for Spanish (Spain) `--languages=es-ES`
3937
                                                    for Spanish (Mexican) `--languages=es-MX`
3938
                                                    for Chinese (Simplified) `--languages=zh-Hans`
3939
                                                    for Chinese (Traditional) `--languages=zh-Hant`
3940
            --locations=<locations>        : (list) locations of the collection, consisting of 2 letter
3941
                                                    `country` code and a `state`, `city` and a postal
3942
                                                    `code` along with a `latitude` and `longitude`.
3943
                                                    for JSON RPC: pass a dictionary with aforementioned
3944
                                                        attributes as keys, eg:
3945
                                                        ...
3946
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
3947
                                                        ...
3948
                                                    for command line: pass a colon delimited list
3949
                                                        with values in the following order:
3950

3951
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
3952

3953
                                                        making sure to include colon for blank values, for
3954
                                                        example to provide only the city:
3955

3956
                                                          ... --locations="::Manchester"
3957

3958
                                                        with all values set:
3959

3960
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
3961

3962
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
3963

3964
                                                          ... --locations="42.990605:-71.460989"
3965

3966
                                                        finally, you can also pass JSON string of dictionary
3967
                                                        on the command line as you would via JSON RPC
3968

3969
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
3970

3971
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
3972
            --channel_id=<channel_id>      : (str) claim id of the publisher channel
3973
            --channel_name=<channel_name>  : (str) name of the publisher channel
3974
            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
3975
                                                   for channel certificates, defaults to all accounts.
3976
            --account_id=<account_id>      : (str) account to use for holding the transaction
3977
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
3978
            --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
3979
            --claim_address=<claim_address>: (str) address where the collection is sent to, if not specified
3980
                                                   it will be determined automatically from the account
3981
            --preview                      : (bool) do not broadcast the transaction
3982
            --blocking                     : (bool) wait until transaction is in mempool
3983

3984
        Returns: {Transaction}
3985
        """
3986
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
3987
        account = wallet.get_account_or_default(account_id)
×
3988
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
3989
        self.valid_collection_name_or_error(name)
×
3990
        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)
×
3991
        amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
3992
        claim_address = await self.get_receiving_address(claim_address, account)
×
3993

3994
        existing_collections = await self.ledger.get_collections(accounts=wallet.accounts, claim_name=name)
×
3995
        if len(existing_collections) > 0:
×
3996
            if not allow_duplicate_name:
×
3997
                # TODO: use error from lbry.error
3998
                raise Exception(
×
3999
                    f"You already have a collection under the name '{name}'. "
4000
                    f"Use --allow-duplicate-name flag to override."
4001
                )
4002

4003
        claim = Claim()
×
4004
        claim.collection.update(claims=claims, **kwargs)
×
4005
        tx = await Transaction.claim_create(
×
4006
            name, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel
4007
        )
4008
        new_txo = tx.outputs[0]
×
4009

4010
        if channel:
×
4011
            new_txo.sign(channel)
×
4012
        await tx.sign(funding_accounts)
×
4013

4014
        if not preview:
×
4015
            await self.broadcast_or_release(tx, blocking)
×
4016
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))
×
4017
        else:
4018
            await account.ledger.release_tx(tx)
×
4019

4020
        return tx
×
4021

4022
    @requires(WALLET_COMPONENT)
1✔
4023
    async def jsonrpc_collection_update(
1✔
4024
            self, claim_id, bid=None,
4025
            channel_id=None, channel_name=None, channel_account_id=None, clear_channel=False,
4026
            account_id=None, wallet_id=None, claim_address=None, funding_account_ids=None,
4027
            preview=False, blocking=False, replace=False, **kwargs):
4028
        """
4029
        Update an existing collection claim.
4030

4031
        Usage:
4032
            collection_update (<claim_id> | --claim_id=<claim_id>) [--bid=<bid>]
4033
                            [--claims=<claims>...] [--clear_claims]
4034
                           [--title=<title>] [--description=<description>]
4035
                           [--tags=<tags>...] [--clear_tags]
4036
                           [--languages=<languages>...] [--clear_languages]
4037
                           [--locations=<locations>...] [--clear_locations]
4038
                           [--thumbnail_url=<thumbnail_url>] [--cover_url=<cover_url>]
4039
                           [--channel_id=<channel_id> | --channel_name=<channel_name>]
4040
                           [--channel_account_id=<channel_account_id>...]
4041
                           [--account_id=<account_id>] [--wallet_id=<wallet_id>]
4042
                           [--claim_address=<claim_address>]
4043
                           [--funding_account_ids=<funding_account_ids>...]
4044
                           [--preview] [--blocking] [--replace]
4045

4046
        Options:
4047
            --claim_id=<claim_id>          : (str) claim_id of the collection to update
4048
            --bid=<bid>                    : (decimal) amount to back the claim
4049
            --claims=<claims>              : (list) claim ids
4050
            --clear_claims                 : (bool) clear existing claim references (prior to adding new ones)
4051
            --title=<title>                : (str) title of the collection
4052
            --description=<description>    : (str) description of the collection
4053
            --tags=<tags>                  : (list) add content tags
4054
            --clear_tags                   : (bool) clear existing tags (prior to adding new ones)
4055
            --languages=<languages>        : (list) languages used by the collection,
4056
                                                    using RFC 5646 format, eg:
4057
                                                    for English `--languages=en`
4058
                                                    for Spanish (Spain) `--languages=es-ES`
4059
                                                    for Spanish (Mexican) `--languages=es-MX`
4060
                                                    for Chinese (Simplified) `--languages=zh-Hans`
4061
                                                    for Chinese (Traditional) `--languages=zh-Hant`
4062
            --clear_languages              : (bool) clear existing languages (prior to adding new ones)
4063
            --locations=<locations>        : (list) locations of the collection, consisting of 2 letter
4064
                                                    `country` code and a `state`, `city` and a postal
4065
                                                    `code` along with a `latitude` and `longitude`.
4066
                                                    for JSON RPC: pass a dictionary with aforementioned
4067
                                                        attributes as keys, eg:
4068
                                                        ...
4069
                                                        "locations": [{'country': 'US', 'state': 'NH'}]
4070
                                                        ...
4071
                                                    for command line: pass a colon delimited list
4072
                                                        with values in the following order:
4073

4074
                                                          "COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE"
4075

4076
                                                        making sure to include colon for blank values, for
4077
                                                        example to provide only the city:
4078

4079
                                                          ... --locations="::Manchester"
4080

4081
                                                        with all values set:
4082

4083
                                                 ... --locations="US:NH:Manchester:03101:42.990605:-71.460989"
4084

4085
                                                        optionally, you can just pass the "LATITUDE:LONGITUDE":
4086

4087
                                                          ... --locations="42.990605:-71.460989"
4088

4089
                                                        finally, you can also pass JSON string of dictionary
4090
                                                        on the command line as you would via JSON RPC
4091

4092
                                                          ... --locations="{'country': 'US', 'state': 'NH'}"
4093

4094
            --clear_locations              : (bool) clear existing locations (prior to adding new ones)
4095
            --thumbnail_url=<thumbnail_url>: (str) thumbnail url
4096
            --channel_id=<channel_id>      : (str) claim id of the publisher channel
4097
            --channel_name=<channel_name>  : (str) name of the publisher channel
4098
            --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
4099
                                                   for channel certificates, defaults to all accounts.
4100
            --account_id=<account_id>      : (str) account in which to look for collection (default: all)
4101
            --wallet_id=<wallet_id>        : (str) restrict operation to specific wallet
4102
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
4103
            --claim_address=<claim_address>: (str) address where the collection is sent
4104
            --preview                      : (bool) do not broadcast the transaction
4105
            --blocking                     : (bool) wait until transaction is in mempool
4106
            --replace                      : (bool) instead of modifying specific values on
4107
                                                    the collection, this will clear all existing values
4108
                                                    and only save passed in values, useful for form
4109
                                                    submissions where all values are always set
4110

4111
        Returns: {Transaction}
4112
        """
4113
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4114
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
4115
        if account_id:
×
4116
            account = wallet.get_account_or_error(account_id)
×
4117
            accounts = [account]
×
4118
        else:
4119
            account = wallet.default_account
×
4120
            accounts = wallet.accounts
×
4121

4122
        existing_collections = await self.ledger.get_collections(
×
4123
            wallet=wallet, accounts=accounts, claim_id=claim_id
4124
        )
4125
        if len(existing_collections) != 1:
×
4126
            account_ids = ', '.join(f"'{account.id}'" for account in accounts)
×
4127
            # TODO: use error from lbry.error
4128
            raise Exception(
×
4129
                f"Can't find the collection '{claim_id}' in account(s) {account_ids}."
4130
            )
4131
        old_txo = existing_collections[0]
×
4132
        if not old_txo.claim.is_collection:
×
4133
            # TODO: use error from lbry.error
4134
            raise Exception(
×
4135
                f"A claim with id '{claim_id}' was found but it is not a collection."
4136
            )
4137

4138
        if bid is not None:
×
4139
            amount = self.get_dewies_or_error('bid', bid, positive_value=True)
×
4140
        else:
4141
            amount = old_txo.amount
×
4142

4143
        if claim_address is not None:
×
4144
            self.valid_address_or_error(claim_address)
×
4145
        else:
4146
            claim_address = old_txo.get_address(account.ledger)
×
4147

4148
        channel = None
×
4149
        if channel_id or channel_name:
×
4150
            channel = await self.get_channel_or_error(
×
4151
                wallet, channel_account_id, channel_id, channel_name, for_signing=True)
4152
        elif old_txo.claim.is_signed and not clear_channel and not replace:
×
4153
            channel = old_txo.channel
×
4154

4155
        if replace:
×
4156
            claim = Claim()
×
4157
            claim.collection.update(**kwargs)
×
4158
        else:
4159
            claim = Claim.from_bytes(old_txo.claim.to_bytes())
×
4160
            claim.collection.update(**kwargs)
×
4161
        tx = await Transaction.claim_update(
×
4162
            old_txo, claim, amount, claim_address, funding_accounts, funding_accounts[0], channel
4163
        )
4164
        new_txo = tx.outputs[0]
×
4165

4166
        if channel:
×
4167
            new_txo.sign(channel)
×
4168
        await tx.sign(funding_accounts)
×
4169

4170
        if not preview:
×
4171
            await self.broadcast_or_release(tx, blocking)
×
4172
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('publish'))
×
4173
        else:
4174
            await account.ledger.release_tx(tx)
×
4175

4176
        return tx
×
4177

4178
    @requires(WALLET_COMPONENT)
1✔
4179
    async def jsonrpc_collection_abandon(self, *args, **kwargs):
1✔
4180
        """
4181
        Abandon one of my collection claims.
4182

4183
        Usage:
4184
            collection_abandon [<claim_id> | --claim_id=<claim_id>]
4185
                            [<txid> | --txid=<txid>] [<nout> | --nout=<nout>]
4186
                            [--account_id=<account_id>] [--wallet_id=<wallet_id>]
4187
                            [--preview] [--blocking]
4188

4189
        Options:
4190
            --claim_id=<claim_id>     : (str) claim_id of the claim to abandon
4191
            --txid=<txid>             : (str) txid of the claim to abandon
4192
            --nout=<nout>             : (int) nout of the claim to abandon
4193
            --account_id=<account_id> : (str) id of the account to use
4194
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
4195
            --preview                 : (bool) do not broadcast the transaction
4196
            --blocking                : (bool) wait until abandon is in mempool
4197

4198
        Returns: {Transaction}
4199
        """
4200
        return await self.jsonrpc_stream_abandon(*args, **kwargs)
×
4201

4202
    @requires(WALLET_COMPONENT)
1✔
4203
    def jsonrpc_collection_list(
1✔
4204
            self, resolve_claims=0, resolve=False, account_id=None,
4205
            wallet_id=None, page=None, page_size=None):
4206
        """
4207
        List my collection claims.
4208

4209
        Usage:
4210
            collection_list [--resolve_claims=<resolve_claims>] [--resolve] [<account_id> | --account_id=<account_id>]
4211
                [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]
4212

4213
        Options:
4214
            --resolve                         : (bool) resolve collection claim
4215
            --resolve_claims=<resolve_claims> : (int) resolve every claim
4216
            --account_id=<account_id>         : (str) id of the account to use
4217
            --wallet_id=<wallet_id>           : (str) restrict results to specific wallet
4218
            --page=<page>                     : (int) page to return during paginating
4219
            --page_size=<page_size>           : (int) number of items on page during pagination
4220

4221
        Returns: {Paginated[Output]}
4222
        """
4223
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4224
        if account_id:
×
4225
            account = wallet.get_account_or_error(account_id)
×
4226
            collections = account.get_collections
×
4227
            collection_count = account.get_collection_count
×
4228
        else:
4229
            collections = partial(self.ledger.get_collections, wallet=wallet, accounts=wallet.accounts)
×
4230
            collection_count = partial(self.ledger.get_collection_count, wallet=wallet, accounts=wallet.accounts)
×
4231
        return paginate_rows(
×
4232
            collections, collection_count, page, page_size,
4233
            resolve=resolve, resolve_claims=resolve_claims
4234
        )
4235

4236
    async def jsonrpc_collection_resolve(
1✔
4237
            self, claim_id=None, url=None, wallet_id=None, page=1, page_size=DEFAULT_PAGE_SIZE):
4238
        """
4239
        Resolve claims in the collection.
4240

4241
        Usage:
4242
            collection_resolve (--claim_id=<claim_id> | --url=<url>)
4243
                [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]
4244

4245
        Options:
4246
            --claim_id=<claim_id>      : (str) claim id of the collection
4247
            --url=<url>                : (str) url of the collection
4248
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4249
            --page=<page>              : (int) page to return during paginating
4250
            --page_size=<page_size>    : (int) number of items on page during pagination
4251

4252
        Returns: {Paginated[Output]}
4253
        """
4254
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4255

4256
        if claim_id:
×
4257
            txo = await self.ledger.get_claim_by_claim_id(claim_id, wallet.accounts)
×
4258
            if not isinstance(txo, Output) or not txo.is_claim:
×
4259
                # TODO: use error from lbry.error
4260
                raise Exception(f"Could not find collection with claim_id '{claim_id}'.")
×
4261
        elif url:
×
4262
            txo = (await self.ledger.resolve(wallet.accounts, [url]))[url]
×
4263
            if not isinstance(txo, Output) or not txo.is_claim:
×
4264
                # TODO: use error from lbry.error
4265
                raise Exception(f"Could not find collection with url '{url}'.")
×
4266
        else:
4267
            # TODO: use error from lbry.error
4268
            raise Exception("Missing argument claim_id or url.")
×
4269

4270
        page_num, page_size = abs(page), min(abs(page_size), 50)
×
4271
        items = await self.ledger.resolve_collection(txo, page_size * (page_num - 1), page_size)
×
4272
        total_items = len(txo.claim.collection.claims.ids)
×
4273

4274
        return {
×
4275
            "items": items,
4276
            "total_pages": int((total_items + (page_size - 1)) / page_size),
4277
            "total_items": total_items,
4278
            "page_size": page_size,
4279
            "page": page
4280
        }
4281

4282
    SUPPORT_DOC = """
1✔
4283
    Create, list and abandon all types of supports.
4284
    """
4285

4286
    @requires(WALLET_COMPONENT)
1✔
4287
    async def jsonrpc_support_create(
1✔
4288
            self, claim_id, amount, tip=False,
4289
            channel_id=None, channel_name=None, channel_account_id=None,
4290
            account_id=None, wallet_id=None, funding_account_ids=None,
4291
            comment=None, preview=False, blocking=False):
4292
        """
4293
        Create a support or a tip for name claim.
4294

4295
        Usage:
4296
            support_create (<claim_id> | --claim_id=<claim_id>) (<amount> | --amount=<amount>)
4297
                           [--tip] [--account_id=<account_id>] [--wallet_id=<wallet_id>]
4298
                           [--channel_id=<channel_id> | --channel_name=<channel_name>]
4299
                           [--channel_account_id=<channel_account_id>...] [--comment=<comment>]
4300
                           [--preview] [--blocking] [--funding_account_ids=<funding_account_ids>...]
4301

4302
        Options:
4303
            --claim_id=<claim_id>         : (str) claim_id of the claim to support
4304
            --amount=<amount>             : (decimal) amount of support
4305
            --tip                         : (bool) send support to claim owner, default: false.
4306
            --channel_id=<channel_id>     : (str) claim id of the supporters identity channel
4307
            --channel_name=<channel_name> : (str) name of the supporters identity channel
4308
          --channel_account_id=<channel_account_id>: (str) one or more account ids for accounts to look in
4309
                                                   for channel certificates, defaults to all accounts.
4310
            --account_id=<account_id>     : (str) account to use for holding the transaction
4311
            --wallet_id=<wallet_id>       : (str) restrict operation to specific wallet
4312
          --funding_account_ids=<funding_account_ids>: (list) ids of accounts to fund this transaction
4313
            --comment=<comment>           : (str) add a comment to the support
4314
            --preview                     : (bool) do not broadcast the transaction
4315
            --blocking                    : (bool) wait until transaction is in mempool
4316

4317
        Returns: {Transaction}
4318
        """
4319
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4320
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
4321
        funding_accounts = wallet.get_accounts_or_all(funding_account_ids)
×
4322
        channel = await self.get_channel_or_none(wallet, channel_account_id, channel_id, channel_name, for_signing=True)
×
4323
        amount = self.get_dewies_or_error("amount", amount)
×
4324
        claim = await self.ledger.get_claim_by_claim_id(claim_id)
×
4325
        claim_address = claim.get_address(self.ledger)
×
4326
        if not tip:
×
4327
            account = wallet.get_account_or_default(account_id)
×
4328
            claim_address = await account.receiving.get_or_create_usable_address()
×
4329

4330
        tx = await Transaction.support(
×
4331
            claim.claim_name, claim_id, amount, claim_address, funding_accounts, funding_accounts[0], channel,
4332
            comment=comment
4333
        )
4334
        new_txo = tx.outputs[0]
×
4335

4336
        if channel:
×
4337
            new_txo.sign(channel)
×
4338
        await tx.sign(funding_accounts)
×
4339

4340
        if not preview:
×
4341
            await self.broadcast_or_release(tx, blocking)
×
4342
            await self.storage.save_supports({claim_id: [{
×
4343
                'txid': tx.id,
4344
                'nout': tx.position,
4345
                'address': claim_address,
4346
                'claim_id': claim_id,
4347
                'amount': dewies_to_lbc(new_txo.amount)
4348
            }]})
4349
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('new_support'))
×
4350
        else:
4351
            await self.ledger.release_tx(tx)
×
4352

4353
        return tx
×
4354

4355
    @requires(WALLET_COMPONENT)
1✔
4356
    def jsonrpc_support_list(self, *args, received=False, sent=False, staked=False, **kwargs):
1✔
4357
        """
4358
        List staked supports and sent/received tips.
4359

4360
        Usage:
4361
            support_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
4362
                         [--name=<name>...] [--claim_id=<claim_id>...]
4363
                         [--received | --sent | --staked] [--is_spent]
4364
                         [--page=<page>] [--page_size=<page_size>] [--no_totals]
4365

4366
        Options:
4367
            --name=<name>              : (str or list) claim name
4368
            --claim_id=<claim_id>      : (str or list) claim id
4369
            --received                 : (bool) only show received (tips)
4370
            --sent                     : (bool) only show sent (tips)
4371
            --staked                   : (bool) only show my staked supports
4372
            --is_spent                 : (bool) show abandoned supports
4373
            --account_id=<account_id>  : (str) id of the account to query
4374
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4375
            --page=<page>              : (int) page to return during paginating
4376
            --page_size=<page_size>    : (int) number of items on page during pagination
4377
            --no_totals                : (bool) do not calculate the total number of pages and items in result set
4378
                                                (significant performance boost)
4379

4380
        Returns: {Paginated[Output]}
4381
        """
4382
        kwargs['type'] = 'support'
×
4383
        if 'is_spent' not in kwargs:
×
4384
            kwargs['is_not_spent'] = True
×
4385
        if received:
×
4386
            kwargs['is_not_my_input'] = True
×
4387
            kwargs['is_my_output'] = True
×
4388
        elif sent:
×
4389
            kwargs['is_my_input'] = True
×
4390
            kwargs['is_not_my_output'] = True
×
4391
            # spent for not my outputs is undetermined
4392
            kwargs.pop('is_spent', None)
×
4393
            kwargs.pop('is_not_spent', None)
×
4394
        elif staked:
×
4395
            kwargs['is_my_input'] = True
×
4396
            kwargs['is_my_output'] = True
×
4397
        return self.jsonrpc_txo_list(*args, **kwargs)
×
4398

4399
    @requires(WALLET_COMPONENT)
1✔
4400
    async def jsonrpc_support_abandon(
1✔
4401
            self, claim_id=None, txid=None, nout=None, keep=None,
4402
            account_id=None, wallet_id=None, preview=False, blocking=False):
4403
        """
4404
        Abandon supports, including tips, of a specific claim, optionally
4405
        keeping some amount as supports.
4406

4407
        Usage:
4408
            support_abandon [--claim_id=<claim_id>] [(--txid=<txid> --nout=<nout>)] [--keep=<keep>]
4409
                            [--account_id=<account_id>] [--wallet_id=<wallet_id>]
4410
                            [--preview] [--blocking]
4411

4412
        Options:
4413
            --claim_id=<claim_id>     : (str) claim_id of the support to abandon
4414
            --txid=<txid>             : (str) txid of the claim to abandon
4415
            --nout=<nout>             : (int) nout of the claim to abandon
4416
            --keep=<keep>             : (decimal) amount of lbc to keep as support
4417
            --account_id=<account_id> : (str) id of the account to use
4418
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
4419
            --preview                 : (bool) do not broadcast the transaction
4420
            --blocking                : (bool) wait until abandon is in mempool
4421

4422
        Returns: {Transaction}
4423
        """
4424
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4425
        assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
×
4426
        if account_id:
×
4427
            account = wallet.get_account_or_error(account_id)
×
4428
            accounts = [account]
×
4429
        else:
4430
            account = wallet.default_account
×
4431
            accounts = wallet.accounts
×
4432

4433
        if txid is not None and nout is not None:
×
4434
            supports = await self.ledger.get_supports(
×
4435
                wallet=wallet, accounts=accounts, **{'txo.txid': txid, 'txo.position': nout}
4436
            )
4437
        elif claim_id is not None:
×
4438
            supports = await self.ledger.get_supports(
×
4439
                wallet=wallet, accounts=accounts, claim_id=claim_id
4440
            )
4441
        else:
4442
            # TODO: use error from lbry.error
4443
            raise Exception('Must specify claim_id, or txid and nout')
×
4444

4445
        if not supports:
×
4446
            # TODO: use error from lbry.error
4447
            raise Exception('No supports found for the specified claim_id or txid:nout')
×
4448

4449
        if keep is not None:
×
4450
            keep = self.get_dewies_or_error('keep', keep)
×
4451
        else:
4452
            keep = 0
×
4453

4454
        outputs = []
×
4455
        if keep > 0:
×
4456
            outputs = [
×
4457
                Output.pay_support_pubkey_hash(
4458
                    keep, supports[0].claim_name, supports[0].claim_id, supports[0].pubkey_hash
4459
                )
4460
            ]
4461

4462
        tx = await Transaction.create(
×
4463
            [Input.spend(txo) for txo in supports], outputs, accounts, account
4464
        )
4465

4466
        if not preview:
×
4467
            await self.broadcast_or_release(tx, blocking)
×
4468
            self.component_manager.loop.create_task(self.analytics_manager.send_claim_action('abandon'))
×
4469
        else:
4470
            await self.ledger.release_tx(tx)
×
4471

4472
        return tx
×
4473

4474
    TRANSACTION_DOC = """
1✔
4475
    Transaction management.
4476
    """
4477

4478
    @requires(WALLET_COMPONENT)
1✔
4479
    def jsonrpc_transaction_list(self, account_id=None, wallet_id=None, page=None, page_size=None):
1✔
4480
        """
4481
        List transactions belonging to wallet
4482

4483
        Usage:
4484
            transaction_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
4485
                             [--page=<page>] [--page_size=<page_size>]
4486

4487
        Options:
4488
            --account_id=<account_id>  : (str) id of the account to query
4489
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4490
            --page=<page>              : (int) page to return during paginating
4491
            --page_size=<page_size>    : (int) number of items on page during pagination
4492

4493
        Returns:
4494
            (list) List of transactions
4495

4496
            {
4497
                "claim_info": (list) claim info if in txn [{
4498
                                                        "address": (str) address of claim,
4499
                                                        "balance_delta": (float) bid amount,
4500
                                                        "amount": (float) claim amount,
4501
                                                        "claim_id": (str) claim id,
4502
                                                        "claim_name": (str) claim name,
4503
                                                        "nout": (int) nout
4504
                                                        }],
4505
                "abandon_info": (list) abandon info if in txn [{
4506
                                                        "address": (str) address of abandoned claim,
4507
                                                        "balance_delta": (float) returned amount,
4508
                                                        "amount": (float) claim amount,
4509
                                                        "claim_id": (str) claim id,
4510
                                                        "claim_name": (str) claim name,
4511
                                                        "nout": (int) nout
4512
                                                        }],
4513
                "confirmations": (int) number of confirmations for the txn,
4514
                "date": (str) date and time of txn,
4515
                "fee": (float) txn fee,
4516
                "support_info": (list) support info if in txn [{
4517
                                                        "address": (str) address of support,
4518
                                                        "balance_delta": (float) support amount,
4519
                                                        "amount": (float) support amount,
4520
                                                        "claim_id": (str) claim id,
4521
                                                        "claim_name": (str) claim name,
4522
                                                        "is_tip": (bool),
4523
                                                        "nout": (int) nout
4524
                                                        }],
4525
                "timestamp": (int) timestamp,
4526
                "txid": (str) txn id,
4527
                "update_info": (list) update info if in txn [{
4528
                                                        "address": (str) address of claim,
4529
                                                        "balance_delta": (float) credited/debited
4530
                                                        "amount": (float) absolute amount,
4531
                                                        "claim_id": (str) claim id,
4532
                                                        "claim_name": (str) claim name,
4533
                                                        "nout": (int) nout
4534
                                                        }],
4535
                "value": (float) value of txn
4536
            }
4537

4538
        """
4539
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4540
        if account_id:
×
4541
            account = wallet.get_account_or_error(account_id)
×
4542
            transactions = account.get_transaction_history
×
4543
            transaction_count = account.get_transaction_history_count
×
4544
        else:
4545
            transactions = partial(
×
4546
                self.ledger.get_transaction_history, wallet=wallet, accounts=wallet.accounts)
4547
            transaction_count = partial(
×
4548
                self.ledger.get_transaction_history_count, wallet=wallet, accounts=wallet.accounts)
4549
        return paginate_rows(transactions, transaction_count, page, page_size, read_only=True)
×
4550

4551
    @requires(WALLET_COMPONENT)
1✔
4552
    def jsonrpc_transaction_show(self, txid):
1✔
4553
        """
4554
        Get a decoded transaction from a txid
4555

4556
        Usage:
4557
            transaction_show (<txid> | --txid=<txid>)
4558

4559
        Options:
4560
            --txid=<txid>  : (str) txid of the transaction
4561

4562
        Returns: {Transaction}
4563
        """
4564
        return self.wallet_manager.get_transaction(txid)
×
4565

4566
    TXO_DOC = """
1✔
4567
    List and sum transaction outputs.
4568
    """
4569

4570
    @staticmethod
1✔
4571
    def _constrain_txo_from_kwargs(
1✔
4572
            constraints, type=None, txid=None,  # pylint: disable=redefined-builtin
4573
            claim_id=None, channel_id=None, not_channel_id=None,
4574
            name=None, reposted_claim_id=None,
4575
            is_spent=False, is_not_spent=False,
4576
            has_source=None, has_no_source=None,
4577
            is_my_input_or_output=None, exclude_internal_transfers=False,
4578
            is_my_output=None, is_not_my_output=None,
4579
            is_my_input=None, is_not_my_input=None):
4580
        if is_spent:
×
4581
            constraints['is_spent'] = True
×
4582
        elif is_not_spent:
×
4583
            constraints['is_spent'] = False
×
4584
        if has_source:
×
4585
            constraints['has_source'] = True
×
4586
        elif has_no_source:
×
4587
            constraints['has_source'] = False
×
4588
        constraints['exclude_internal_transfers'] = exclude_internal_transfers
×
4589
        if is_my_input_or_output is True:
×
4590
            constraints['is_my_input_or_output'] = True
×
4591
        else:
4592
            if is_my_input is True:
×
4593
                constraints['is_my_input'] = True
×
4594
            elif is_not_my_input is True:
×
4595
                constraints['is_my_input'] = False
×
4596
            if is_my_output is True:
×
4597
                constraints['is_my_output'] = True
×
4598
            elif is_not_my_output is True:
×
4599
                constraints['is_my_output'] = False
×
4600
        database.constrain_single_or_list(constraints, 'txo_type', type, lambda x: TXO_TYPES[x])
×
4601
        database.constrain_single_or_list(constraints, 'channel_id', channel_id)
×
4602
        database.constrain_single_or_list(constraints, 'channel_id', not_channel_id, negate=True)
×
4603
        database.constrain_single_or_list(constraints, 'claim_id', claim_id)
×
4604
        database.constrain_single_or_list(constraints, 'claim_name', name)
×
4605
        database.constrain_single_or_list(constraints, 'txid', txid)
×
4606
        database.constrain_single_or_list(constraints, 'reposted_claim_id', reposted_claim_id)
×
4607
        return constraints
×
4608

4609
    @requires(WALLET_COMPONENT)
1✔
4610
    def jsonrpc_txo_list(
1✔
4611
            self, account_id=None, wallet_id=None, page=None, page_size=None,
4612
            resolve=False, order_by=None, no_totals=False, include_received_tips=False, **kwargs):
4613
        """
4614
        List my transaction outputs.
4615

4616
        Usage:
4617
            txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] [--claim_id=<claim_id>...]
4618
                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
4619
                     [--name=<name>...] [--is_spent | --is_not_spent]
4620
                     [--is_my_input_or_output |
4621
                         [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]
4622
                     ]
4623
                     [--exclude_internal_transfers] [--include_received_tips]
4624
                     [--wallet_id=<wallet_id>] [--page=<page>] [--page_size=<page_size>]
4625
                     [--resolve] [--order_by=<order_by>][--no_totals]
4626

4627
        Options:
4628
            --type=<type>              : (str or list) claim type: stream, channel, support,
4629
                                         purchase, collection, repost, other
4630
            --txid=<txid>              : (str or list) transaction id of outputs
4631
            --claim_id=<claim_id>      : (str or list) claim id
4632
            --channel_id=<channel_id>  : (str or list) claims in this channel
4633
      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel
4634
            --name=<name>              : (str or list) claim name
4635
            --is_spent                 : (bool) only show spent txos
4636
            --is_not_spent             : (bool) only show not spent txos
4637
            --is_my_input_or_output    : (bool) txos which have your inputs or your outputs,
4638
                                                if using this flag the other related flags
4639
                                                are ignored (--is_my_output, --is_my_input, etc)
4640
            --is_my_output             : (bool) show outputs controlled by you
4641
            --is_not_my_output         : (bool) show outputs not controlled by you
4642
            --is_my_input              : (bool) show outputs created by you
4643
            --is_not_my_input          : (bool) show outputs not created by you
4644
           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:
4645
                                                "--is_my_input --is_my_output --type=other"
4646
                                                this allows to exclude "change" payments, this
4647
                                                flag can be used in combination with any of the other flags
4648
            --include_received_tips    : (bool) calculate the amount of tips received for claim outputs
4649
            --account_id=<account_id>  : (str) id of the account to query
4650
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4651
            --page=<page>              : (int) page to return during paginating
4652
            --page_size=<page_size>    : (int) number of items on page during pagination
4653
            --resolve                  : (bool) resolves each claim to provide additional metadata
4654
            --order_by=<order_by>      : (str) field to order by: 'name', 'height', 'amount' and 'none'
4655
            --no_totals                : (bool) do not calculate the total number of pages and items in result set
4656
                                                (significant performance boost)
4657

4658
        Returns: {Paginated[Output]}
4659
        """
4660
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4661
        if account_id:
×
4662
            account = wallet.get_account_or_error(account_id)
×
4663
            claims = account.get_txos
×
4664
            claim_count = account.get_txo_count
×
4665
        else:
4666
            claims = partial(self.ledger.get_txos, wallet=wallet, accounts=wallet.accounts, read_only=True)
×
4667
            claim_count = partial(self.ledger.get_txo_count, wallet=wallet, accounts=wallet.accounts, read_only=True)
×
4668
        constraints = {
×
4669
            'resolve': resolve,
4670
            'include_is_spent': True,
4671
            'include_is_my_input': True,
4672
            'include_is_my_output': True,
4673
            'include_received_tips': include_received_tips
4674
        }
4675
        if order_by is not None:
×
4676
            if order_by == 'name':
×
4677
                constraints['order_by'] = 'txo.claim_name'
×
4678
            elif order_by in ('height', 'amount', 'none'):
×
4679
                constraints['order_by'] = order_by
×
4680
            else:
4681
                # TODO: use error from lbry.error
4682
                raise ValueError(f"'{order_by}' is not a valid --order_by value.")
×
4683
        self._constrain_txo_from_kwargs(constraints, **kwargs)
×
4684
        return paginate_rows(claims, None if no_totals else claim_count, page, page_size, **constraints)
×
4685

4686
    @requires(WALLET_COMPONENT)
1✔
4687
    async def jsonrpc_txo_spend(
1✔
4688
            self, account_id=None, wallet_id=None, batch_size=100,
4689
            include_full_tx=False, preview=False, blocking=False, **kwargs):
4690
        """
4691
        Spend transaction outputs, batching into multiple transactions as necessary.
4692

4693
        Usage:
4694
            txo_spend [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...] [--claim_id=<claim_id>...]
4695
                      [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
4696
                      [--name=<name>...] [--is_my_input | --is_not_my_input]
4697
                      [--exclude_internal_transfers] [--wallet_id=<wallet_id>]
4698
                      [--preview] [--blocking] [--batch_size=<batch_size>] [--include_full_tx]
4699

4700
        Options:
4701
            --type=<type>              : (str or list) claim type: stream, channel, support,
4702
                                         purchase, collection, repost, other
4703
            --txid=<txid>              : (str or list) transaction id of outputs
4704
            --claim_id=<claim_id>      : (str or list) claim id
4705
            --channel_id=<channel_id>  : (str or list) claims in this channel
4706
      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel
4707
            --name=<name>              : (str or list) claim name
4708
            --is_my_input              : (bool) show outputs created by you
4709
            --is_not_my_input          : (bool) show outputs not created by you
4710
           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:
4711
                                                "--is_my_input --is_my_output --type=other"
4712
                                                this allows to exclude "change" payments, this
4713
                                                flag can be used in combination with any of the other flags
4714
            --account_id=<account_id>  : (str) id of the account to query
4715
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4716
            --preview                  : (bool) do not broadcast the transaction
4717
            --blocking                 : (bool) wait until abandon is in mempool
4718
            --batch_size=<batch_size>  : (int) number of txos to spend per transactions
4719
            --include_full_tx          : (bool) include entire tx in output and not just the txid
4720

4721
        Returns: {List[Transaction]}
4722
        """
4723
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4724
        accounts = [wallet.get_account_or_error(account_id)] if account_id else wallet.accounts
×
4725
        txos = await self.ledger.get_txos(
×
4726
            wallet=wallet, accounts=accounts, read_only=True,
4727
            no_tx=True, no_channel_info=True,
4728
            **self._constrain_txo_from_kwargs(
4729
                {}, is_not_spent=True, is_my_output=True, **kwargs
4730
            )
4731
        )
4732
        txs = []
×
4733
        while txos:
×
4734
            txs.append(
×
4735
                await Transaction.create(
4736
                    [Input.spend(txos.pop()) for _ in range(min(len(txos), batch_size))],
4737
                    [], accounts, accounts[0]
4738
                )
4739
            )
4740
        if not preview:
×
4741
            for tx in txs:
×
4742
                await self.broadcast_or_release(tx, blocking)
×
4743
        if include_full_tx:
×
4744
            return txs
×
4745
        return [{'txid': tx.id} for tx in txs]
×
4746

4747
    @requires(WALLET_COMPONENT)
1✔
4748
    def jsonrpc_txo_sum(self, account_id=None, wallet_id=None, **kwargs):
1✔
4749
        """
4750
        Sum of transaction outputs.
4751

4752
        Usage:
4753
            txo_list [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]
4754
                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
4755
                     [--claim_id=<claim_id>...] [--name=<name>...]
4756
                     [--is_spent] [--is_not_spent]
4757
                     [--is_my_input_or_output |
4758
                         [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]
4759
                     ]
4760
                     [--exclude_internal_transfers] [--wallet_id=<wallet_id>]
4761

4762
        Options:
4763
            --type=<type>              : (str or list) claim type: stream, channel, support,
4764
                                         purchase, collection, repost, other
4765
            --txid=<txid>              : (str or list) transaction id of outputs
4766
            --claim_id=<claim_id>      : (str or list) claim id
4767
            --name=<name>              : (str or list) claim name
4768
            --channel_id=<channel_id>  : (str or list) claims in this channel
4769
      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel
4770
            --is_spent                 : (bool) only show spent txos
4771
            --is_not_spent             : (bool) only show not spent txos
4772
            --is_my_input_or_output    : (bool) txos which have your inputs or your outputs,
4773
                                                if using this flag the other related flags
4774
                                                are ignored (--is_my_output, --is_my_input, etc)
4775
            --is_my_output             : (bool) show outputs controlled by you
4776
            --is_not_my_output         : (bool) show outputs not controlled by you
4777
            --is_my_input              : (bool) show outputs created by you
4778
            --is_not_my_input          : (bool) show outputs not created by you
4779
           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:
4780
                                                "--is_my_input --is_my_output --type=other"
4781
                                                this allows to exclude "change" payments, this
4782
                                                flag can be used in combination with any of the other flags
4783
            --account_id=<account_id>  : (str) id of the account to query
4784
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4785

4786
        Returns: int
4787
        """
4788
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4789
        return self.ledger.get_txo_sum(
×
4790
            wallet=wallet, accounts=[wallet.get_account_or_error(account_id)] if account_id else wallet.accounts,
4791
            read_only=True, **self._constrain_txo_from_kwargs({}, **kwargs)
4792
        )
4793

4794
    @requires(WALLET_COMPONENT)
1✔
4795
    async def jsonrpc_txo_plot(
1✔
4796
            self, account_id=None, wallet_id=None,
4797
            days_back=0, start_day=None, days_after=None, end_day=None, **kwargs):
4798
        """
4799
        Plot transaction output sum over days.
4800

4801
        Usage:
4802
            txo_plot [--account_id=<account_id>] [--type=<type>...] [--txid=<txid>...]
4803
                     [--claim_id=<claim_id>...] [--name=<name>...] [--is_spent] [--is_not_spent]
4804
                     [--channel_id=<channel_id>...] [--not_channel_id=<not_channel_id>...]
4805
                     [--is_my_input_or_output |
4806
                         [[--is_my_output | --is_not_my_output] [--is_my_input | --is_not_my_input]]
4807
                     ]
4808
                     [--exclude_internal_transfers] [--wallet_id=<wallet_id>]
4809
                     [--days_back=<days_back> |
4810
                        [--start_day=<start_day> [--days_after=<days_after> | --end_day=<end_day>]]
4811
                     ]
4812

4813
        Options:
4814
            --type=<type>              : (str or list) claim type: stream, channel, support,
4815
                                         purchase, collection, repost, other
4816
            --txid=<txid>              : (str or list) transaction id of outputs
4817
            --claim_id=<claim_id>      : (str or list) claim id
4818
            --name=<name>              : (str or list) claim name
4819
            --channel_id=<channel_id>  : (str or list) claims in this channel
4820
      --not_channel_id=<not_channel_id>: (str or list) claims not in this channel
4821
            --is_spent                 : (bool) only show spent txos
4822
            --is_not_spent             : (bool) only show not spent txos
4823
            --is_my_input_or_output    : (bool) txos which have your inputs or your outputs,
4824
                                                if using this flag the other related flags
4825
                                                are ignored (--is_my_output, --is_my_input, etc)
4826
            --is_my_output             : (bool) show outputs controlled by you
4827
            --is_not_my_output         : (bool) show outputs not controlled by you
4828
            --is_my_input              : (bool) show outputs created by you
4829
            --is_not_my_input          : (bool) show outputs not created by you
4830
           --exclude_internal_transfers: (bool) excludes any outputs that are exactly this combination:
4831
                                                "--is_my_input --is_my_output --type=other"
4832
                                                this allows to exclude "change" payments, this
4833
                                                flag can be used in combination with any of the other flags
4834
            --account_id=<account_id>  : (str) id of the account to query
4835
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4836
            --days_back=<days_back>    : (int) number of days back from today
4837
                                               (not compatible with --start_day, --days_after, --end_day)
4838
            --start_day=<start_day>    : (date) start on specific date (YYYY-MM-DD)
4839
                                               (instead of --days_back)
4840
            --days_after=<days_after>  : (int) end number of days after --start_day
4841
                                               (instead of --end_day)
4842
            --end_day=<end_day>        : (date) end on specific date (YYYY-MM-DD)
4843
                                               (instead of --days_after)
4844

4845
        Returns: List[Dict]
4846
        """
4847
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4848
        plot = await self.ledger.get_txo_plot(
×
4849
            wallet=wallet, accounts=[wallet.get_account_or_error(account_id)] if account_id else wallet.accounts,
4850
            read_only=True, days_back=days_back, start_day=start_day, days_after=days_after, end_day=end_day,
4851
            **self._constrain_txo_from_kwargs({}, **kwargs)
4852
        )
4853
        for row in plot:
×
4854
            row['total'] = dewies_to_lbc(row['total'])
×
4855
        return plot
×
4856

4857
    UTXO_DOC = """
1✔
4858
    Unspent transaction management.
4859
    """
4860

4861
    @requires(WALLET_COMPONENT)
1✔
4862
    def jsonrpc_utxo_list(self, *args, **kwargs):
1✔
4863
        """
4864
        List unspent transaction outputs
4865

4866
        Usage:
4867
            utxo_list [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
4868
                      [--page=<page>] [--page_size=<page_size>]
4869

4870
        Options:
4871
            --account_id=<account_id>  : (str) id of the account to query
4872
            --wallet_id=<wallet_id>    : (str) restrict results to specific wallet
4873
            --page=<page>              : (int) page to return during paginating
4874
            --page_size=<page_size>    : (int) number of items on page during pagination
4875

4876
        Returns: {Paginated[Output]}
4877
        """
4878
        kwargs['type'] = ['other', 'purchase']
×
4879
        kwargs['is_not_spent'] = True
×
4880
        return self.jsonrpc_txo_list(*args, **kwargs)
×
4881

4882
    @requires(WALLET_COMPONENT)
1✔
4883
    async def jsonrpc_utxo_release(self, account_id=None, wallet_id=None):
1✔
4884
        """
4885
        When spending a UTXO it is locally locked to prevent double spends;
4886
        occasionally this can result in a UTXO being locked which ultimately
4887
        did not get spent (failed to broadcast, spend transaction was not
4888
        accepted by blockchain node, etc). This command releases the lock
4889
        on all UTXOs in your account.
4890

4891
        Usage:
4892
            utxo_release [<account_id> | --account_id=<account_id>] [--wallet_id=<wallet_id>]
4893

4894
        Options:
4895
            --account_id=<account_id> : (str) id of the account to query
4896
            --wallet_id=<wallet_id>   : (str) restrict operation to specific wallet
4897

4898
        Returns:
4899
            None
4900
        """
4901
        wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
×
4902
        if account_id is not None:
×
4903
            await wallet.get_account_or_error(account_id).release_all_outputs()
×
4904
        else:
4905
            for account in wallet.accounts:
×
4906
                await account.release_all_outputs()
×
4907

4908
    BLOB_DOC = """
1✔
4909
    Blob management.
4910
    """
4911

4912
    @requires(WALLET_COMPONENT, DHT_COMPONENT, BLOB_COMPONENT)
1✔
4913
    async def jsonrpc_blob_get(self, blob_hash, timeout=None, read=False):
1✔
4914
        """
4915
        Download and return a blob
4916

4917
        Usage:
4918
            blob_get (<blob_hash> | --blob_hash=<blob_hash>) [--timeout=<timeout>] [--read]
4919

4920
        Options:
4921
        --blob_hash=<blob_hash>                        : (str) blob hash of the blob to get
4922
        --timeout=<timeout>                            : (int) timeout in number of seconds
4923

4924
        Returns:
4925
            (str) Success/Fail message or (dict) decoded data
4926
        """
4927

4928
        blob = await download_blob(asyncio.get_event_loop(), self.conf, self.blob_manager, self.dht_node, blob_hash)
1✔
4929
        if read:
1!
4930
            with blob.reader_context() as handle:
1✔
4931
                return handle.read().decode()
1✔
4932
        elif isinstance(blob, BlobBuffer):
×
4933
            log.warning("manually downloaded blob buffer could have missed garbage collection, clearing it")
×
4934
            blob.delete()
×
4935
        return "Downloaded blob %s" % blob_hash
×
4936

4937
    @requires(BLOB_COMPONENT, DATABASE_COMPONENT)
1✔
4938
    async def jsonrpc_blob_delete(self, blob_hash):
1✔
4939
        """
4940
        Delete a blob
4941

4942
        Usage:
4943
            blob_delete (<blob_hash> | --blob_hash=<blob_hash>)
4944

4945
        Options:
4946
            --blob_hash=<blob_hash>  : (str) blob hash of the blob to delete
4947

4948
        Returns:
4949
            (str) Success/fail message
4950
        """
4951
        if not blob_hash or not is_valid_blobhash(blob_hash):
×
4952
            return f"Invalid blob hash to delete '{blob_hash}'"
×
4953
        streams = self.file_manager.get_filtered(sd_hash=blob_hash)
×
4954
        if streams:
×
4955
            await self.file_manager.delete(streams[0])
×
4956
        else:
4957
            await self.blob_manager.delete_blobs([blob_hash])
×
4958
        return "Deleted %s" % blob_hash
×
4959

4960
    PEER_DOC = """
1✔
4961
    DHT / Blob Exchange peer commands.
4962
    """
4963

4964
    async def jsonrpc_peer_list(self, blob_hash, page=None, page_size=None):
1✔
4965
        """
4966
        Get peers for blob hash
4967

4968
        Usage:
4969
            peer_list (<blob_hash> | --blob_hash=<blob_hash>)
4970
                [--page=<page>] [--page_size=<page_size>]
4971

4972
        Options:
4973
            --blob_hash=<blob_hash>                                  : (str) find available peers for this blob hash
4974
            --page=<page>                                            : (int) page to return during paginating
4975
            --page_size=<page_size>                                  : (int) number of items on page during pagination
4976

4977
        Returns:
4978
            (list) List of contact dictionaries {'address': <peer ip>, 'udp_port': <dht port>, 'tcp_port': <peer port>,
4979
             'node_id': <peer node id>}
4980
        """
4981

4982
        if not is_valid_blobhash(blob_hash):
×
4983
            # TODO: use error from lbry.error
4984
            raise Exception("invalid blob hash")
×
4985
        peer_q = asyncio.Queue(loop=self.component_manager.loop)
×
4986
        if self.component_manager.has_component(TRACKER_ANNOUNCER_COMPONENT):
×
4987
            tracker = self.component_manager.get_component(TRACKER_ANNOUNCER_COMPONENT)
×
4988
            tracker_peers = await tracker.get_kademlia_peer_list(bytes.fromhex(blob_hash))
×
4989
            log.info("Found %d peers for %s from trackers.", len(tracker_peers), blob_hash[:8])
×
4990
            peer_q.put_nowait(tracker_peers)
×
4991
        elif not self.component_manager.has_component(DHT_COMPONENT):
×
4992
            raise Exception("Peer list needs, at least, either a DHT component or a Tracker component for discovery.")
×
4993
        peers = []
×
4994
        if self.component_manager.has_component(DHT_COMPONENT):
×
4995
            await self.dht_node._peers_for_value_producer(blob_hash, peer_q)
×
4996
        while not peer_q.empty():
×
4997
            peers.extend(peer_q.get_nowait())
×
4998
        results = {
×
4999
            (peer.address, peer.tcp_port): {
5000
                "node_id": hexlify(peer.node_id).decode() if peer.node_id else None,
5001
                "address": peer.address,
5002
                "udp_port": peer.udp_port,
5003
                "tcp_port": peer.tcp_port,
5004
            }
5005
            for peer in peers
5006
        }
5007
        return paginate_list(list(results.values()), page, page_size)
×
5008

5009
    @requires(DATABASE_COMPONENT)
1✔
5010
    async def jsonrpc_blob_announce(self, blob_hash=None, stream_hash=None, sd_hash=None):
1✔
5011
        """
5012
        Announce blobs to the DHT
5013

5014
        Usage:
5015
            blob_announce (<blob_hash> | --blob_hash=<blob_hash>
5016
                          | --stream_hash=<stream_hash> | --sd_hash=<sd_hash>)
5017

5018
        Options:
5019
            --blob_hash=<blob_hash>        : (str) announce a blob, specified by blob_hash
5020
            --stream_hash=<stream_hash>    : (str) announce all blobs associated with
5021
                                             stream_hash
5022
            --sd_hash=<sd_hash>            : (str) announce all blobs associated with
5023
                                             sd_hash and the sd_hash itself
5024

5025
        Returns:
5026
            (bool) true if successful
5027
        """
5028
        blob_hashes = []
×
5029
        if blob_hash:
×
5030
            blob_hashes.append(blob_hash)
×
5031
        elif stream_hash or sd_hash:
×
5032
            if sd_hash and stream_hash:
×
5033
                # TODO: use error from lbry.error
5034
                raise Exception("either the sd hash or the stream hash should be provided, not both")
×
5035
            if sd_hash:
×
5036
                stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash)
×
5037
            blobs = await self.storage.get_blobs_for_stream(stream_hash, only_completed=True)
×
5038
            blob_hashes.extend(blob.blob_hash for blob in blobs if blob.blob_hash is not None)
×
5039
        else:
5040
            # TODO: use error from lbry.error
5041
            raise Exception('single argument must be specified')
×
5042
        await self.storage.should_single_announce_blobs(blob_hashes, immediate=True)
×
5043
        return True
×
5044

5045
    @requires(BLOB_COMPONENT, WALLET_COMPONENT)
1✔
5046
    async def jsonrpc_blob_list(self, uri=None, stream_hash=None, sd_hash=None, needed=None,
1✔
5047
                                finished=None, page=None, page_size=None):
5048
        """
5049
        Returns blob hashes. If not given filters, returns all blobs known by the blob manager
5050

5051
        Usage:
5052
            blob_list [--needed] [--finished] [<uri> | --uri=<uri>]
5053
                      [<stream_hash> | --stream_hash=<stream_hash>]
5054
                      [<sd_hash> | --sd_hash=<sd_hash>]
5055
                      [--page=<page>] [--page_size=<page_size>]
5056

5057
        Options:
5058
            --needed                     : (bool) only return needed blobs
5059
            --finished                   : (bool) only return finished blobs
5060
            --uri=<uri>                  : (str) filter blobs by stream in a uri
5061
            --stream_hash=<stream_hash>  : (str) filter blobs by stream hash
5062
            --sd_hash=<sd_hash>          : (str) filter blobs in a stream by sd hash, ie the hash of the stream
5063
                                                 descriptor blob for a stream that has been downloaded
5064
            --page=<page>                : (int) page to return during paginating
5065
            --page_size=<page_size>      : (int) number of items on page during pagination
5066

5067
        Returns:
5068
            (list) List of blob hashes
5069
        """
5070

5071
        if uri or stream_hash or sd_hash:
×
5072
            if uri:
×
5073
                metadata = (await self.resolve([], uri))[uri]
×
5074
                sd_hash = utils.get_sd_hash(metadata)
×
5075
                stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash)
×
5076
            elif stream_hash:
×
5077
                sd_hash = await self.storage.get_sd_blob_hash_for_stream(stream_hash)
×
5078
            elif sd_hash:
×
5079
                stream_hash = await self.storage.get_stream_hash_for_sd_hash(sd_hash)
×
5080
                sd_hash = await self.storage.get_sd_blob_hash_for_stream(stream_hash)
×
5081
            if sd_hash:
×
5082
                blobs = [sd_hash]
×
5083
            else:
5084
                blobs = []
×
5085
            if stream_hash:
×
5086
                blobs.extend([b.blob_hash for b in (await self.storage.get_blobs_for_stream(stream_hash))[:-1]])
×
5087
        else:
5088
            blobs = list(self.blob_manager.completed_blob_hashes)
×
5089
        if needed:
×
5090
            blobs = [blob_hash for blob_hash in blobs if not self.blob_manager.is_blob_verified(blob_hash)]
×
5091
        if finished:
×
5092
            blobs = [blob_hash for blob_hash in blobs if self.blob_manager.is_blob_verified(blob_hash)]
×
5093
        return paginate_list(blobs, page, page_size)
×
5094

5095
    @requires(BLOB_COMPONENT)
1✔
5096
    async def jsonrpc_blob_reflect(self, blob_hashes, reflector_server=None):
1✔
5097
        """
5098
        Reflects specified blobs
5099

5100
        Usage:
5101
            blob_reflect (<blob_hashes>...) [--reflector_server=<reflector_server>]
5102

5103
        Options:
5104
            --reflector_server=<reflector_server>          : (str) reflector address
5105

5106
        Returns:
5107
            (list) reflected blob hashes
5108
        """
5109

5110
        raise NotImplementedError()
×
5111

5112
    @requires(BLOB_COMPONENT)
1✔
5113
    async def jsonrpc_blob_reflect_all(self):
1✔
5114
        """
5115
        Reflects all saved blobs
5116

5117
        Usage:
5118
            blob_reflect_all
5119

5120
        Options:
5121
            None
5122

5123
        Returns:
5124
            (bool) true if successful
5125
        """
5126

5127
        raise NotImplementedError()
×
5128

5129
    @requires(DISK_SPACE_COMPONENT)
1✔
5130
    async def jsonrpc_blob_clean(self):
1✔
5131
        """
5132
        Deletes blobs to cleanup disk space
5133

5134
        Usage:
5135
            blob_clean
5136

5137
        Options:
5138
            None
5139

5140
        Returns:
5141
            (bool) true if successful
5142
        """
5143
        return await self.disk_space_manager.clean()
×
5144

5145
    @requires(FILE_MANAGER_COMPONENT)
1✔
5146
    async def jsonrpc_file_reflect(self, **kwargs):
1✔
5147
        """
5148
        Reflect all the blobs in a file matching the filter criteria
5149

5150
        Usage:
5151
            file_reflect [--sd_hash=<sd_hash>] [--file_name=<file_name>]
5152
                         [--stream_hash=<stream_hash>] [--rowid=<rowid>]
5153
                         [--reflector=<reflector>]
5154

5155
        Options:
5156
            --sd_hash=<sd_hash>          : (str) get file with matching sd hash
5157
            --file_name=<file_name>      : (str) get file with matching file name in the
5158
                                           downloads folder
5159
            --stream_hash=<stream_hash>  : (str) get file with matching stream hash
5160
            --rowid=<rowid>              : (int) get file with matching row id
5161
            --reflector=<reflector>      : (str) reflector server, ip address or url
5162
                                           by default choose a server from the config
5163

5164
        Returns:
5165
            (list) list of blobs reflected
5166
        """
5167

5168
        server, port = kwargs.get('server'), kwargs.get('port')
×
5169
        if server and port:
×
5170
            port = int(port)
×
5171
        else:
5172
            server, port = random.choice(self.conf.reflector_servers)
×
5173
        reflected = await asyncio.gather(*[
×
5174
            self.file_manager.source_managers['stream'].reflect_stream(stream, server, port)
5175
            for stream in self.file_manager.get_filtered(**kwargs)
5176
        ])
5177
        total = []
×
5178
        for reflected_for_stream in reflected:
×
5179
            total.extend(reflected_for_stream)
×
5180
        return total
×
5181

5182
    @requires(DHT_COMPONENT)
1✔
5183
    async def jsonrpc_peer_ping(self, node_id, address, port):
1✔
5184
        """
5185
        Send a kademlia ping to the specified peer. If address and port are provided the peer is directly pinged,
5186
        if not provided the peer is located first.
5187

5188
        Usage:
5189
            peer_ping (<node_id> | --node_id=<node_id>) (<address> | --address=<address>) (<port> | --port=<port>)
5190

5191
        Options:
5192
            None
5193

5194
        Returns:
5195
            (str) pong, or {'error': <error message>} if an error is encountered
5196
        """
5197
        peer = None
×
5198
        if node_id and address and port:
×
5199
            peer = make_kademlia_peer(unhexlify(node_id), address, udp_port=int(port))
×
5200
            try:
×
5201
                return await self.dht_node.protocol.get_rpc_peer(peer).ping()
×
5202
            except asyncio.TimeoutError:
×
5203
                return {'error': 'timeout'}
×
5204
        if not peer:
×
5205
            return {'error': 'peer not found'}
×
5206

5207
    @requires(DHT_COMPONENT)
1✔
5208
    def jsonrpc_routing_table_get(self):
1✔
5209
        """
5210
        Get DHT routing information
5211

5212
        Usage:
5213
            routing_table_get
5214

5215
        Options:
5216
            None
5217

5218
        Returns:
5219
            (dict) dictionary containing routing and peer information
5220
            {
5221
                "buckets": {
5222
                    <bucket index>: [
5223
                        {
5224
                            "address": (str) peer address,
5225
                            "udp_port": (int) peer udp port,
5226
                            "tcp_port": (int) peer tcp port,
5227
                            "node_id": (str) peer node id,
5228
                        }
5229
                    ]
5230
                },
5231
                "node_id": (str) the local dht node id
5232
                "prefix_neighbors_count": (int) the amount of peers sharing the same byte prefix of the local node id
5233
            }
5234
        """
5235
        result = {
×
5236
            'buckets': {},
5237
            'prefix_neighbors_count': 0
5238
        }
5239

5240
        for i, _ in enumerate(self.dht_node.protocol.routing_table.buckets):
×
5241
            result['buckets'][i] = []
×
5242
            for peer in self.dht_node.protocol.routing_table.buckets[i].peers:
×
5243
                host = {
×
5244
                    "address": peer.address,
5245
                    "udp_port": peer.udp_port,
5246
                    "tcp_port": peer.tcp_port,
5247
                    "node_id": hexlify(peer.node_id).decode(),
5248
                }
5249
                result['buckets'][i].append(host)
×
5250
                result['prefix_neighbors_count'] += 1 if peer.node_id[0] == self.dht_node.protocol.node_id[0] else 0
×
5251

5252
        result['node_id'] = hexlify(self.dht_node.protocol.node_id).decode()
×
5253
        return result
×
5254

5255
    TRACEMALLOC_DOC = """
1✔
5256
    Controls and queries tracemalloc memory tracing tools for troubleshooting.
5257
    """
5258

5259
    def jsonrpc_tracemalloc_enable(self):  # pylint: disable=no-self-use
1✔
5260
        """
5261
        Enable tracemalloc memory tracing
5262

5263
        Usage:
5264
            jsonrpc_tracemalloc_enable
5265

5266
        Options:
5267
            None
5268

5269
        Returns:
5270
            (bool) is it tracing?
5271
        """
5272
        tracemalloc.start()
×
5273
        return tracemalloc.is_tracing()
×
5274

5275
    def jsonrpc_tracemalloc_disable(self):  # pylint: disable=no-self-use
1✔
5276
        """
5277
        Disable tracemalloc memory tracing
5278

5279
        Usage:
5280
            jsonrpc_tracemalloc_disable
5281

5282
        Options:
5283
            None
5284

5285
        Returns:
5286
            (bool) is it tracing?
5287
        """
5288
        tracemalloc.stop()
×
5289
        return tracemalloc.is_tracing()
×
5290

5291
    def jsonrpc_tracemalloc_top(self, items: int = 10):  # pylint: disable=no-self-use
1✔
5292
        """
5293
        Show most common objects, the place that created them and their size.
5294

5295
        Usage:
5296
            jsonrpc_tracemalloc_top [(<items> | --items=<items>)]
5297

5298
        Options:
5299
            --items=<items>               : (int) maximum items to return, from the most common
5300

5301
        Returns:
5302
            (dict) dictionary containing most common objects in memory
5303
            {
5304
                "line": (str) filename and line number where it was created,
5305
                "code": (str) code that created it,
5306
                "size": (int) size in bytes, for each "memory block",
5307
                "count" (int) number of memory blocks
5308
            }
5309
        """
5310
        if not tracemalloc.is_tracing():
×
5311
            # TODO: use error from lbry.error
5312
            raise Exception("Enable tracemalloc first! See 'tracemalloc set' command.")
×
5313
        stats = tracemalloc.take_snapshot().filter_traces((
×
5314
            tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
5315
            tracemalloc.Filter(False, "<unknown>"),
5316
            # tracemalloc and linecache here use some memory, but thats not relevant
5317
            tracemalloc.Filter(False, tracemalloc.__file__),
5318
            tracemalloc.Filter(False, linecache.__file__),
5319
        )).statistics('lineno', True)
5320
        results = []
×
5321
        for stat in stats:
×
5322
            frame = stat.traceback[0]
×
5323
            filename = os.sep.join(frame.filename.split(os.sep)[-2:])
×
5324
            line = linecache.getline(frame.filename, frame.lineno).strip()
×
5325
            results.append({
×
5326
                "line": f"{filename}:{frame.lineno}",
5327
                "code": line,
5328
                "size": stat.size,
5329
                "count": stat.count
5330
            })
5331
            if len(results) == items:
×
5332
                break
×
5333
        return results
×
5334

5335
    async def broadcast_or_release(self, tx, blocking=False):
1✔
5336
        await self.wallet_manager.broadcast_or_release(tx, blocking)
×
5337

5338
    def valid_address_or_error(self, address, allow_script_address=False):
1✔
5339
        try:
×
5340
            assert self.ledger.is_pubkey_address(address) or (
×
5341
                allow_script_address and self.ledger.is_script_address(address)
5342
            )
5343
        except:
×
5344
            # TODO: use error from lbry.error
5345
            raise Exception(f"'{address}' is not a valid address")
×
5346

5347
    @staticmethod
1✔
5348
    def valid_stream_name_or_error(name: str):
1✔
5349
        try:
×
5350
            if not name:
×
5351
                raise InputStringIsBlankError('Stream name')
×
5352
            parsed = URL.parse(name)
×
5353
            if parsed.has_channel:
×
5354
                # TODO: use error from lbry.error
5355
                raise Exception(
×
5356
                    "Stream names cannot start with '@' symbol. This is reserved for channels claims."
5357
                )
5358
            if not parsed.has_stream or parsed.stream.name != name:
×
5359
                # TODO: use error from lbry.error
5360
                raise Exception('Stream name has invalid characters.')
×
5361
        except (TypeError, ValueError):
×
5362
            # TODO: use error from lbry.error
5363
            raise Exception("Invalid stream name.")
×
5364

5365
    @staticmethod
1✔
5366
    def valid_collection_name_or_error(name: str):
1✔
5367
        try:
×
5368
            if not name:
×
5369
                # TODO: use error from lbry.error
5370
                raise Exception('Collection name cannot be blank.')
×
5371
            parsed = URL.parse(name)
×
5372
            if parsed.has_channel:
×
5373
                # TODO: use error from lbry.error
5374
                raise Exception(
×
5375
                    "Collection names cannot start with '@' symbol. This is reserved for channels claims."
5376
                )
5377
            if not parsed.has_stream or parsed.stream.name != name:
×
5378
                # TODO: use error from lbry.error
5379
                raise Exception('Collection name has invalid characters.')
×
5380
        except (TypeError, ValueError):
×
5381
            # TODO: use error from lbry.error
5382
            raise Exception("Invalid collection name.")
×
5383

5384
    @staticmethod
1✔
5385
    def valid_channel_name_or_error(name: str):
1✔
5386
        try:
×
5387
            if not name:
×
5388
                # TODO: use error from lbry.error
5389
                raise Exception(
×
5390
                    "Channel name cannot be blank."
5391
                )
5392
            parsed = URL.parse(name)
×
5393
            if not parsed.has_channel:
×
5394
                # TODO: use error from lbry.error
5395
                raise Exception("Channel names must start with '@' symbol.")
×
5396
            if parsed.channel.name != name:
×
5397
                # TODO: use error from lbry.error
5398
                raise Exception("Channel name has invalid character")
×
5399
        except (TypeError, ValueError):
×
5400
            # TODO: use error from lbry.error
5401
            raise Exception("Invalid channel name.")
×
5402

5403
    def get_fee_address(self, kwargs: dict, claim_address: str) -> str:
1✔
5404
        if 'fee_address' in kwargs:
×
5405
            self.valid_address_or_error(kwargs['fee_address'])
×
5406
            return kwargs['fee_address']
×
5407
        if 'fee_currency' in kwargs or 'fee_amount' in kwargs:
×
5408
            return claim_address
×
5409

5410
    async def get_receiving_address(self, address: str, account: Optional[Account]) -> str:
1✔
5411
        if address is None and account is not None:
×
5412
            return await account.receiving.get_or_create_usable_address()
×
5413
        self.valid_address_or_error(address)
×
5414
        return address
×
5415

5416
    async def get_channel_or_none(
1✔
5417
            self, wallet: Wallet, account_ids: List[str], channel_id: str = None,
5418
            channel_name: str = None, for_signing: bool = False) -> Output:
5419
        if channel_id is not None or channel_name is not None:
×
5420
            return await self.get_channel_or_error(
×
5421
                wallet, account_ids, channel_id, channel_name, for_signing
5422
            )
5423

5424
    async def get_channel_or_error(
1✔
5425
            self, wallet: Wallet, account_ids: List[str], channel_id: str = None,
5426
            channel_name: str = None, for_signing: bool = False) -> Output:
5427
        if channel_id:
×
5428
            key, value = 'id', channel_id
×
5429
        elif channel_name:
×
5430
            key, value = 'name', channel_name
×
5431
        else:
5432
            # TODO: use error from lbry.error
5433
            raise ValueError("Couldn't find channel because a channel_id or channel_name was not provided.")
×
5434
        channels = await self.ledger.get_channels(
×
5435
            wallet=wallet, accounts=wallet.get_accounts_or_all(account_ids),
5436
            **{f'claim_{key}': value}
5437
        )
5438
        if len(channels) == 1:
×
5439
            if for_signing and not channels[0].has_private_key:
×
5440
                # TODO: use error from lbry.error
5441
                raise PrivateKeyNotFoundError(key, value)
×
5442
            return channels[0]
×
5443
        elif len(channels) > 1:
×
5444
            # TODO: use error from lbry.error
5445
            raise ValueError(
×
5446
                f"Multiple channels found with channel_{key} '{value}', "
5447
                f"pass a channel_id to narrow it down."
5448
            )
5449
        # TODO: use error from lbry.error
5450
        raise ValueError(f"Couldn't find channel with channel_{key} '{value}'.")
×
5451

5452
    @staticmethod
1✔
5453
    def get_dewies_or_error(argument: str, lbc: str, positive_value=False):
1✔
5454
        try:
×
5455
            dewies = lbc_to_dewies(lbc)
×
5456
            if positive_value and dewies <= 0:
×
5457
                # TODO: use error from lbry.error
5458
                raise ValueError(f"'{argument}' value must be greater than 0.0")
×
5459
            return dewies
×
5460
        except ValueError as e:
×
5461
            # TODO: use error from lbry.error
5462
            raise ValueError(f"Invalid value for '{argument}': {e.args[0]}")
×
5463

5464
    async def resolve(self, accounts, urls, **kwargs):
1✔
5465
        results = await self.ledger.resolve(accounts, urls, **kwargs)
×
5466
        if self.conf.save_resolved_claims and results:
×
5467
            try:
×
5468
                await self.storage.save_claim_from_output(
×
5469
                    self.ledger,
5470
                    *(result for result in results.values() if isinstance(result, Output))
5471
                )
5472
            except DecodeError:
×
5473
                pass
×
5474
        return results
×
5475

5476
    @staticmethod
1✔
5477
    def _old_get_temp_claim_info(tx, txo, address, claim_dict, name):
1✔
5478
        return {
×
5479
            "claim_id": txo.claim_id,
5480
            "name": name,
5481
            "amount": dewies_to_lbc(txo.amount),
5482
            "address": address,
5483
            "txid": tx.id,
5484
            "nout": txo.position,
5485
            "value": claim_dict,
5486
            "height": -1,
5487
            "claim_sequence": -1,
5488
        }
5489

5490

5491
def loggly_time_string(date):
1✔
5492
    formatted_dt = date.strftime("%Y-%m-%dT%H:%M:%S")
×
5493
    milliseconds = str(round(date.microsecond * (10.0 ** -5), 3))
×
5494
    return quote(formatted_dt + milliseconds + "Z")
×
5495

5496

5497
def get_loggly_query_string(installation_id):
1✔
5498
    base_loggly_search_url = "https://lbry.loggly.com/search#"
×
5499
    now = utils.now()
×
5500
    yesterday = now - utils.timedelta(days=1)
×
5501
    params = {
×
5502
        'terms': f'json.installation_id:{installation_id[:SHORT_ID_LEN]}*',
5503
        'from': loggly_time_string(yesterday),
5504
        'to': loggly_time_string(now)
5505
    }
5506
    data = urlencode(params)
×
5507
    return base_loggly_search_url + data
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc