Repository: lbryio/lbry-sdk
Branch: master
Commit: e7666f489418
Files: 318
Total size: 3.2 MB
Directory structure:
gitextract_scmdz25q/
├── .github/
│ └── workflows/
│ ├── main.yml
│ └── release.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── INSTALL.md
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── SECURITY.md
├── docker/
│ ├── Dockerfile.dht_node
│ ├── Dockerfile.wallet_server
│ ├── Dockerfile.web
│ ├── README.md
│ ├── docker-compose-wallet-server.yml
│ ├── docker-compose.yml
│ ├── hooks/
│ │ └── build
│ ├── install_choco.ps1
│ ├── set_build.py
│ ├── wallet_server_entrypoint.sh
│ └── webconf.yaml
├── docs/
│ └── api.json
├── example_daemon_settings.yml
├── lbry/
│ ├── .dockerignore
│ ├── __init__.py
│ ├── blob/
│ │ ├── __init__.py
│ │ ├── blob_file.py
│ │ ├── blob_info.py
│ │ ├── blob_manager.py
│ │ ├── disk_space_manager.py
│ │ └── writer.py
│ ├── blob_exchange/
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── downloader.py
│ │ ├── serialization.py
│ │ └── server.py
│ ├── build_info.py
│ ├── conf.py
│ ├── connection_manager.py
│ ├── constants.py
│ ├── crypto/
│ │ ├── __init__.py
│ │ ├── base58.py
│ │ ├── crypt.py
│ │ ├── hash.py
│ │ └── util.py
│ ├── dht/
│ │ ├── __init__.py
│ │ ├── blob_announcer.py
│ │ ├── constants.py
│ │ ├── error.py
│ │ ├── node.py
│ │ ├── peer.py
│ │ ├── protocol/
│ │ │ ├── __init__.py
│ │ │ ├── data_store.py
│ │ │ ├── distance.py
│ │ │ ├── iterative_find.py
│ │ │ ├── protocol.py
│ │ │ └── routing_table.py
│ │ └── serialization/
│ │ ├── __init__.py
│ │ ├── bencoding.py
│ │ └── datagram.py
│ ├── error/
│ │ ├── Makefile
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── generate.py
│ ├── extras/
│ │ ├── __init__.py
│ │ ├── cli.py
│ │ ├── daemon/
│ │ │ ├── __init__.py
│ │ │ ├── analytics.py
│ │ │ ├── client.py
│ │ │ ├── component.py
│ │ │ ├── componentmanager.py
│ │ │ ├── components.py
│ │ │ ├── daemon.py
│ │ │ ├── exchange_rate_manager.py
│ │ │ ├── json_response_encoder.py
│ │ │ ├── migrator/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── dbmigrator.py
│ │ │ │ ├── migrate10to11.py
│ │ │ │ ├── migrate11to12.py
│ │ │ │ ├── migrate12to13.py
│ │ │ │ ├── migrate13to14.py
│ │ │ │ ├── migrate14to15.py
│ │ │ │ ├── migrate15to16.py
│ │ │ │ ├── migrate1to2.py
│ │ │ │ ├── migrate2to3.py
│ │ │ │ ├── migrate3to4.py
│ │ │ │ ├── migrate4to5.py
│ │ │ │ ├── migrate5to6.py
│ │ │ │ ├── migrate6to7.py
│ │ │ │ ├── migrate7to8.py
│ │ │ │ ├── migrate8to9.py
│ │ │ │ └── migrate9to10.py
│ │ │ ├── security.py
│ │ │ ├── storage.py
│ │ │ └── undecorated.py
│ │ └── system_info.py
│ ├── file/
│ │ ├── __init__.py
│ │ ├── file_manager.py
│ │ ├── source.py
│ │ └── source_manager.py
│ ├── file_analysis.py
│ ├── prometheus.py
│ ├── schema/
│ │ ├── Makefile
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── attrs.py
│ │ ├── base.py
│ │ ├── claim.py
│ │ ├── compat.py
│ │ ├── mime_types.py
│ │ ├── purchase.py
│ │ ├── result.py
│ │ ├── support.py
│ │ ├── tags.py
│ │ ├── types/
│ │ │ ├── __init__.py
│ │ │ ├── v1/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── certificate_pb2.py
│ │ │ │ ├── fee_pb2.py
│ │ │ │ ├── legacy_claim_pb2.py
│ │ │ │ ├── metadata_pb2.py
│ │ │ │ ├── signature_pb2.py
│ │ │ │ ├── source_pb2.py
│ │ │ │ └── stream_pb2.py
│ │ │ └── v2/
│ │ │ ├── __init__.py
│ │ │ ├── claim_pb2.py
│ │ │ ├── purchase_pb2.py
│ │ │ ├── result_pb2.py
│ │ │ ├── support_pb2.py
│ │ │ └── wallet.json
│ │ └── url.py
│ ├── stream/
│ │ ├── __init__.py
│ │ ├── background_downloader.py
│ │ ├── descriptor.py
│ │ ├── downloader.py
│ │ ├── managed_stream.py
│ │ ├── reflector/
│ │ │ ├── __init__.py
│ │ │ ├── client.py
│ │ │ └── server.py
│ │ └── stream_manager.py
│ ├── testcase.py
│ ├── torrent/
│ │ ├── __init__.py
│ │ ├── session.py
│ │ ├── torrent.py
│ │ ├── torrent_manager.py
│ │ └── tracker.py
│ ├── utils.py
│ ├── wallet/
│ │ ├── __init__.py
│ │ ├── account.py
│ │ ├── bcd_data_stream.py
│ │ ├── bip32.py
│ │ ├── checkpoints.py
│ │ ├── claim_proofs.py
│ │ ├── coinselection.py
│ │ ├── constants.py
│ │ ├── database.py
│ │ ├── dewies.py
│ │ ├── hash.py
│ │ ├── header.py
│ │ ├── ledger.py
│ │ ├── manager.py
│ │ ├── mnemonic.py
│ │ ├── network.py
│ │ ├── orchstr8/
│ │ │ ├── __init__.py
│ │ │ ├── cli.py
│ │ │ ├── node.py
│ │ │ └── service.py
│ │ ├── rpc/
│ │ │ ├── __init__.py
│ │ │ ├── framing.py
│ │ │ ├── jsonrpc.py
│ │ │ ├── session.py
│ │ │ ├── socks.py
│ │ │ └── util.py
│ │ ├── script.py
│ │ ├── stream.py
│ │ ├── tasks.py
│ │ ├── transaction.py
│ │ ├── udp.py
│ │ ├── usage_payment.py
│ │ ├── util.py
│ │ ├── wallet.py
│ │ └── words/
│ │ ├── __init__.py
│ │ ├── chinese_simplified.py
│ │ ├── english.py
│ │ ├── japanese.py
│ │ ├── portuguese.py
│ │ └── spanish.py
│ └── winpaths.py
├── scripts/
│ ├── Dockerfile.lbry_orchstr8
│ ├── check_signature.py
│ ├── check_video.py
│ ├── checkpoints.py
│ ├── checktrie.py
│ ├── deploy_dev_wallet_server.sh
│ ├── dht_crawler.py
│ ├── dht_monitor.py
│ ├── dht_node.py
│ ├── download_blob_from_peer.py
│ ├── find_max_server_load.py
│ ├── generate_json_api.py
│ ├── hook-coincurve.py
│ ├── hook-libtorrent.py
│ ├── idea/
│ │ ├── lbry-sdk.iml
│ │ ├── modules.xml
│ │ └── vcs.xml
│ ├── initialize_hub_from_snapshot.sh
│ ├── monitor_slow_queries.py
│ ├── publish_performance.py
│ ├── release.py
│ ├── repair_0_31_1_db.py
│ ├── sd_hash_sampler.py
│ ├── standalone_blob_server.py
│ ├── test_claim_search.py
│ ├── time_to_first_byte.py
│ ├── troubleshoot_p2p_and_dht_webservice.py
│ └── wallet_server_monitor.py
├── setup.cfg
├── setup.py
├── tests/
│ ├── __init__.py
│ ├── dht_mocks.py
│ ├── integration/
│ │ ├── __init__.py
│ │ ├── blockchain/
│ │ │ ├── __init__.py
│ │ │ ├── test_account_commands.py
│ │ │ ├── test_blockchain_reorganization.py
│ │ │ ├── test_network.py
│ │ │ ├── test_purchase_command.py
│ │ │ ├── test_sync.py
│ │ │ ├── test_wallet_commands.py
│ │ │ └── test_wallet_server_sessions.py
│ │ ├── claims/
│ │ │ ├── __init__.py
│ │ │ └── test_claim_commands.py
│ │ ├── datanetwork/
│ │ │ ├── __init__.py
│ │ │ ├── test_dht.py
│ │ │ ├── test_file_commands.py
│ │ │ └── test_streaming.py
│ │ ├── other/
│ │ │ ├── __init__.py
│ │ │ ├── test_chris45.py
│ │ │ ├── test_cli.py
│ │ │ ├── test_exchange_rate_manager.py
│ │ │ ├── test_other_commands.py
│ │ │ └── test_transcoding.py
│ │ ├── takeovers/
│ │ │ ├── __init__.py
│ │ │ └── test_resolve_command.py
│ │ └── transactions/
│ │ ├── __init__.py
│ │ ├── test_internal_transaction_api.py
│ │ ├── test_transaction_commands.py
│ │ └── test_transactions.py
│ ├── test_utils.py
│ └── unit/
│ ├── __init__.py
│ ├── analytics/
│ │ ├── __init__.py
│ │ └── test_track.py
│ ├── blob/
│ │ ├── __init__.py
│ │ ├── test_blob_file.py
│ │ └── test_blob_manager.py
│ ├── blob_exchange/
│ │ ├── __init__.py
│ │ └── test_transfer_blob.py
│ ├── components/
│ │ ├── __init__.py
│ │ └── test_component_manager.py
│ ├── core/
│ │ ├── __init__.py
│ │ └── test_utils.py
│ ├── database/
│ │ ├── __init__.py
│ │ └── test_SQLiteStorage.py
│ ├── dht/
│ │ ├── __init__.py
│ │ ├── protocol/
│ │ │ ├── __init__.py
│ │ │ ├── test_data_store.py
│ │ │ ├── test_distance.py
│ │ │ ├── test_kbucket.py
│ │ │ ├── test_protocol.py
│ │ │ └── test_routing_table.py
│ │ ├── serialization/
│ │ │ ├── __init__.py
│ │ │ ├── test_bencoding.py
│ │ │ └── test_datagram.py
│ │ ├── test_blob_announcer.py
│ │ ├── test_node.py
│ │ └── test_peer.py
│ ├── lbrynet_daemon/
│ │ ├── __init__.py
│ │ ├── test_Daemon.py
│ │ ├── test_allowed_origin.py
│ │ ├── test_exchange_rate_manager.py
│ │ └── test_mime_types.py
│ ├── schema/
│ │ ├── __init__.py
│ │ ├── test_claim_from_bytes.py
│ │ ├── test_mime_types.py
│ │ ├── test_models.py
│ │ ├── test_tags.py
│ │ └── test_url.py
│ ├── stream/
│ │ ├── __init__.py
│ │ ├── test_managed_stream.py
│ │ ├── test_reflector.py
│ │ ├── test_stream_descriptor.py
│ │ └── test_stream_manager.py
│ ├── test_cli.py
│ ├── test_conf.py
│ ├── test_utils.py
│ ├── torrent/
│ │ ├── __init__.py
│ │ └── test_tracker.py
│ └── wallet/
│ ├── __init__.py
│ ├── key_fixtures.py
│ ├── server/
│ │ ├── __init__.py
│ │ └── test_migration.py
│ ├── test_account.py
│ ├── test_bcd_data_stream.py
│ ├── test_bip32.py
│ ├── test_claim_proofs.py
│ ├── test_coinselection.py
│ ├── test_database.py
│ ├── test_dewies.py
│ ├── test_hash.py
│ ├── test_headers.py
│ ├── test_ledger.py
│ ├── test_mnemonic.py
│ ├── test_schema_signing.py
│ ├── test_script.py
│ ├── test_stream_controller.py
│ ├── test_transaction.py
│ ├── test_utils.py
│ └── test_wallet.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: ci
on: ["push", "pull_request", "workflow_dispatch"]
jobs:
lint:
name: lint
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: extract pip cache
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
restore-keys: ${{ runner.os }}-pip-
- run: pip install --user --upgrade pip wheel
- run: pip install -e .[lint]
- run: make lint
tests-unit:
name: "tests / unit"
strategy:
matrix:
os:
- ubuntu-20.04
- macos-13
- windows-2022
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: set pip cache dir
shell: bash
run: echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV
- name: extract pip cache
uses: actions/cache@v4
with:
path: ${{ env.PIP_CACHE_DIR }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
restore-keys: ${{ runner.os }}-pip-
- id: os-name
uses: ASzc/change-string-case-action@v6
with:
string: ${{ runner.os }}
- run: python -m pip install --user --upgrade pip wheel
- if: startsWith(runner.os, 'linux')
run: pip install -e .[test]
- if: startsWith(runner.os, 'linux')
env:
HOME: /tmp
run: make test-unit-coverage
- if: startsWith(runner.os, 'linux') != true
run: pip install -e .[test]
- if: startsWith(runner.os, 'linux') != true
env:
HOME: /tmp
run: coverage run --source=lbry -m unittest tests/unit/test_conf.py
- name: submit coverage report
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: tests-unit-${{ steps.os-name.outputs.lowercase }}
COVERALLS_PARALLEL: true
run: |
pip install coveralls
coveralls --service=github
tests-integration:
name: "tests / integration"
runs-on: ubuntu-20.04
strategy:
matrix:
test:
- datanetwork
- blockchain
- claims
- takeovers
- transactions
- other
steps:
- name: Configure sysctl limits
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: 7.12.1
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- if: matrix.test == 'other'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends ffmpeg
- name: extract pip cache
uses: actions/cache@v4
with:
path: ./.tox
key: tox-integration-${{ matrix.test }}-${{ hashFiles('setup.py') }}
restore-keys: txo-integration-${{ matrix.test }}-
- run: pip install tox coverage coveralls
- if: matrix.test == 'claims'
run: rm -rf .tox
- run: tox -e ${{ matrix.test }}
- name: submit coverage report
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: tests-integration-${{ matrix.test }}
COVERALLS_PARALLEL: true
run: |
coverage combine tests
coveralls --service=github
coverage:
needs: ["tests-unit", "tests-integration"]
runs-on: ubuntu-20.04
steps:
- name: finalize coverage report submission
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install coveralls
coveralls --service=github --finish
build:
needs: ["lint", "tests-unit", "tests-integration"]
name: "build / binary"
strategy:
matrix:
os:
- ubuntu-20.04
- macos-13
- windows-2022
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- id: os-name
uses: ASzc/change-string-case-action@v6
with:
string: ${{ runner.os }}
- name: set pip cache dir
shell: bash
run: echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV
- name: extract pip cache
uses: actions/cache@v4
with:
path: ${{ env.PIP_CACHE_DIR }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
restore-keys: ${{ runner.os }}-pip-
- run: pip install pyinstaller==6.0
- run: pip install -e .
- if: startsWith(github.ref, 'refs/tags/v')
run: python docker/set_build.py
- if: startsWith(runner.os, 'linux') || startsWith(runner.os, 'mac')
name: Build & Run (Unix)
run: |
pyinstaller --onefile --name lbrynet lbry/extras/cli.py
dist/lbrynet --version
- if: startsWith(runner.os, 'windows')
name: Build & Run (Windows)
run: |
pip install pywin32==301
pyinstaller --additional-hooks-dir=scripts/. --icon=icons/lbry256.ico --onefile --name lbrynet lbry/extras/cli.py
dist/lbrynet.exe --version
- uses: actions/upload-artifact@v4
with:
name: lbrynet-${{ steps.os-name.outputs.lowercase }}
path: dist/
release:
name: "release"
if: startsWith(github.ref, 'refs/tags/v')
needs: ["build"]
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- name: upload binaries
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_API_TOKEN }}
run: |
pip install githubrelease
chmod +x lbrynet-macos/lbrynet
chmod +x lbrynet-linux/lbrynet
zip --junk-paths lbrynet-mac.zip lbrynet-macos/lbrynet
zip --junk-paths lbrynet-linux.zip lbrynet-linux/lbrynet
zip --junk-paths lbrynet-windows.zip lbrynet-windows/lbrynet.exe
ls -lh
githubrelease release lbryio/lbry-sdk info ${GITHUB_REF#refs/tags/}
githubrelease asset lbryio/lbry-sdk upload ${GITHUB_REF#refs/tags/} \
lbrynet-mac.zip lbrynet-linux.zip lbrynet-windows.zip
githubrelease release lbryio/lbry-sdk publish ${GITHUB_REF#refs/tags/}
================================================
FILE: .github/workflows/release.yml
================================================
name: slack
on:
release:
types: [published]
jobs:
release:
name: "slack notification"
runs-on: ubuntu-20.04
steps:
- uses: LoveToKnow/slackify-markdown-action@v1.0.0
id: markdown
with:
text: "There is a new SDK release: ${{github.event.release.html_url}}\n${{ github.event.release.body }}"
- uses: slackapi/slack-github-action@v1.14.0
env:
CHANGELOG: ' ${{ steps.markdown.outputs.text }}'
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_BOT_WEBHOOK }}
with:
payload: '{"type": "mrkdwn", "text": ${{ toJSON(env.CHANGELOG) }} }'
================================================
FILE: CHANGELOG.md
================================================
# Change Log
This changelog is no longer up to date. For release notes, see https://github.com/lbryio/lbry-sdk/releases.
================================================
FILE: CONTRIBUTING.md
================================================
## Contributing to LBRY
https://lbry.tech/contribute
================================================
FILE: INSTALL.md
================================================
# Installing LBRY
If only the JSON-RPC API server is needed, the recommended way to install LBRY is to use a pre-built binary. We provide binaries for all major operating systems. See the [README](README.md)!
These instructions are for installing LBRY from source, which is recommended if you are interested in doing development work or LBRY is not available on your operating system (godspeed, TempleOS users).
Here's a video walkthrough of this setup, which is itself hosted by the LBRY network and provided via [spee.ch](https://github.com/lbryio/spee.ch):
[](https://spee.ch/967f99344308f1e90f0620d91b6c93e4dfb240e0/lbrynet-dev-setup.mp4)
## Prerequisites
Running `lbrynet` from source requires Python 3.7. Get the installer for your OS [here](https://www.python.org/downloads/release/python-370/).
After installing Python 3.7, you'll need to install some additional libraries depending on your operating system.
Because of [issue #2769](https://github.com/lbryio/lbry-sdk/issues/2769)
at the moment the `lbrynet` daemon will only work correctly with Python 3.7.
If Python 3.8+ is used, the daemon will start but the RPC server
may not accept messages, returning the following:
```
Could not connect to daemon. Are you sure it's running?
```
### macOS
macOS users will need to install [xcode command line tools](https://developer.xamarin.com/guides/testcloud/calabash/configuring/osx/install-xcode-command-line-tools/) and [homebrew](http://brew.sh/).
These environment variables also need to be set:
```
PYTHONUNBUFFERED=1
EVENT_NOKQUEUE=1
```
Remaining dependencies can then be installed by running:
```
brew install python protobuf
```
Assistance installing Python3: https://docs.python-guide.org/starting/install3/osx/.
### Linux
On Ubuntu (we recommend 18.04 or 20.04), install the following:
```
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install build-essential python3.7 python3.7-dev git python3.7-venv libssl-dev python-protobuf
```
The [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa) provides Python 3.7
for those Ubuntu distributions that no longer have it in their
official repositories.
On Raspbian, you will also need to install `python-pyparsing`.
If you're running another Linux distro, install the equivalent of the above packages for your system.
## Installation
### Linux/Mac
Clone the repository:
```bash
git clone https://github.com/lbryio/lbry-sdk.git
cd lbry-sdk
```
Create a Python virtual environment for lbry-sdk:
```bash
python3.7 -m venv lbry-venv
```
Activate virtual environment:
```bash
source lbry-venv/bin/activate
```
Make sure you're on Python 3.7+ as default in the virtual environment:
```bash
python --version
```
Install packages:
```bash
make install
```
If you are on Linux and using PyCharm, generates initial configs:
```bash
make idea
```
To verify your installation, `which lbrynet` should return a path inside
of the `lbry-venv` folder.
```bash
(lbry-venv) $ which lbrynet
/opt/lbry-sdk/lbry-venv/bin/lbrynet
```
To exit the virtual environment simply use the command `deactivate`.
### Windows
Clone the repository:
```bash
git clone https://github.com/lbryio/lbry-sdk.git
cd lbry-sdk
```
Create a Python virtual environment for lbry-sdk:
```bash
python -m venv lbry-venv
```
Activate virtual environment:
```bash
lbry-venv\Scripts\activate
```
Install packages:
```bash
pip install -e .
```
## Run the tests
### Elasticsearch
For running integration tests, Elasticsearch is required to be available at localhost:9200/
The easiest way to start it is using docker with:
```bash
make elastic-docker
```
Alternative installation methods are available [at Elasticsearch website](https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html).
To run the unit and integration tests from the repo directory:
```
python -m unittest discover tests.unit
python -m unittest discover tests.integration
```
## Usage
To start the API server:
```
lbrynet start
```
Whenever the code inside [lbry-sdk/lbry](./lbry)
is modified we should run `make install` to recompile the `lbrynet`
executable with the newest code.
## Development
When developing, remember to enter the environment,
and if you wish start the server interactively.
```bash
$ source lbry-venv/bin/activate
(lbry-venv) $ python lbry/extras/cli.py start
```
Parameters can be passed in the same way.
```bash
(lbry-venv) $ python lbry/extras/cli.py wallet balance
```
If a Python debugger (`pdb` or `ipdb`) is installed we can also start it
in this way, set up break points, and step through the code.
```bash
(lbry-venv) $ pip install ipdb
(lbry-venv) $ ipdb lbry/extras/cli.py
```
Happy hacking!
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015-2022 LBRY Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
include README.md
include CHANGELOG.md
include LICENSE
recursive-include lbry *.txt *.py
================================================
FILE: Makefile
================================================
.PHONY: install tools lint test test-unit test-unit-coverage test-integration idea
install:
pip install -e .
lint:
pylint --rcfile=setup.cfg lbry
#mypy --ignore-missing-imports lbry
test: test-unit test-integration
test-unit:
python -m unittest discover tests.unit
test-unit-coverage:
coverage run --source=lbry -m unittest discover -vv tests.unit
test-integration:
tox
idea:
mkdir -p .idea
cp -r scripts/idea/* .idea
elastic-docker:
docker run -d -v lbryhub:/usr/share/elasticsearch/data -p 9200:9200 -p 9300:9300 -e"ES_JAVA_OPTS=-Xms512m -Xmx512m" -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.12.1
================================================
FILE: README.md
================================================
# LBRY SDK [](https://github.com/lbryio/lbry-sdk/actions/workflows/main.yml) [](https://coveralls.io/github/lbryio/lbry-sdk)
LBRY is a decentralized peer-to-peer protocol for publishing and accessing digital content. It utilizes the [LBRY blockchain](https://github.com/lbryio/lbrycrd) as a global namespace and database of digital content. Blockchain entries contain searchable content metadata, identities, rights and access rules. LBRY also provides a data network that consists of peers (seeders) uploading and downloading data from other peers, possibly in exchange for payments, as well as a distributed hash table used by peers to discover other peers.
LBRY SDK for Python is currently the most fully featured implementation of the LBRY Network protocols and includes many useful components and tools for building decentralized applications. Primary features and components include:
* Built on Python 3.7 and `asyncio`.
* Kademlia DHT (Distributed Hash Table) implementation for finding peers to download from and announcing to peers what we have to host ([lbry.dht](https://github.com/lbryio/lbry-sdk/tree/master/lbry/dht)).
* Blob exchange protocol for transferring encrypted blobs of content and negotiating payments ([lbry.blob_exchange](https://github.com/lbryio/lbry-sdk/tree/master/lbry/blob_exchange)).
* Protobuf schema for encoding and decoding metadata stored on the blockchain ([lbry.schema](https://github.com/lbryio/lbry-sdk/tree/master/lbry/schema)).
* Wallet implementation for the LBRY blockchain ([lbry.wallet](https://github.com/lbryio/lbry-sdk/tree/master/lbry/wallet)).
* Daemon with a JSON-RPC API to ease building end user applications in any language and for automating various tasks ([lbry.extras.daemon](https://github.com/lbryio/lbry-sdk/tree/master/lbry/extras/daemon)).
## Installation
Our [releases page](https://github.com/lbryio/lbry-sdk/releases) contains pre-built binaries of the latest release, pre-releases, and past releases for macOS, Debian-based Linux, and Windows. [Automated travis builds](http://build.lbry.io/daemon/) are also available for testing.
## Usage
Run `lbrynet start` to launch the API server.
By default, `lbrynet` will provide a JSON-RPC server at `http://localhost:5279`. It is easy to interact with via cURL or sane programming languages.
Our [quickstart guide](https://lbry.tech/playground) provides a simple walkthrough and examples for learning.
With the daemon running, `lbrynet commands` will show you a list of commands.
The full API is documented [here](https://lbry.tech/api/sdk).
## Running from source
Installing from source is also relatively painless. Full instructions are in [INSTALL.md](INSTALL.md)
## Contributing
Contributions to this project are welcome, encouraged, and compensated. For more details, please check [this](https://lbry.tech/contribute) link.
## License
This project is MIT licensed. For the full license, see [LICENSE](LICENSE).
## Security
We take security seriously. Please contact security@lbry.com regarding any security issues. [Our PGP key is here](https://lbry.com/faq/pgp-key) if you need it.
## Contact
The primary contact for this project is [@eukreign](mailto:lex@lbry.com).
## Additional information and links
The documentation for the API can be found [here](https://lbry.tech/api/sdk).
Daemon defaults, ports, and other settings are documented [here](https://lbry.tech/resources/daemon-settings).
Settings can be configured using a daemon-settings.yml file. An example can be found [here](https://github.com/lbryio/lbry-sdk/blob/master/example_daemon_settings.yml).
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
While we are not at v1.0 yet, only the latest release will be supported.
## Reporting a Vulnerability
See https://lbry.com/faq/security
================================================
FILE: docker/Dockerfile.dht_node
================================================
FROM debian:10-slim
ARG user=lbry
ARG projects_dir=/home/$user
ARG db_dir=/database
ARG DOCKER_TAG
ARG DOCKER_COMMIT=docker
ENV DOCKER_TAG=$DOCKER_TAG DOCKER_COMMIT=$DOCKER_COMMIT
RUN apt-get update && \
apt-get -y --no-install-recommends install \
wget \
automake libtool \
tar unzip \
build-essential \
pkg-config \
libleveldb-dev \
python3.7 \
python3-dev \
python3-pip \
python3-wheel \
python3-setuptools && \
update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \
rm -rf /var/lib/apt/lists/*
RUN groupadd -g 999 $user && useradd -m -u 999 -g $user $user
COPY . $projects_dir
RUN chown -R $user:$user $projects_dir
RUN mkdir -p $db_dir
RUN chown -R $user:$user $db_dir
USER $user
WORKDIR $projects_dir
RUN python3 -m pip install -U setuptools pip
RUN make install
RUN python3 docker/set_build.py
RUN rm ~/.cache -rf
VOLUME $db_dir
ENTRYPOINT ["python3", "scripts/dht_node.py"]
================================================
FILE: docker/Dockerfile.wallet_server
================================================
FROM debian:10-slim
ARG user=lbry
ARG db_dir=/database
ARG projects_dir=/home/$user
ARG DOCKER_TAG
ARG DOCKER_COMMIT=docker
ENV DOCKER_TAG=$DOCKER_TAG DOCKER_COMMIT=$DOCKER_COMMIT
RUN apt-get update && \
apt-get -y --no-install-recommends install \
wget \
tar unzip \
build-essential \
automake libtool \
pkg-config \
libleveldb-dev \
python3.7 \
python3-dev \
python3-pip \
python3-wheel \
python3-cffi \
python3-setuptools && \
update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \
rm -rf /var/lib/apt/lists/*
RUN groupadd -g 999 $user && useradd -m -u 999 -g $user $user
RUN mkdir -p $db_dir
RUN chown -R $user:$user $db_dir
COPY . $projects_dir
RUN chown -R $user:$user $projects_dir
USER $user
WORKDIR $projects_dir
RUN pip install uvloop
RUN make install
RUN python3 docker/set_build.py
RUN rm ~/.cache -rf
# entry point
ARG host=0.0.0.0
ARG tcp_port=50001
ARG daemon_url=http://lbry:lbry@localhost:9245/
VOLUME $db_dir
ENV TCP_PORT=$tcp_port
ENV HOST=$host
ENV DAEMON_URL=$daemon_url
ENV DB_DIRECTORY=$db_dir
ENV MAX_SESSIONS=1000000000
ENV MAX_SEND=1000000000000000000
ENV EVENT_LOOP_POLICY=uvloop
COPY ./docker/wallet_server_entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
================================================
FILE: docker/Dockerfile.web
================================================
FROM debian:10-slim
ARG user=lbry
ARG downloads_dir=/database
ARG projects_dir=/home/$user
ARG DOCKER_TAG
ARG DOCKER_COMMIT=docker
ENV DOCKER_TAG=$DOCKER_TAG DOCKER_COMMIT=$DOCKER_COMMIT
RUN apt-get update && \
apt-get -y --no-install-recommends install \
wget \
automake libtool \
tar unzip \
build-essential \
pkg-config \
libleveldb-dev \
python3.7 \
python3-dev \
python3-pip \
python3-wheel \
python3-setuptools && \
update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \
rm -rf /var/lib/apt/lists/*
RUN groupadd -g 999 $user && useradd -m -u 999 -g $user $user
RUN mkdir -p $downloads_dir
RUN chown -R $user:$user $downloads_dir
COPY . $projects_dir
RUN chown -R $user:$user $projects_dir
USER $user
WORKDIR $projects_dir
RUN pip install uvloop
RUN make install
RUN python3 docker/set_build.py
RUN rm ~/.cache -rf
# entry point
VOLUME $downloads_dir
COPY ./docker/webconf.yaml /webconf.yaml
ENTRYPOINT ["/home/lbry/.local/bin/lbrynet", "start", "--config=/webconf.yaml"]
================================================
FILE: docker/README.md
================================================
### How to run with docker-compose
1. Edit config file and after that fix permissions with
```
sudo chown -R 999:999 webconf.yaml
```
2. Start SDK with
```
docker-compose up -d
```
================================================
FILE: docker/docker-compose-wallet-server.yml
================================================
version: "3"
volumes:
wallet_server:
es01:
services:
wallet_server:
depends_on:
- es01
image: lbry/wallet-server:${WALLET_SERVER_TAG:-latest-release}
restart: always
network_mode: host
ports:
- "50001:50001" # rpc port
- "2112:2112" # uncomment to enable prometheus
volumes:
- "wallet_server:/database"
environment:
- DAEMON_URL=http://lbry:lbry@127.0.0.1:9245
- MAX_QUERY_WORKERS=4
- CACHE_MB=1024
- CACHE_ALL_TX_HASHES=
- CACHE_ALL_CLAIM_TXOS=
- MAX_SEND=1000000000000000000
- MAX_RECEIVE=1000000000000000000
- MAX_SESSIONS=100000
- HOST=0.0.0.0
- TCP_PORT=50001
- PROMETHEUS_PORT=2112
- FILTERING_CHANNEL_IDS=770bd7ecba84fd2f7607fb15aedd2b172c2e153f 95e5db68a3101df19763f3a5182e4b12ba393ee8
- BLOCKING_CHANNEL_IDS=dd687b357950f6f271999971f43c785e8067c3a9 06871aa438032244202840ec59a469b303257cad b4a2528f436eca1bf3bf3e10ff3f98c57bd6c4c6
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.11.0
container_name: es01
environment:
- node.name=es01
- discovery.type=single-node
- indices.query.bool.max_clause_count=8192
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms4g -Xmx4g" # no more than 32, remember to disable swap
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- es01:/usr/share/elasticsearch/data
ports:
- 127.0.0.1:9200:9200
================================================
FILE: docker/docker-compose.yml
================================================
version: '3'
services:
websdk:
image: vshyba/websdk
ports:
- '5279:5279'
- '5280:5280'
volumes:
- ./webconf.yaml:/webconf.yaml
================================================
FILE: docker/hooks/build
================================================
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "$DIR/../.." ## make sure we're in the right place. Docker Hub screws this up sometimes
echo "docker build dir: $(pwd)"
docker build --build-arg DOCKER_TAG=$DOCKER_TAG --build-arg DOCKER_COMMIT=$SOURCE_COMMIT -f $DOCKERFILE_PATH -t $IMAGE_NAME .
================================================
FILE: docker/install_choco.ps1
================================================
# requires powershell and .NET 4+. see https://chocolatey.org/install for more info.
$chocoVersion = powershell choco -v
if(-not($chocoVersion)){
Write-Output "Chocolatey is not installed, installing now"
Write-Output "IF YOU KEEP GETTING THIS MESSAGE ON EVERY BUILD, TRY RESTARTING THE GITLAB RUNNER SO IT GETS CHOCO INTO IT'S ENV"
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}
else{
Write-Output "Chocolatey version $chocoVersion is already installed"
}
================================================
FILE: docker/set_build.py
================================================
import sys
import os
import re
import logging
import lbry.build_info as build_info_mod
log = logging.getLogger()
log.addHandler(logging.StreamHandler())
log.setLevel(logging.DEBUG)
def _check_and_set(d: dict, key: str, value: str):
try:
d[key]
except KeyError:
raise Exception(f"{key} var does not exist in {build_info_mod.__file__}")
d[key] = value
def main():
build_info = {item: build_info_mod.__dict__[item] for item in dir(build_info_mod) if not item.startswith("__")}
commit_hash = os.getenv('DOCKER_COMMIT', os.getenv('GITHUB_SHA'))
if commit_hash is None:
raise ValueError("Commit hash not found in env vars")
_check_and_set(build_info, "COMMIT_HASH", commit_hash[:6])
docker_tag = os.getenv('DOCKER_TAG')
if docker_tag:
_check_and_set(build_info, "DOCKER_TAG", docker_tag)
_check_and_set(build_info, "BUILD", "docker")
else:
if re.match(r'refs/tags/v\d+\.\d+\.\d+$', str(os.getenv('GITHUB_REF'))):
_check_and_set(build_info, "BUILD", "release")
else:
_check_and_set(build_info, "BUILD", "qa")
log.debug("build info: %s", ", ".join([f"{k}={v}" for k, v in build_info.items()]))
with open(build_info_mod.__file__, 'w') as f:
f.write("\n".join([f"{k} = \"{v}\"" for k, v in build_info.items()]) + "\n")
if __name__ == '__main__':
sys.exit(main())
================================================
FILE: docker/wallet_server_entrypoint.sh
================================================
#!/bin/bash
# entrypoint for wallet server Docker image
set -euo pipefail
SNAPSHOT_URL="${SNAPSHOT_URL:-}" #off by default. latest snapshot at https://lbry.com/snapshot/wallet
if [[ -n "$SNAPSHOT_URL" ]] && [[ ! -f /database/lbry-leveldb ]]; then
files="$(ls)"
echo "Downloading wallet snapshot from $SNAPSHOT_URL"
wget --no-verbose --trust-server-names --content-disposition "$SNAPSHOT_URL"
echo "Extracting snapshot..."
filename="$(grep -vf <(echo "$files") <(ls))" # finds the file that was not there before
case "$filename" in
*.tgz|*.tar.gz|*.tar.bz2 ) tar xvf "$filename" --directory /database ;;
*.zip ) unzip "$filename" -d /database ;;
* ) echo "Don't know how to extract ${filename}. SNAPSHOT COULD NOT BE LOADED" && exit 1 ;;
esac
rm "$filename"
fi
/home/lbry/.local/bin/lbry-hub-elastic-sync
echo 'starting server'
/home/lbry/.local/bin/lbry-hub "$@"
================================================
FILE: docker/webconf.yaml
================================================
allowed_origin: "*"
max_key_fee: "0.0 USD"
save_files: false
save_blobs: false
streaming_server: "0.0.0.0:5280"
api: "0.0.0.0:5279"
data_dir: /tmp
download_dir: /tmp
wallet_dir: /tmp
================================================
FILE: docs/api.json
================================================
{
"main": {
"doc": "Ungrouped commands.",
"commands": [
{
"name": "ffmpeg_find",
"description": "Get ffmpeg installation information",
"arguments": [],
"returns": "(dict) Dictionary of ffmpeg information\n {\n 'available': (bool) found ffmpeg,\n 'which': (str) path to ffmpeg,\n 'analyze_audio_volume': (bool) should ffmpeg analyze audio\n }",
"examples": []
},
{
"name": "get",
"description": "Download stream from a LBRY name.",
"arguments": [
{
"name": "uri",
"type": "str",
"description": "uri of the content to download",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "specified name for the downloaded file, overrides the stream file name",
"is_required": false
},
{
"name": "download_directory",
"type": "str",
"description": "full path to the directory to download into",
"is_required": false
},
{
"name": "timeout",
"type": "int",
"description": "download timeout in number of seconds",
"is_required": false
},
{
"name": "save_file",
"type": "bool",
"description": "save the file to the downloads directory",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "wallet to check for claim purchase receipts",
"is_required": false
}
],
"returns": " {\n \"streaming_url\": \"(str) url to stream the file using range requests\",\n \"completed\": \"(bool) true if download is completed\",\n \"file_name\": \"(str) name of file\",\n \"download_directory\": \"(str) download directory\",\n \"points_paid\": \"(float) credit paid to download file\",\n \"stopped\": \"(bool) true if download is stopped\",\n \"stream_hash\": \"(str) stream hash of file\",\n \"stream_name\": \"(str) stream name\",\n \"suggested_file_name\": \"(str) suggested file name\",\n \"sd_hash\": \"(str) sd hash of file\",\n \"download_path\": \"(str) download path of file\",\n \"mime_type\": \"(str) mime type of file\",\n \"key\": \"(str) key attached to file\",\n \"total_bytes_lower_bound\": \"(int) lower bound file size in bytes\",\n \"total_bytes\": \"(int) file upper bound size in bytes\",\n \"written_bytes\": \"(int) written size in bytes\",\n \"blobs_completed\": \"(int) number of fully downloaded blobs\",\n \"blobs_in_stream\": \"(int) total blobs on stream\",\n \"blobs_remaining\": \"(int) total blobs remaining to download\",\n \"status\": \"(str) downloader status\",\n \"claim_id\": \"(str) None if claim is not found else the claim id\",\n \"txid\": \"(str) None if claim is not found else the transaction id\",\n \"nout\": \"(int) None if claim is not found else the transaction output index\",\n \"outpoint\": \"(str) None if claim is not found else the tx and output\",\n \"metadata\": \"(dict) None if claim is not found else the claim metadata\",\n \"channel_claim_id\": \"(str) None if claim is not found or not signed\",\n \"channel_name\": \"(str) None if claim is not found or not signed\",\n \"claim_name\": \"(str) None if claim is not found else the claim name\",\n \"reflector_progress\": \"(int) reflector upload progress, 0 to 100\",\n \"uploading_to_reflector\": \"(bool) set to True when currently uploading to reflector\"\n }",
"examples": [
{
"title": "Get a file",
"curl": "curl -d'{\"method\": \"get\", \"params\": {\"uri\": \"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\"}}' http://localhost:5279/",
"lbrynet": "lbrynet get astream#ad25e05aa7dc5e9994869040c6103f9a8728db46",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"get\", \"params\": {\"uri\": \"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"added_on\": 1655141677,\n \"blobs_completed\": 1,\n \"blobs_in_stream\": 1,\n \"blobs_remaining\": 0,\n \"channel_claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"channel_name\": \"@channel\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_name\": \"astream\",\n \"completed\": true,\n \"confirmations\": 4,\n \"content_fee\": null,\n \"download_directory\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd\",\n \"download_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd/tmpr832hp1x\",\n \"file_name\": \"tmpr832hp1x\",\n \"height\": 214,\n \"is_fully_reflected\": false,\n \"key\": \"66f888fe00cf558494c2fcbd5903d00d\",\n \"metadata\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"mime_type\": \"application/octet-stream\",\n \"nout\": 0,\n \"outpoint\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234:0\",\n \"points_paid\": 0.0,\n \"protobuf\": \"01a2795714b0923650b7fa288618591f0c2f2e5c5961baf110460a527e38b68f0653c1c79f2cae1f57ade55c009fb7578175c9d93d7edb0546d1378e1d33d99df8df0d5cfe9e7a5b63053a4e7ce003f95f5890b27f0a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d707238333268703178180b22186170706c69636174696f6e2f6f637465742d73747265616d32309ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"purchase_receipt\": null,\n \"reflector_progress\": 0,\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"status\": \"finished\",\n \"stopped\": true,\n \"stream_hash\": \"c48ff9950efbcb78b20d311467b1b0e321069ef5ece96898eb08a27a62d1ef2594cd24f1e8d6993f00c48fd6a4221490\",\n \"stream_name\": \"tmpr832hp1x\",\n \"streaming_url\": \"http://localhost:5280/stream/9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"suggested_file_name\": \"tmpr832hp1x\",\n \"timestamp\": 1655141671,\n \"total_bytes\": 16,\n \"total_bytes_lower_bound\": 0,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"uploading_to_reflector\": false,\n \"written_bytes\": 11\n }\n}"
}
]
},
{
"name": "publish",
"description": "Create or replace a stream claim at a given name (use 'stream create/update' for more control).",
"arguments": [
{
"name": "name",
"type": "str",
"description": "name of the content (can only consist of a-z A-Z 0-9 and -(dash))",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": false
},
{
"name": "file_path",
"type": "str",
"description": "path to file to be associated with name.",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "name of file to be associated with stream.",
"is_required": false
},
{
"name": "file_hash",
"type": "str",
"description": "hash of file to be associated with stream.",
"is_required": false
},
{
"name": "validate_file",
"type": "bool",
"description": "validate that the video container and encodings match common web browser support or that optimization succeeds if specified. FFmpeg is required",
"is_required": false
},
{
"name": "optimize_file",
"type": "bool",
"description": "transcode the video & audio if necessary to ensure common web browser support. FFmpeg is required",
"is_required": false
},
{
"name": "fee_currency",
"type": "string",
"description": "specify fee currency",
"is_required": false
},
{
"name": "fee_amount",
"type": "decimal",
"description": "content download fee",
"is_required": false
},
{
"name": "fee_address",
"type": "str",
"description": "address where to send fee payments, will use value from --claim_address if not provided",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the publication",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the publication",
"is_required": false
},
{
"name": "author",
"type": "str",
"description": "author of the publication. The usage for this field is not the same as for channels. The author field is used to credit an author who is not the publisher and is not represented by the channel. For example, a pdf file of 'The Odyssey' has an author of 'Homer' but may by published to a channel such as '@classics', or to no channel at all",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "add content tags",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the channel, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations relevant to the stream, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "license",
"type": "str",
"description": "publication license",
"is_required": false
},
{
"name": "license_url",
"type": "str",
"description": "publication license url",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "release_time",
"type": "int",
"description": "original public release of content, seconds since UNIX epoch",
"is_required": false
},
{
"name": "width",
"type": "int",
"description": "image/video width, automatically calculated from media file",
"is_required": false
},
{
"name": "height",
"type": "int",
"description": "image/video height, automatically calculated from media file",
"is_required": false
},
{
"name": "duration",
"type": "int",
"description": "audio/video duration in seconds, automatically calculated",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "sd_hash of stream",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the publisher channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of publisher channel",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account to use for holding the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the claim is sent to, if not specified it will be determined automatically from the account",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Publish a file",
"curl": "curl -d'{\"method\": \"publish\", \"params\": {\"name\": \"a-new-stream\", \"bid\": \"1.0\", \"file_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmp1wt4ndjd\", \"validate_file\": false, \"optimize_file\": false, \"tags\": [], \"languages\": [], \"locations\": [], \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet publish a-new-stream --bid=1.0 --file_path=/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmp1wt4ndjd",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"publish\", \"params\": {\"name\": \"a-new-stream\", \"bid\": \"1.0\", \"file_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmp1wt4ndjd\", \"validate_file\": false, \"optimize_file\": false, \"tags\": [], \"languages\": [], \"locations\": [], \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"0100000001c701b5f207fe0b1abd990f0899a8c8bc4bf6c8ce07d0eafc8a59e44bce4f95a9010000006b483045022100fb66c628da4fb63d7f1ae98d3c2bd15b5e4ae4aa7863891b943989f92c1aed0902202362799eb6f3d1cd66bd230c6710fafa248ad48073a7070e1ba51d24b7c7f453012102f272fd093af2228160e96af70d809c329d9c547375b6d838498880f7c3051290ffffffff0200e1f50500000000bfb50c612d6e65772d73747265616d4c94000a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d70317774346e646a64180b22186170706c69636174696f6e2f6f637465742d73747265616d32304e61ee6b6a7810750982faa6c805323e33b5d21d2798d4add70c7588640ed8c5f70c822382be5fe87fd5ae356e26896e6d7576a9147ec6c9961e0d398d337699d566702f0ffe1ea07888ac54826311000000001976a914bf0468b0a4fd815b9a4bef7d7c3cd59a67e35a9b88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"n2qN5bEHEQ1keWyR6GbNd15dbHE6z1FoZb\",\n \"amount\": \"3.941448\",\n \"confirmations\": 3,\n \"height\": 217,\n \"nout\": 1,\n \"timestamp\": 1655141675,\n \"txid\": \"a9954fce4be4598afcead007cec8f64bbcc8a899080f99bd1a0bfe07f2b501c7\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"ms5HaDddaUyAAMczcdCbYuTFkuBdAgQxeY\",\n \"amount\": \"1.0\",\n \"claim_id\": \"006db49526758cf95dc2d9cd2354114e67441baa\",\n \"claim_op\": \"create\",\n \"confirmations\": -2,\n \"height\": -2,\n \"meta\": {},\n \"name\": \"a-new-stream\",\n \"normalized_name\": \"a-new-stream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://a-new-stream#006db49526758cf95dc2d9cd2354114e67441baa\",\n \"timestamp\": null,\n \"txid\": \"0c646996c6c30899690e4536037c21e1f2190b89be07af626b3b61f17c4fc431\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmp1wt4ndjd\",\n \"sd_hash\": \"4e61ee6b6a7810750982faa6c805323e33b5d21d2798d4add70c7588640ed8c5f70c822382be5fe87fd5ae356e26896e\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"mxvxe2qg1xUdSoMWFch86uZKS5X3t2Sj3t\",\n \"amount\": \"2.917341\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"0c646996c6c30899690e4536037c21e1f2190b89be07af626b3b61f17c4fc431\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.024107\",\n \"total_input\": \"3.941448\",\n \"total_output\": \"3.917341\",\n \"txid\": \"0c646996c6c30899690e4536037c21e1f2190b89be07af626b3b61f17c4fc431\"\n }\n}"
}
]
},
{
"name": "resolve",
"description": "Get the claim that a URL refers to.",
"arguments": [
{
"name": "urls",
"type": "str, list",
"description": "one or more urls to resolve",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "wallet to check for claim purchase receipts",
"is_required": false
},
{
"name": "new_sdk_server",
"type": "str",
"description": "URL of the new SDK server (EXPERIMENTAL)",
"is_required": false
},
{
"name": "include_purchase_receipt",
"type": "bool",
"description": "lookup and include a receipt if this wallet has purchased the claim being resolved",
"is_required": false
},
{
"name": "include_is_my_output",
"type": "bool",
"description": "lookup and include a boolean indicating if claim being resolved is yours",
"is_required": false
},
{
"name": "include_sent_supports",
"type": "bool",
"description": "lookup and sum the total amount of supports you've made to this claim",
"is_required": false
},
{
"name": "include_sent_tips",
"type": "bool",
"description": "lookup and sum the total amount of tips you've made to this claim (only makes sense when claim is not yours)",
"is_required": false
},
{
"name": "include_received_tips",
"type": "bool",
"description": "lookup and sum the total amount of tips you've received to this claim (only makes sense when claim is yours)",
"is_required": false
}
],
"returns": "Dictionary of results, keyed by url\n '': {\n If a resolution error occurs:\n 'error': Error message\n\n If the url resolves to a channel or a claim in a channel:\n 'certificate': {\n 'address': (str) claim address,\n 'amount': (float) claim amount,\n 'effective_amount': (float) claim amount including supports,\n 'claim_id': (str) claim id,\n 'claim_sequence': (int) claim sequence number (or -1 if unknown),\n 'decoded_claim': (bool) whether or not the claim value was decoded,\n 'height': (int) claim height,\n 'confirmations': (int) claim depth,\n 'timestamp': (int) timestamp of the block that included this claim tx,\n 'has_signature': (bool) included if decoded_claim\n 'name': (str) claim name,\n 'permanent_url': (str) permanent url of the certificate claim,\n 'supports: (list) list of supports [{'txid': (str) txid,\n 'nout': (int) nout,\n 'amount': (float) amount}],\n 'txid': (str) claim txid,\n 'nout': (str) claim nout,\n 'signature_is_valid': (bool), included if has_signature,\n 'value': ClaimDict if decoded, otherwise hex string\n }\n\n If the url resolves to a channel:\n 'claims_in_channel': (int) number of claims in the channel,\n\n If the url resolves to a claim:\n 'claim': {\n 'address': (str) claim address,\n 'amount': (float) claim amount,\n 'effective_amount': (float) claim amount including supports,\n 'claim_id': (str) claim id,\n 'claim_sequence': (int) claim sequence number (or -1 if unknown),\n 'decoded_claim': (bool) whether or not the claim value was decoded,\n 'height': (int) claim height,\n 'depth': (int) claim depth,\n 'has_signature': (bool) included if decoded_claim\n 'name': (str) claim name,\n 'permanent_url': (str) permanent url of the claim,\n 'channel_name': (str) channel name if claim is in a channel\n 'supports: (list) list of supports [{'txid': (str) txid,\n 'nout': (int) nout,\n 'amount': (float) amount}]\n 'txid': (str) claim txid,\n 'nout': (str) claim nout,\n 'signature_is_valid': (bool), included if has_signature,\n 'value': ClaimDict if decoded, otherwise hex string\n }\n }",
"examples": [
{
"title": "Resolve a claim",
"curl": "curl -d'{\"method\": \"resolve\", \"params\": {\"urls\": [\"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\"], \"include_purchase_receipt\": false, \"include_is_my_output\": false, \"include_sent_supports\": false, \"include_sent_tips\": false, \"include_received_tips\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet resolve astream#ad25e05aa7dc5e9994869040c6103f9a8728db46",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"resolve\", \"params\": {\"urls\": [\"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\"], \"include_purchase_receipt\": false, \"include_is_my_output\": false, \"include_sent_supports\": false, \"include_sent_tips\": false, \"include_received_tips\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\": {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5/astream#a\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 4,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"meta\": {\n \"activation_height\": 214,\n \"creation_height\": 213,\n \"creation_timestamp\": 1655141671,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 714,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 213\n },\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"short_url\": \"lbry://astream#a\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 8,\n \"has_signing_key\": false,\n \"height\": 210,\n \"meta\": {\n \"activation_height\": 210,\n \"claims_in_channel\": 2,\n \"creation_height\": 209,\n \"creation_timestamp\": 1655141670,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 710,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 209\n },\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"short_url\": \"lbry://@channel#5\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n }\n}"
}
]
},
{
"name": "routing_table_get",
"description": "Get DHT routing information",
"arguments": [],
"returns": "(dict) dictionary containing routing and peer information\n {\n \"buckets\": {\n : [\n {\n \"address\": (str) peer address,\n \"udp_port\": (int) peer udp port,\n \"tcp_port\": (int) peer tcp port,\n \"node_id\": (str) peer node id,\n }\n ]\n },\n \"node_id\": (str) the local dht node id\n \"prefix_neighbors_count\": (int) the amount of peers sharing the same byte prefix of the local node id\n }",
"examples": []
},
{
"name": "status",
"description": "Get daemon status",
"arguments": [],
"returns": "(dict) lbrynet-daemon status\n {\n 'installation_id': (str) installation id - base58,\n 'is_running': (bool),\n 'skipped_components': (list) [names of skipped components (str)],\n 'startup_status': { Does not include components which have been skipped\n 'blob_manager': (bool),\n 'blockchain_headers': (bool),\n 'database': (bool),\n 'dht': (bool),\n 'exchange_rate_manager': (bool),\n 'hash_announcer': (bool),\n 'peer_protocol_server': (bool),\n 'file_manager': (bool),\n 'libtorrent_component': (bool),\n 'upnp': (bool),\n 'wallet': (bool),\n },\n 'connection_status': {\n 'code': (str) connection status code,\n 'message': (str) connection status message\n },\n 'blockchain_headers': {\n 'downloading_headers': (bool),\n 'download_progress': (float) 0-100.0\n },\n 'wallet': {\n 'connected': (str) host and port of the connected spv server,\n 'blocks': (int) local blockchain height,\n 'blocks_behind': (int) remote_height - local_height,\n 'best_blockhash': (str) block hash of most recent block,\n 'is_encrypted': (bool),\n 'is_locked': (bool),\n 'connected_servers': (list) [\n {\n 'host': (str) server hostname,\n 'port': (int) server port,\n 'latency': (int) milliseconds\n }\n ],\n },\n 'libtorrent_component': {\n 'running': (bool) libtorrent was detected and started successfully,\n },\n 'dht': {\n 'node_id': (str) lbry dht node id - hex encoded,\n 'peers_in_routing_table': (int) the number of peers in the routing table,\n },\n 'blob_manager': {\n 'finished_blobs': (int) number of finished blobs in the blob manager,\n 'connections': {\n 'incoming_bps': {\n : (int) bytes per second received,\n },\n 'outgoing_bps': {\n : (int) bytes per second sent,\n },\n 'total_outgoing_mps': (float) megabytes per second sent,\n 'total_incoming_mps': (float) megabytes per second received,\n 'max_outgoing_mbs': (float) maximum bandwidth (megabytes per second) sent, since the\n daemon was started\n 'max_incoming_mbs': (float) maximum bandwidth (megabytes per second) received, since the\n daemon was started\n 'total_sent' : (int) total number of bytes sent since the daemon was started\n 'total_received' : (int) total number of bytes received since the daemon was started\n }\n },\n 'hash_announcer': {\n 'announce_queue_size': (int) number of blobs currently queued to be announced\n },\n 'file_manager': {\n 'managed_files': (int) count of files in the stream manager,\n },\n 'upnp': {\n 'aioupnp_version': (str),\n 'redirects': {\n : (int) external_port,\n },\n 'gateway': (str) manufacturer and model,\n 'dht_redirect_set': (bool),\n 'peer_redirect_set': (bool),\n 'external_ip': (str) external ip address,\n }\n }",
"examples": [
{
"title": "Get status",
"curl": "curl -d'{\"method\": \"status\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet status",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"status\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"background_downloader\": {\n \"available_free_space_mb\": null,\n \"ongoing_download\": false,\n \"running\": false\n },\n \"blob_manager\": {\n \"connections\": {\n \"incoming_bps\": {},\n \"max_incoming_mbs\": 0.0,\n \"max_outgoing_mbs\": 0.0,\n \"outgoing_bps\": {},\n \"total_incoming_mbs\": 0.0,\n \"total_outgoing_mbs\": 0.0,\n \"total_received\": 0,\n \"total_sent\": 0\n },\n \"finished_blobs\": 0\n },\n \"disk_space\": {\n \"content_blobs_storage_used_mb\": 0,\n \"published_blobs_storage_used_mb\": 0,\n \"running\": true,\n \"seed_blobs_storage_used_mb\": 0,\n \"total_used_mb\": 0\n },\n \"ffmpeg_status\": {\n \"analyze_audio_volume\": true,\n \"available\": true,\n \"which\": \"/opt/homebrew/bin/ffmpeg\"\n },\n \"file_manager\": {\n \"managed_files\": 0\n },\n \"installation_id\": \"3a1UGocRVHs9fAYWhNTRGoq2mpi97NqGEbkofcVkZvJei5xqimckTxfDRdjuKt4Hr6\",\n \"is_running\": true,\n \"skipped_components\": [\n \"dht\",\n \"upnp\",\n \"hash_announcer\",\n \"peer_protocol_server\",\n \"libtorrent_component\"\n ],\n \"startup_status\": {\n \"background_downloader\": true,\n \"blob_manager\": true,\n \"database\": true,\n \"disk_space\": true,\n \"exchange_rate_manager\": true,\n \"file_manager\": true,\n \"tracker_announcer_component\": true,\n \"wallet\": true,\n \"wallet_server_payments\": true\n },\n \"wallet\": {\n \"available_servers\": 1,\n \"best_blockhash\": \"47eb373669435f62580b2855670788eaf07e62551c2d38361e24700f130cab36\",\n \"blocks\": 206,\n \"blocks_behind\": 0,\n \"connected\": \"localhost:50002\",\n \"connected_features\": {\n \"daily_fee\": \"0\",\n \"description\": \"\",\n \"donation_address\": \"\",\n \"genesis_hash\": \"6e3fcf1299d4ec5d79c3a4c91d624a4acf9e2e173d95a1a0504f677669687556\",\n \"hash_function\": \"sha256\",\n \"hosts\": {},\n \"payment_address\": \"\",\n \"protocol_max\": \"0.199.0\",\n \"protocol_min\": \"0.54.0\",\n \"pruning\": null,\n \"server_version\": \"0.107.0\",\n \"trending_algorithm\": \"fast_ar\"\n },\n \"headers_synchronization_progress\": 100,\n \"known_servers\": 0,\n \"servers\": [\n {\n \"availability\": true,\n \"host\": \"localhost\",\n \"latency\": 0.00792012499999828,\n \"port\": 50002\n }\n ]\n },\n \"wallet_server_payments\": {\n \"max_fee\": \"0.0\",\n \"running\": false\n }\n }\n}"
}
]
},
{
"name": "stop",
"description": "Stop lbrynet API server.",
"arguments": [],
"returns": "(string) Shutdown message",
"examples": []
},
{
"name": "version",
"description": "Get lbrynet API server version information",
"arguments": [],
"returns": "(dict) Dictionary of lbry version information\n {\n 'processor': (str) processor type,\n 'python_version': (str) python version,\n 'platform': (str) platform string,\n 'os_release': (str) os release string,\n 'os_system': (str) os name,\n 'version': (str) lbrynet version,\n 'build': (str) \"dev\" | \"qa\" | \"rc\" | \"release\",\n }",
"examples": [
{
"title": "Get version",
"curl": "curl -d'{\"method\": \"version\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet version",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"version\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"build\": \"dev\",\n \"lbrynet_version\": \"0.109.0\",\n \"os_release\": \"21.5.0\",\n \"os_system\": \"Darwin\",\n \"platform\": \"Darwin-21.5.0-x86_64-i386-64bit\",\n \"processor\": \"i386\",\n \"python_version\": \"3.7.13\",\n \"version\": \"0.109.0\"\n }\n}"
}
]
}
]
},
"account": {
"doc": "Create, modify and inspect wallet accounts.",
"commands": [
{
"name": "account_add",
"description": "Add a previously created account from a seed, private key or public key (read-only).\nSpecify --single_key for single address or vanity address accounts.",
"arguments": [
{
"name": "account_name",
"type": "str",
"description": "name of the account to add",
"is_required": true
},
{
"name": "seed",
"type": "str",
"description": "seed to generate new account from",
"is_required": false
},
{
"name": "private_key",
"type": "str",
"description": "private key for new account",
"is_required": false
},
{
"name": "public_key",
"type": "str",
"description": "public key for new account",
"is_required": false
},
{
"name": "single_key",
"type": "bool",
"description": "create single key account, default is multi-key",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": " {\n \"id\": \"account_id\",\n \"is_default\": \"this account is used by default\",\n \"ledger\": \"name of crypto currency and network\",\n \"name\": \"optional account name\",\n \"seed\": \"human friendly words from which account can be recreated\",\n \"encrypted\": \"if account is encrypted\",\n \"private_key\": \"extended private key\",\n \"public_key\": \"extended public key\",\n \"address_generator\": \"settings for generating addresses\",\n \"modified_on\": \"date of last modification to account settings\"\n }",
"examples": [
{
"title": "Add an account from seed",
"curl": "curl -d'{\"method\": \"account_add\", \"params\": {\"account_name\": \"new account\", \"seed\": \"miss ready crop oval canyon such sing powder figure math noodle style\", \"single_key\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet account add \"new account\" --seed=\"miss ready crop oval canyon such sing powder figure math noodle style\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_add\", \"params\": {\"account_name\": \"new account\", \"seed\": \"miss ready crop oval canyon such sing powder figure math noodle style\", \"single_key\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"address_generator\": {\n \"change\": {\n \"gap\": 6,\n \"maximum_uses_per_address\": 1\n },\n \"name\": \"deterministic-chain\",\n \"receiving\": {\n \"gap\": 20,\n \"maximum_uses_per_address\": 1\n }\n },\n \"encrypted\": false,\n \"id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"is_default\": false,\n \"ledger\": \"lbc_regtest\",\n \"modified_on\": 1655141657,\n \"name\": \"new account\",\n \"private_key\": \"tprv8ZgxMBicQKsPdmLUTJtRsfwAeL527nfpJE1GhFFLzKtbuuzwYorQyq8guwgzFCQkqs8EuR2MGLkbh3xuEb1pFuAjx1Web8Vk3RXrrLVKwRU\",\n \"public_key\": \"tpubD6NzVbkrYhZ4XENGLxZ2H5bHDMaxH7risXc3ymHeQbgzkQFiBCg1AKkZ66UqGohv1bXg5579Z3r4omYeqMEd1Khhyuf4dssfmBzfiEce23o\",\n \"seed\": \"miss ready crop oval canyon such sing powder figure math noodle style\"\n }\n}"
}
]
},
{
"name": "account_balance",
"description": "Return the balance of an account",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "If provided only the balance for this account will be given. Otherwise default account.",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "balance for specific wallet",
"is_required": false
},
{
"name": "confirmations",
"type": "int",
"description": "Only include transactions with this many confirmed blocks.",
"is_required": false
}
],
"returns": "(decimal) amount of lbry credits in wallet",
"examples": [
{
"title": "Get default account balance",
"curl": "curl -d'{\"method\": \"account_balance\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet account balance",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_balance\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"available\": \"7.999876\",\n \"reserved\": \"0.0\",\n \"reserved_subtotals\": {\n \"claims\": \"0.0\",\n \"supports\": \"0.0\",\n \"tips\": \"0.0\"\n },\n \"total\": \"7.999876\"\n }\n}"
},
{
"title": "Get balance for specific account by id",
"curl": "curl -d'{\"method\": \"account_balance\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"}}' http://localhost:5279/",
"lbrynet": "lbrynet account balance \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_balance\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"available\": \"2.0\",\n \"reserved\": \"0.0\",\n \"reserved_subtotals\": {\n \"claims\": \"0.0\",\n \"supports\": \"0.0\",\n \"tips\": \"0.0\"\n },\n \"total\": \"2.0\"\n }\n}"
}
]
},
{
"name": "account_create",
"description": "Create a new account. Specify --single_key if you want to use\nthe same address for all transactions (not recommended).",
"arguments": [
{
"name": "account_name",
"type": "str",
"description": "name of the account to create",
"is_required": true
},
{
"name": "single_key",
"type": "bool",
"description": "create single key account, default is multi-key",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": " {\n \"id\": \"account_id\",\n \"is_default\": \"this account is used by default\",\n \"ledger\": \"name of crypto currency and network\",\n \"name\": \"optional account name\",\n \"seed\": \"human friendly words from which account can be recreated\",\n \"encrypted\": \"if account is encrypted\",\n \"private_key\": \"extended private key\",\n \"public_key\": \"extended public key\",\n \"address_generator\": \"settings for generating addresses\",\n \"modified_on\": \"date of last modification to account settings\"\n }",
"examples": [
{
"title": "Create an account",
"curl": "curl -d'{\"method\": \"account_create\", \"params\": {\"account_name\": \"generated account\", \"single_key\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet account create \"generated account\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_create\", \"params\": {\"account_name\": \"generated account\", \"single_key\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"address_generator\": {\n \"change\": {\n \"gap\": 6,\n \"maximum_uses_per_address\": 1\n },\n \"name\": \"deterministic-chain\",\n \"receiving\": {\n \"gap\": 20,\n \"maximum_uses_per_address\": 1\n }\n },\n \"encrypted\": false,\n \"id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"is_default\": false,\n \"ledger\": \"lbc_regtest\",\n \"modified_on\": 1655141657,\n \"name\": \"generated account\",\n \"private_key\": \"tprv8ZgxMBicQKsPdmLUTJtRsfwAeL527nfpJE1GhFFLzKtbuuzwYorQyq8guwgzFCQkqs8EuR2MGLkbh3xuEb1pFuAjx1Web8Vk3RXrrLVKwRU\",\n \"public_key\": \"tpubD6NzVbkrYhZ4XENGLxZ2H5bHDMaxH7risXc3ymHeQbgzkQFiBCg1AKkZ66UqGohv1bXg5579Z3r4omYeqMEd1Khhyuf4dssfmBzfiEce23o\",\n \"seed\": \"miss ready crop oval canyon such sing powder figure math noodle style\"\n }\n}"
}
]
},
{
"name": "account_deposit",
"description": "Spend a time locked transaction into your account.",
"arguments": [
{
"name": "txid",
"type": "str",
"description": "id of the transaction",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "output number in the transaction",
"is_required": false
},
{
"name": "redeem_script",
"type": "str",
"description": "redeem script for output",
"is_required": false
},
{
"name": "private_key",
"type": "str",
"description": "private key to sign transaction",
"is_required": false
},
{
"name": "to_account",
"type": "str",
"description": "deposit to this account",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "limit operation to specific wallet.",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until tx has synced",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "account_fund",
"description": "Transfer some amount (or --everything) to an account from another\naccount (can be the same account). Amounts are interpreted as LBC.\nYou can also spread the transfer across a number of --outputs (cannot\nbe used together with --everything).",
"arguments": [
{
"name": "to_account",
"type": "str",
"description": "send to this account",
"is_required": false
},
{
"name": "from_account",
"type": "str",
"description": "spend from this account",
"is_required": false
},
{
"name": "amount",
"type": "decimal",
"description": "the amount to transfer lbc",
"is_required": true
},
{
"name": "everything",
"type": "bool",
"description": "transfer everything (excluding claims), default: false.",
"is_required": false
},
{
"name": "outputs",
"type": "int",
"description": "split payment across many outputs, default: 1.",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "limit operation to specific wallet.",
"is_required": false
},
{
"name": "broadcast",
"type": "bool",
"description": "actually broadcast the transaction, default: false.",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Transfer 2 LBC from default account to specific account",
"curl": "curl -d'{\"method\": \"account_fund\", \"params\": {\"to_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"amount\": \"2.0\", \"everything\": false, \"broadcast\": true}}' http://localhost:5279/",
"lbrynet": "lbrynet account fund --to_account=\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\" --amount=2.0 --broadcast",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_fund\", \"params\": {\"to_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"amount\": \"2.0\", \"everything\": false, \"broadcast\": true}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"01000000012a489c130efda054091c1e91a258ad57772619a9b42ab5e416768e32334e581c000000006a47304402207fd2ffd9fe8363604a428161ace5ad1bb3bab156e9746dcf4d9b784e90abd8c6022024766cb49008eeac5b963a97de0d63611e8a5d7299776eab95598514b18ad1da0121028a467e50cb5555cc3d4ce66944ffbf5566cfe7b8a553e3bf40d4d214d4cb5233ffffffff0200c2eb0b000000001976a9140e92a8bca34238d407745d94fc5b21f0e39f518888ac90d7ae2f000000001976a914169f2f9502bfb8ee9c1d97c315ef13672721638b88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mqwYxGxBhfFodPpkCDGXK3eiSHpfRPi6SD\",\n \"amount\": \"10.0\",\n \"confirmations\": 6,\n \"height\": 201,\n \"nout\": 0,\n \"timestamp\": 1655141669,\n \"txid\": \"1c584e33328e7616e4b52ab4a919267757ad58a2911e1c0954a0fd0e139c482a\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mgr1So3484shNr8kNMsavaohFwwRm9AnFU\",\n \"amount\": \"2.0\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 0,\n \"timestamp\": null,\n \"txid\": \"f03159770e23cab4da7779dcf1a6809f6f518c61ab540d6a452d0bc5509874b8\",\n \"type\": \"payment\"\n },\n {\n \"address\": \"mhaZrhgfvGv49E47oDupq8wBppSNfhMFam\",\n \"amount\": \"7.999876\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"f03159770e23cab4da7779dcf1a6809f6f518c61ab540d6a452d0bc5509874b8\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.000124\",\n \"total_input\": \"10.0\",\n \"total_output\": \"9.999876\",\n \"txid\": \"f03159770e23cab4da7779dcf1a6809f6f518c61ab540d6a452d0bc5509874b8\"\n }\n}"
},
{
"title": "Spread LBC between multiple addresses",
"curl": "curl -d'{\"method\": \"account_fund\", \"params\": {\"to_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"from_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"amount\": \"1.5\", \"everything\": false, \"outputs\": 2, \"broadcast\": true}}' http://localhost:5279/",
"lbrynet": "lbrynet account fund --to_account=\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\" --from_account=\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\" --amount=1.5 --outputs=2 --broadcast",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_fund\", \"params\": {\"to_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"from_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"amount\": \"1.5\", \"everything\": false, \"outputs\": 2, \"broadcast\": true}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"0100000001b8749850c50b2d456a0d54ab618c516f9f80a6f1dc7977dab4ca230e775931f0000000006b48304502210087b512d6b2772190e417f1b4252c0905d09c98eab3a229e3a3f2ab3bda11f2c7022030d2be08ee86c4b1e3899cebea7e1a5b68c74022502e57cc7b141c083fac45b401210232b7e745cbafb7b3506b71afe38ba9a1c049aec1426c3e5442b246604a14c5f6ffffffff03c0687804000000001976a9144f694178c5f66475002876866115a8a74177fcba88acc0687804000000001976a9144f694178c5f66475002876866115a8a74177fcba88ac6cb9fa02000000001976a9140e92a8bca34238d407745d94fc5b21f0e39f518888ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mgr1So3484shNr8kNMsavaohFwwRm9AnFU\",\n \"amount\": \"2.0\",\n \"confirmations\": 1,\n \"height\": 207,\n \"nout\": 0,\n \"timestamp\": 1655141670,\n \"txid\": \"f03159770e23cab4da7779dcf1a6809f6f518c61ab540d6a452d0bc5509874b8\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mnkqmWKPUwkeDuq7pndpfrzXFQDmH9QTBo\",\n \"amount\": \"0.75\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 0,\n \"timestamp\": null,\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\",\n \"type\": \"payment\"\n },\n {\n \"address\": \"mnkqmWKPUwkeDuq7pndpfrzXFQDmH9QTBo\",\n \"amount\": \"0.75\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\",\n \"type\": \"payment\"\n },\n {\n \"address\": \"mgr1So3484shNr8kNMsavaohFwwRm9AnFU\",\n \"amount\": \"0.499859\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 2,\n \"timestamp\": null,\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.000141\",\n \"total_input\": \"2.0\",\n \"total_output\": \"1.999859\",\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\"\n }\n}"
},
{
"title": "Transfer all LBC to a specified account",
"curl": "curl -d'{\"method\": \"account_fund\", \"params\": {\"from_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"everything\": true, \"broadcast\": true}}' http://localhost:5279/",
"lbrynet": "lbrynet account fund --from_account=\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\" --everything --broadcast",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_fund\", \"params\": {\"from_account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"everything\": true, \"broadcast\": true}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"0100000003615517b8ac960bf2dddcb710b5ab9d10ad4775706c5f6e2bde911de71b06aeb1000000006a473044022070ccee08cbef735f798b9b29f5fc0f3e6541c4c32178b8ef5d8f45c3789e29ff02203df22817e856760c6453e7c869832fa385ab5284e8f8bd97815ded9f2b2a14bb012103087f88b56b3724117f595eabb5ac5282b9e73842f11c47ad1d20de6d856e72b5ffffffff615517b8ac960bf2dddcb710b5ab9d10ad4775706c5f6e2bde911de71b06aeb1010000006b483045022100d183c50ecdff7371e9a411e0252bab59d3b55984ddfc0b8adaa32087b67114a8022020003d8932c41cd71006e00eb346fa624d64b634c45ff12b8c1b3a4fd8b38a58012103087f88b56b3724117f595eabb5ac5282b9e73842f11c47ad1d20de6d856e72b5ffffffff615517b8ac960bf2dddcb710b5ab9d10ad4775706c5f6e2bde911de71b06aeb1020000006a473044022072d277ab8cdbcc3bbe4ffdcf6e34c55ab0de334f5ffbb3417690ac6e6cfea20d02206302c2026251b84b420427baaf91edcd93ab26264aff676816bbc80b70e05c3301210232b7e745cbafb7b3506b71afe38ba9a1c049aec1426c3e5442b246604a14c5f6ffffffff015027eb0b000000001976a914d90aac550e652c485f8c75585fa9bdf7b56c0f0c88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mnkqmWKPUwkeDuq7pndpfrzXFQDmH9QTBo\",\n \"amount\": \"0.75\",\n \"confirmations\": 1,\n \"height\": 208,\n \"nout\": 0,\n \"timestamp\": 1655141670,\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\",\n \"type\": \"payment\"\n },\n {\n \"address\": \"mnkqmWKPUwkeDuq7pndpfrzXFQDmH9QTBo\",\n \"amount\": \"0.75\",\n \"confirmations\": 1,\n \"height\": 208,\n \"nout\": 1,\n \"timestamp\": 1655141670,\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\",\n \"type\": \"payment\"\n },\n {\n \"address\": \"mgr1So3484shNr8kNMsavaohFwwRm9AnFU\",\n \"amount\": \"0.499859\",\n \"confirmations\": 1,\n \"height\": 208,\n \"nout\": 2,\n \"timestamp\": 1655141670,\n \"txid\": \"b1ae061be71d91de2b6e5f6c707547ad109dabb510b7dcddf20b96acb8175561\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"n1JZiGPzhiFUPTysS5PABHjdiRqfWNMSaX\",\n \"amount\": \"1.999604\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 0,\n \"timestamp\": null,\n \"txid\": \"40747da878033aa34fa25310b12e9625d820cfb9be384254863c59ed6bc3c0e8\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.000255\",\n \"total_input\": \"1.999859\",\n \"total_output\": \"1.999604\",\n \"txid\": \"40747da878033aa34fa25310b12e9625d820cfb9be384254863c59ed6bc3c0e8\"\n }\n}"
}
]
},
{
"name": "account_list",
"description": "List details of all of the accounts or a specific account.",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "If provided only the balance for this account will be given",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "accounts in specific wallet",
"is_required": false
},
{
"name": "confirmations",
"type": "int",
"description": "required confirmations (default: 0)",
"is_required": false
},
{
"name": "include_claims",
"type": "bool",
"description": "include claims, requires than a LBC account is specified (default: false)",
"is_required": false
},
{
"name": "show_seed",
"type": "bool",
"description": "show the seed for the account",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"id\": \"account_id\",\n \"is_default\": \"this account is used by default\",\n \"ledger\": \"name of crypto currency and network\",\n \"name\": \"optional account name\",\n \"seed\": \"human friendly words from which account can be recreated\",\n \"encrypted\": \"if account is encrypted\",\n \"private_key\": \"extended private key\",\n \"public_key\": \"extended public key\",\n \"address_generator\": \"settings for generating addresses\",\n \"modified_on\": \"date of last modification to account settings\"\n }\n ]\n }",
"examples": [
{
"title": "List your accounts",
"curl": "curl -d'{\"method\": \"account_list\", \"params\": {\"include_claims\": false, \"show_seed\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet account list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_list\", \"params\": {\"include_claims\": false, \"show_seed\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address_generator\": {\n \"change\": {\n \"gap\": 6,\n \"maximum_uses_per_address\": 1\n },\n \"name\": \"deterministic-chain\",\n \"receiving\": {\n \"gap\": 20,\n \"maximum_uses_per_address\": 1\n }\n },\n \"certificates\": 0,\n \"coins\": 10.0,\n \"encrypted\": false,\n \"id\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"is_default\": true,\n \"ledger\": \"lbc_regtest\",\n \"name\": \"Account #mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"public_key\": \"tpubD6NzVbkrYhZ4Yj1pAFUmbQ8bvpo4d5aQyWUzVPpagEiAddqLjxzdvfzpxEinbB2JRXCLkHJ8kSSz4scPnw3rnQeYLmXEsr9cXkQLV4ZjwUh\",\n \"satoshis\": 1000000000\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "account_max_address_gap",
"description": "Finds ranges of consecutive addresses that are unused and returns the length\nof the longest such range: for change and receiving address chains. This is\nuseful to figure out ideal values to set for 'receiving_gap' and 'change_gap'\naccount settings.",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "account for which to get max gaps",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(map) maximum gap for change and receiving addresses",
"examples": []
},
{
"name": "account_remove",
"description": "Remove an existing account.",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "id of the account to remove",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": " {\n \"id\": \"account_id\",\n \"is_default\": \"this account is used by default\",\n \"ledger\": \"name of crypto currency and network\",\n \"name\": \"optional account name\",\n \"seed\": \"human friendly words from which account can be recreated\",\n \"encrypted\": \"if account is encrypted\",\n \"private_key\": \"extended private key\",\n \"public_key\": \"extended public key\",\n \"address_generator\": \"settings for generating addresses\",\n \"modified_on\": \"date of last modification to account settings\"\n }",
"examples": [
{
"title": "Remove an account",
"curl": "curl -d'{\"method\": \"account_remove\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"}}' http://localhost:5279/",
"lbrynet": "lbrynet account remove mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_remove\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"address_generator\": {\n \"change\": {\n \"gap\": 6,\n \"maximum_uses_per_address\": 1\n },\n \"name\": \"deterministic-chain\",\n \"receiving\": {\n \"gap\": 20,\n \"maximum_uses_per_address\": 1\n }\n },\n \"encrypted\": false,\n \"id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"is_default\": false,\n \"ledger\": \"lbc_regtest\",\n \"modified_on\": 1655141657,\n \"name\": \"generated account\",\n \"private_key\": \"tprv8ZgxMBicQKsPdmLUTJtRsfwAeL527nfpJE1GhFFLzKtbuuzwYorQyq8guwgzFCQkqs8EuR2MGLkbh3xuEb1pFuAjx1Web8Vk3RXrrLVKwRU\",\n \"public_key\": \"tpubD6NzVbkrYhZ4XENGLxZ2H5bHDMaxH7risXc3ymHeQbgzkQFiBCg1AKkZ66UqGohv1bXg5579Z3r4omYeqMEd1Khhyuf4dssfmBzfiEce23o\",\n \"seed\": \"miss ready crop oval canyon such sing powder figure math noodle style\"\n }\n}"
}
]
},
{
"name": "account_send",
"description": "Send the same number of credits to multiple addresses from a specific account (or default account).",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "account to fund the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until tx has synced",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "account_set",
"description": "Change various settings on an account.",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "id of the account to change",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "default",
"type": "bool",
"description": "make this account the default",
"is_required": false
},
{
"name": "new_name",
"type": "str",
"description": "new name for the account",
"is_required": false
},
{
"name": "receiving_gap",
"type": "int",
"description": "set the gap for receiving addresses",
"is_required": false
},
{
"name": "receiving_max_uses",
"type": "int",
"description": "set the maximum number of times to use a receiving address",
"is_required": false
},
{
"name": "change_gap",
"type": "int",
"description": "set the gap for change addresses",
"is_required": false
},
{
"name": "change_max_uses",
"type": "int",
"description": "set the maximum number of times to use a change address",
"is_required": false
}
],
"returns": " {\n \"id\": \"account_id\",\n \"is_default\": \"this account is used by default\",\n \"ledger\": \"name of crypto currency and network\",\n \"name\": \"optional account name\",\n \"seed\": \"human friendly words from which account can be recreated\",\n \"encrypted\": \"if account is encrypted\",\n \"private_key\": \"extended private key\",\n \"public_key\": \"extended public key\",\n \"address_generator\": \"settings for generating addresses\",\n \"modified_on\": \"date of last modification to account settings\"\n }",
"examples": [
{
"title": "Modify maximum number of times a change address can be reused",
"curl": "curl -d'{\"method\": \"account_set\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"default\": false, \"change_max_uses\": 10}}' http://localhost:5279/",
"lbrynet": "lbrynet account set mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn --change_max_uses=10",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"account_set\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\", \"default\": false, \"change_max_uses\": 10}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"address_generator\": {\n \"change\": {\n \"gap\": 6,\n \"maximum_uses_per_address\": 10\n },\n \"name\": \"deterministic-chain\",\n \"receiving\": {\n \"gap\": 20,\n \"maximum_uses_per_address\": 1\n }\n },\n \"encrypted\": false,\n \"id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"is_default\": false,\n \"ledger\": \"lbc_regtest\",\n \"modified_on\": 1655141657,\n \"name\": \"new account\",\n \"private_key\": \"tprv8ZgxMBicQKsPdmLUTJtRsfwAeL527nfpJE1GhFFLzKtbuuzwYorQyq8guwgzFCQkqs8EuR2MGLkbh3xuEb1pFuAjx1Web8Vk3RXrrLVKwRU\",\n \"public_key\": \"tpubD6NzVbkrYhZ4XENGLxZ2H5bHDMaxH7risXc3ymHeQbgzkQFiBCg1AKkZ66UqGohv1bXg5579Z3r4omYeqMEd1Khhyuf4dssfmBzfiEce23o\",\n \"seed\": \"miss ready crop oval canyon such sing powder figure math noodle style\"\n }\n}"
}
]
}
]
},
"address": {
"doc": "List, generate and verify addresses.",
"commands": [
{
"name": "address_is_mine",
"description": "Checks if an address is associated with the current wallet.",
"arguments": [
{
"name": "address",
"type": "str",
"description": "address to check",
"is_required": true
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(bool) true, if address is associated with current wallet",
"examples": [
{
"title": "Check if address is mine",
"curl": "curl -d'{\"method\": \"address_is_mine\", \"params\": {\"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\"}}' http://localhost:5279/",
"lbrynet": "lbrynet address is_mine muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"address_is_mine\", \"params\": {\"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": true\n}"
}
]
},
{
"name": "address_list",
"description": "List account addresses or details of single address.",
"arguments": [
{
"name": "address",
"type": "str",
"description": "just show details for single address",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n \"an address in base58\"\n ]\n }",
"examples": [
{
"title": "List addresses in default account",
"curl": "curl -d'{\"method\": \"address_list\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet address list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"address_list\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mhSwvz7Qfuh343S8WrXpPoPcaxbtw7QM5W\",\n \"pubkey\": \"tpubDA9GDAntyJu4NnUS2eNJcNYev2VpQi2g9SJZHw9jwfJLVkCvDqtTNJCxS2c25i4BbBZwGZWFBHgAbrQ77qzN2vAiqxwj2w7rTEsxFHv1oKV\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mhaZrhgfvGv49E47oDupq8wBppSNfhMFam\",\n \"pubkey\": \"tpubDA9GDAntyJu4H7sF54p2y2XUQKZecnakxWYJpSzRAnKgMxNip8xJVz7piyaZUwivQvz4RxbGpyn8NQPHqJmHsGDfkwXeHFsgJAHWP6Sqpxq\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"miK8WKVbTb25PHkuzGtgjMkUrmrWZe4d7U\",\n \"pubkey\": \"tpubDA9GDAntyJu4Vt4X61z35Cr2HxGTr4PDUpmX5abtbGQ2Zs5EJJ3kAwwhdyCC1EGx4NqiErEXzictM4SxnfQng7jEhiKofkQHkgysqQb6VyQ\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"miihJtUK4WKc6u3gxirkNmKTbPBvFUxt81\",\n \"pubkey\": \"tpubDA9GDAntyJu4JbNXGCuV83JHncjaq7VvaxuGBNNEuMRdk8aXKLQ7xNPncmrtzS5kjKqWQhW79YCve3EfUAvE6dsVn9DQXdWjq9KZQcn1cZs\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"pubkey\": \"tpubDA9GDAntyJu4K82qyamAJ31vsRiLmGAGQAvvjMWbyHf5yJFDQt8sVpGCftFYMmhME5bRYSXzogRbJkthKhHLsgxJURfNofnNiJbNKDiNQLk\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mnHsWiDjPYw27jPzMfqUFM9scdsgHQ6NJg\",\n \"pubkey\": \"tpubDA9GDAntyJu4mwA4W8hg3Wsf5PH3YWgbcoB4dr3Cf77KELoQeD8WfN4avxprcKKd2E4ivZGTJ45ntCimLTrQibBcanGxudpvqCiDfvCocM1\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mp2RdZPTyBs9WrtPodcrSf9rTBSRKCTBqR\",\n \"pubkey\": \"tpubDA9GDAntyJu4ftNy5YcBuVWmiCNsjsEtZHDi45Q3TKDqgaRsQTMCq1STgCV3KhcM3VWANdk2gPaSmF4Acf39WGgcwRjsbfYgCPUDGfkn5cQ\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mpKoa7UDF5mD7bupzZpdnyXxo13vPHAPAj\",\n \"pubkey\": \"tpubDA9GDAntyJu4ygvkf9qDcakZPBDGQzqZ3qkQLWT7odrTx3sEKwMU8S5YRs1siZVLGEGBkya6oMPKB4DDJ8idy78mMujvCJ9xg8MbRDn2zFk\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mqaQqa8sxKjoPtSiacuKZX61v2TASZiK7e\",\n \"pubkey\": \"tpubDA9GDAntyJu56YFCUSaYZTbzDQmLUtpKMhxLFP3BgnRa7QUEQnbZVEYUBSQ8yiMbUhA83Pt72b18NnyWE7EvFP7Nf2gY1ZDq9xTBM9cRcar\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mqdwaA91Fe888P29tm8XqDbutdq4jfmKzV\",\n \"pubkey\": \"tpubDA9GDAntyJu4tKNwyo8vLQ2KTvSTteofqjM4YDEgUxUd1n4o5iBme1QcYF799H7Svi1MiUkQsgsehfEoELY9zY5me6JuALY7fcDTdpL4Kks\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mqwYxGxBhfFodPpkCDGXK3eiSHpfRPi6SD\",\n \"pubkey\": \"tpubDA9GDAntyJu4EExRfNAiaX8BF4aMZodvmbW6fyohsh2KrJtEpsA3RYGpHadnFW2XNQ3AM8rMnrF8XzDett9gYAqcwgX5NfB7aGwZUzFnY7G\",\n \"used_times\": 1\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"ms5HaDddaUyAAMczcdCbYuTFkuBdAgQxeY\",\n \"pubkey\": \"tpubDA9GDAntyJu4djXrpu7QzDQyq3L9vYpePKx7oxhBnhb9iogFm5yhr35v6hY6F2nbTC1cNtNPMj9mdCGAzgRAPcp314RMpdu1vdZ8HbKGzsg\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mtq45ABFdcEBnuG8VjRfyhRW35rzWZa7U9\",\n \"pubkey\": \"tpubDA9GDAntyJu4cfSnoTMmBNQG7PS9nYkWih6c9DdVQ4BsdcYh77xvCqRnYfxUSzEepHt3CbRVQjknCpX3e7di9UdvZB2eUHDrp33EeudNqGW\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mtvRgoqoAZdrqrsTdGYmqKzByKuerqgupK\",\n \"pubkey\": \"tpubDA9GDAntyJu52jLmREuYqCNJmX1uzsAxWsYME8PoUK4wG9KrdDrNRdGwmSwB7XZewgBJ9wu31TeL1cWMwegUc5QzF6xtGcPhPXsuSBtu1DE\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mtwm7SVcxtxXHgkxwLfrPodkuo14wKgNrz\",\n \"pubkey\": \"tpubDA9GDAntyJu4YkkdNxGKiUABfQ1phUrbuQaVS7u7bE7xjKEMMfJ9ga1pb3kijvY6v7TuV9nTDCkng1XJcgBda3btMCh3Fwt9ngGwHRDXm2Z\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"pubkey\": \"tpubDA9GDAntyJu4MyJWHNPKXbWDmkmg1VVMSxumiRmBHQfQKqfr7BGiHRrP4pHXexmorYAXjHkzB4naVB8qp6Cr5JZBuBoD9QGWYmAM9NcQZqP\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mur916Vn2bk7ASmHqtVs294wgU6KeHVxmq\",\n \"pubkey\": \"tpubDA9GDAntyJu4Eixz66KLYzFsprgwbQ1CsTEZjy48cRRHt4Md4mfgJ8ps8h8sp8dg35rD3GD5WuUes9GB5ceVKJehHsZUCtcUxDbExrozfN1\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mv4KFMYgTdH3c2etDRvyybzKAxfCk3rHi5\",\n \"pubkey\": \"tpubDA9GDAntyJu4UuzqvQhgnXkPz2wJNL7zUWZLE7HYP7woQ62wuJnBNt6QLGw2Gc1WRJZ3sa558CsedMJEaFuMa6zUaVmoQysF6WwwT7yuhaF\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mvJRJRn2ZvdLwLsKuAyenCpBkY9QxbegGF\",\n \"pubkey\": \"tpubDA9GDAntyJu4qkyiyr8CRDo12dohq2BcBCY8M7qRELp6xXJncbhjdx4eMZNhSfHd5SWa2qYm4Aupot866XqZ9G2UznaN5PtWF1faNKqBxT4\",\n \"used_times\": 0\n },\n {\n \"account\": \"mhjFSotvK8b2eJHZw2Tgf8czVLDsG2tLyu\",\n \"address\": \"mwSmsEyeJE7CxzLvwoGqoHLWmd4r7nQeeJ\",\n \"pubkey\": \"tpubDA9GDAntyJu4S7YgeBYbzaaCUPadcuhYmgR2Ugrj7yuvkfpGt1qRYkA9uQE8ekfkN2cSU7ywTqRSXmYVm4DBxQM5PbfLE1FGh7vrfyTDZDv\",\n \"used_times\": 0\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 53,\n \"total_pages\": 3\n }\n}"
},
{
"title": "List addresses in specified account",
"curl": "curl -d'{\"method\": \"address_list\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"}}' http://localhost:5279/",
"lbrynet": "lbrynet address list --account_id=\"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"address_list\", \"params\": {\"account_id\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mgPgMEPSvErRFvVihFmUrSD6W84u4TA7Hv\",\n \"pubkey\": \"tpubDA9GDAntyJu4VrQXCBni5u3bPJu4qHp6SPKn4eanRKvXfRgQSXHPTT5fW74hcEG68sg2eUPpA1HZeqEQaHJgNZFBuDi6pGenPASE9rwpPkt\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mgr1So3484shNr8kNMsavaohFwwRm9AnFU\",\n \"pubkey\": \"tpubDA9GDAntyJu4GoY24GN59DWm4A3uux8Gv71kD1fw85Nt7o4ZtdACNjM5ZE2gSN1GYrGYXoz8mzXoQRbPUQs4JTqARTSt5Faco4HM41rJTgf\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mguUfyvvbYuEWiXxKWCWp8dsvphC374QKC\",\n \"pubkey\": \"tpubDA9GDAntyJu4MQvij6HQg6Wc2Q2uWj7CAVoMnqsueh118giBDp2TYv1fXdgV2tPPvmXtwDqf99RNrcSn5YXXcELhd9msNEPw9hLPucG6CpE\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mhBjCUYvnJw7pLYyfyG3xzDohfJmwMjntM\",\n \"pubkey\": \"tpubDA9GDAntyJu4Jgaa9TzJ652wFyuLwBgMgaDKNFbMGhu7oNTpkuuB36ALmtRvtFCkozkSrZogj4dcuW246R9m3Nsm2d69oacBzbeiuyPCtQt\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mhfBgZWfPDF7wD6h97admwDYdaNs1SXafX\",\n \"pubkey\": \"tpubDA9GDAntyJu4zZ1UjFJcgC2zx8okqHedbh1scNYtEThVR53SJKr32oGq2LjUoz35Q7gBqQRcv67JiLVjtes5jpuQbjiaEio9iUmHDyQQCfK\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mi2aTSX87vtAg1QJoW6sP61cByjWgmv9sf\",\n \"pubkey\": \"tpubDA9GDAntyJu4MqkRoL5tDyCk12Mah17UjSYoev1pGqe9M5ZsbdmNDpDCpadXX3sgSKfarF7EDoitqnr5ixCvr6ReAF6wdaKMKJnsrVdeNL4\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mjPQuMg9VLMfJBRMTLkK5tc1F4v83EZuSG\",\n \"pubkey\": \"tpubDA9GDAntyJu4REycnhURTm9svTtCshEKdSKfA2hdmDWaM4Nh13NbppVuQ37G5rPQ26B5rfEw8fMQk3hPGnZ62bCB2Qq7b7wXLnnGqqFTNcT\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mjwhcGupeMHK5vPM3SftKjyPycFbFLYw9E\",\n \"pubkey\": \"tpubDA9GDAntyJu4oKFXbsX3iGqRfBiVZqQKinVB28dzjTYJWwP4bEURX6ogBUhvXwUqTDXX84v1Z6gBeoKQxTs3XKt55fk72NHf1VyRYmWco44\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mm29WyoMPQPJq7QVJGjtbxTvmPW1jL4KbF\",\n \"pubkey\": \"tpubDA9GDAntyJu4etnuFGKHEnL5VnoeExkX3o1KxcFyQQGYgRRSmKSYGGQcZjwH4GSpjnLSVhguac3aBRQYqB1C3c2GcD63EeWo4JtMZhExRpe\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mm6ju92EAUoX3Kx8yKMRDvV1Yw8fnMCbwJ\",\n \"pubkey\": \"tpubDA9GDAntyJu4NNzPx3B2mGLM3eqA8ocs77UZeA15ny7XLE3gCJoYCFpF8i6UU5UQc2qDZVwnhmwyUpriaaTCTNWBqZVpKbaCEjKFfvaJuog\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mnBXVNxS4Xs8xg3AzrQbfcjtBSmivXoh4n\",\n \"pubkey\": \"tpubDA9GDAntyJu4TV1sWBpVyD9DWnsH4m6vLKsv7ZurM71veSumgBymKN7BGT7DqCd2udCpDEaeRLX2A9XfD8h84yaJTpCvBiQs9gnQDXKNgi1\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mpPkZYKWvde2ycPeBwCPW4n1ZC4Z9dti8f\",\n \"pubkey\": \"tpubDA9GDAntyJu4F6uszUizCBJBZLn78LwFFXQkcJ6PpqBouLU2iFKVwZ4GPY6r4QvYjrvJ8UPJ8hyemLcxecveUmFeEJmH27ZBC3xkabgfaPn\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mpURY7Y9WPTf4yStztmtdiqnJEip3rzKdz\",\n \"pubkey\": \"tpubDA9GDAntyJu522u6TeHwazDETcpokjNqmmoJkdVDzqrYp6HGDo5RQAWpgBFAM18D9dfrqdoT3SXYShDneC8RLqZySidcuFfh85LaAtTXHaq\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mpaJaBdSVWmhvAx5M1DaHt5zZTbXhFiiLE\",\n \"pubkey\": \"tpubDA9GDAntyJu4sqMt3KuJpS1jL77fsZq2on7goPwczD4kTRCZHaMSFdggg7NsrrJgRUnyPq2t4WmW9EEbf9j5hTbW8ZVQK5hvkxxnWkQW9zY\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mszRRxz2VQZKsAKthY9heSB5xwdakZ6gkw\",\n \"pubkey\": \"tpubDA9GDAntyJu4xMmBekaRLedHLNdx6ksxZajRv7imPf5XFpciQKXDRPCv5rJ9oUDWX2oGp8JccjG4p3TY66wVhxXd77sDD3qDGjnMWJ9ZyFs\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mu6r6EMyp6nnwmydoFDaokhsaKndxxW4HY\",\n \"pubkey\": \"tpubDA9GDAntyJu4LXiLsMzUnVbqkRZv2aVv83v3PrhKatPww5wMr1H6t2bGSNBxTaTbr2Bhf1Ppm8AiE6jnQemf4rrYv5Fz6XujqBQADAm1k74\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mykwrnckz6sZ2KEe5Wpagt7SjPJdXF6m2V\",\n \"pubkey\": \"tpubDA9GDAntyJu4FiP8EBnyFjKgM7BzYai8Q5HZSr2Z4SRy3j2Zs47ivz1KvNZKDqF33Ljs3mrBUFgKycnqKHNY3AKe3WKZj8kyEAuBZNuPtvH\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"myqynCQQm83z7bjz1rJpFzv8aZxheNoosq\",\n \"pubkey\": \"tpubDA9GDAntyJu4YzqpSUC4UfUEEgWMowZmbpJRhRFS6WyLnN1hADYUty5dWxnUg6NccpNmQFL9kBWnoBiLEMoJuSdjbNZwr3tA8LZgh5byreV\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mzSzaNd9c3QoGiXESZcxv9vmwP2V7ujZJg\",\n \"pubkey\": \"tpubDA9GDAntyJu4iUzB1QVZ9ckBNHsM1WTHubjkDHpvCmeJYXvMdw6WABx9uJWQ8nFpEz8gd6sWDxSkkh9wbTTx4QH1XaNN4KSxtqxB3BQcM9V\",\n \"used_times\": 0\n },\n {\n \"account\": \"mqVK8Df2cBxZgUnxGXXd8JhTm1LHvPofdn\",\n \"address\": \"mzjHGbQvE5orzqRjmJxsbKN7jedNGmv4QD\",\n \"pubkey\": \"tpubDA9GDAntyJu4SzcUV91pjutvrUw1m5gRyzDC5BZbhgzMCZ5tCcMPuH9Ej3BoHv4RdVB1U4LbahzGTUdVZFSHqPng2eg9PLVbde4w8J17nGW\",\n \"used_times\": 0\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 26,\n \"total_pages\": 2\n }\n}"
}
]
},
{
"name": "address_unused",
"description": "Return an address containing no balance, will create\na new address if there is none.",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": " \"an address in base58\"",
"examples": [
{
"title": "Get an unused address",
"curl": "curl -d'{\"method\": \"address_unused\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet address unused",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"address_unused\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\"\n}"
}
]
}
]
},
"blob": {
"doc": "Blob management.",
"commands": [
{
"name": "blob_announce",
"description": "Announce blobs to the DHT",
"arguments": [
{
"name": "blob_hash",
"type": "str",
"description": "announce a blob, specified by blob_hash",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "announce all blobs associated with stream_hash",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "announce all blobs associated with sd_hash and the sd_hash itself",
"is_required": false
}
],
"returns": "(bool) true if successful",
"examples": []
},
{
"name": "blob_clean",
"description": "Deletes blobs to cleanup disk space",
"arguments": [],
"returns": "(bool) true if successful",
"examples": []
},
{
"name": "blob_delete",
"description": "Delete a blob",
"arguments": [
{
"name": "blob_hash",
"type": "str",
"description": "blob hash of the blob to delete",
"is_required": true
}
],
"returns": "(str) Success/fail message",
"examples": [
{
"title": "Delete a blob",
"curl": "curl -d'{\"method\": \"blob_delete\", \"params\": {\"blob_hash\": \"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\"}}' http://localhost:5279/",
"lbrynet": "lbrynet blob delete d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"blob_delete\", \"params\": {\"blob_hash\": \"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": \"Deleted d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\"\n}"
}
]
},
{
"name": "blob_get",
"description": "Download and return a blob",
"arguments": [
{
"name": "blob_hash",
"type": "str",
"description": "blob hash of the blob to get",
"is_required": true
},
{
"name": "timeout",
"type": "int",
"description": "timeout in number of seconds",
"is_required": false
}
],
"returns": "(str) Success/Fail message or (dict) decoded data",
"examples": []
},
{
"name": "blob_list",
"description": "Returns blob hashes. If not given filters, returns all blobs known by the blob manager",
"arguments": [
{
"name": "needed",
"type": "bool",
"description": "only return needed blobs",
"is_required": false
},
{
"name": "finished",
"type": "bool",
"description": "only return finished blobs",
"is_required": false
},
{
"name": "uri",
"type": "str",
"description": "filter blobs by stream in a uri",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "filter blobs by stream hash",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "filter blobs in a stream by sd hash, ie the hash of the stream descriptor blob for a stream that has been downloaded",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": "(list) List of blob hashes",
"examples": [
{
"title": "List your local blobs",
"curl": "curl -d'{\"method\": \"blob_list\", \"params\": {\"needed\": false, \"finished\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet blob list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"blob_list\", \"params\": {\"needed\": false, \"finished\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n \"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\",\n \"3413d65026fcabc798f477a457e205e96b3ac921176df62f79a9d90b9c5bed665bb3ad849c8b472c27d13cfbc6440e9f\",\n \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"536763bbb86446ef57a8a45493b12503ea4c74d865692343ff60c3b40431b5a8624341ae1e48aaf601057071cb25b7c3\"\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 4,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "blob_reflect",
"description": "Reflects specified blobs",
"arguments": [
{
"name": "reflector_server",
"type": "str",
"description": "reflector address",
"is_required": false
}
],
"returns": "(list) reflected blob hashes",
"examples": []
},
{
"name": "blob_reflect_all",
"description": "Reflects all saved blobs",
"arguments": [],
"returns": "(bool) true if successful",
"examples": []
}
]
},
"channel": {
"doc": "Create, update, abandon and list your channel claims.",
"commands": [
{
"name": "channel_abandon",
"description": "Abandon one of my channel claims.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the claim to abandon",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "txid of the claim to abandon",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "nout of the claim to abandon",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until abandon is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Abandon a channel claim",
"curl": "curl -d'{\"method\": \"channel_abandon\", \"params\": {\"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet channel abandon 595c2e2f0c1f59188628fab7503692b0145779a2",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"channel_abandon\", \"params\": {\"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"010000000164ffcff7cce034b5b519f047c9aa5c4cf0dc52d6e5804d74174140a5c52182ab000000006a4730440220554a49624aa9ba48ff6f6ea0e747e3f0fce88e9fbdaef0ec8336e8367dc8552802203988c9dc46b066b699fe7f93bd2fcad05cd86b309846f65b85c3b32ba98667c6012103687cff1a4970d2e992ba0035de7251dd822cb495c17ec0c5c4543a514227cd65ffffffff0134b7f505000000001976a91406c0b5a2fea95e096d4406846d1eb01a1993d5a388ac00000000\",\n \"inputs\": [\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 9,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mg8fCtz1uf7aZe8UVgzMcBPw1V3yEu4Qry\",\n \"amount\": \"0.999893\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 0,\n \"timestamp\": null,\n \"txid\": \"3691918e1a838c00b759609bfc89effdfab93860a9bc68d06edcc6ee0bb566d5\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.000107\",\n \"total_input\": \"1.0\",\n \"total_output\": \"0.999893\",\n \"txid\": \"3691918e1a838c00b759609bfc89effdfab93860a9bc68d06edcc6ee0bb566d5\"\n }\n}"
}
]
},
{
"name": "channel_create",
"description": "Create a new channel by generating a channel private key and establishing an '@' prefixed claim.",
"arguments": [
{
"name": "name",
"type": "str",
"description": "name of the channel prefixed with '@'",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": true
},
{
"name": "allow_duplicate_name",
"type": "bool",
"description": "create new channel even if one already exists with given name. default: false.",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the publication",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the publication",
"is_required": false
},
{
"name": "email",
"type": "str",
"description": "email of channel owner",
"is_required": false
},
{
"name": "website_url",
"type": "str",
"description": "website url",
"is_required": false
},
{
"name": "featured",
"type": "list",
"description": "claim_ids of featured content in channel",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "content tags",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the channel, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations of the channel, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "cover_url",
"type": "str",
"description": "url of cover image",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account to use for holding the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the channel is sent to, if not specified it will be determined automatically from the account",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Create a channel claim without metadata",
"curl": "curl -d'{\"method\": \"channel_create\", \"params\": {\"name\": \"@channel\", \"bid\": \"1.0\", \"featured\": [], \"tags\": [], \"languages\": [], \"locations\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet channel create @channel 1.0",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"channel_create\", \"params\": {\"name\": \"@channel\", \"bid\": \"1.0\", \"featured\": [], \"tags\": [], \"languages\": [], \"locations\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"0100000001b8749850c50b2d456a0d54ab618c516f9f80a6f1dc7977dab4ca230e775931f0010000006a473044022041b5434aab5d73b41aad02687a657c9b99e1ef83fa8eabb08407359cbe6731e30220293f3f8ac7bf6ade85f4a181920b20d6d55b04acbce1edffc9ef7d1f94a88fdc012102ebd9926866ffd2ea504ee0ec7affe0f85f76e8a3c55149680ff74a63bd655123ffffffff0200e1f505000000004cb508406368616e6e656c260012230a2103bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e56d7576a914988d8d19fed515ed8e9d60a1c31c8aed750e441d88acc462a029000000001976a914b3aa073545c0d148feed1b89f24a3c7302c2f53188ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mhaZrhgfvGv49E47oDupq8wBppSNfhMFam\",\n \"amount\": \"7.999876\",\n \"confirmations\": 2,\n \"height\": 207,\n \"nout\": 1,\n \"timestamp\": 1655141670,\n \"txid\": \"f03159770e23cab4da7779dcf1a6809f6f518c61ab540d6a452d0bc5509874b8\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"create\",\n \"confirmations\": -2,\n \"has_signing_key\": true,\n \"height\": -2,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": null,\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\"\n },\n \"value_type\": \"channel\"\n },\n {\n \"address\": \"mwtvw9x13nsVo6xkkrt3RFkk5h6TAjgPdK\",\n \"amount\": \"6.983769\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.016107\",\n \"total_input\": \"7.999876\",\n \"total_output\": \"7.983769\",\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\"\n }\n}"
},
{
"title": "Create a channel claim with all metadata",
"curl": "curl -d'{\"method\": \"channel_create\", \"params\": {\"name\": \"@bigchannel\", \"bid\": \"1.0\", \"title\": \"Big Channel\", \"description\": \"A channel with lots of videos.\", \"email\": \"creator@smallmedia.com\", \"website_url\": \"http://smallmedia.com\", \"featured\": [], \"tags\": [\"music\", \"art\"], \"languages\": [\"pt-BR\", \"uk\"], \"locations\": [\"BR\", \"UA::Kiyv\"], \"thumbnail_url\": \"http://smallmedia.com/logo.jpg\", \"cover_url\": \"http://smallmedia.com/logo.jpg\", \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet channel create @bigchannel 1.0 --title=\"Big Channel\" --description=\"A channel with lots of videos.\" --email=\"creator@smallmedia.com\" --tags=music --tags=art --languages=pt-BR --languages=uk --locations=BR --locations=UA::Kiyv --website_url=\"http://smallmedia.com\" --thumbnail_url=\"http://smallmedia.com/logo.jpg\" --cover_url=\"http://smallmedia.com/logo.jpg\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"channel_create\", \"params\": {\"name\": \"@bigchannel\", \"bid\": \"1.0\", \"title\": \"Big Channel\", \"description\": \"A channel with lots of videos.\", \"email\": \"creator@smallmedia.com\", \"website_url\": \"http://smallmedia.com\", \"featured\": [], \"tags\": [\"music\", \"art\"], \"languages\": [\"pt-BR\", \"uk\"], \"locations\": [\"BR\", \"UA::Kiyv\"], \"thumbnail_url\": \"http://smallmedia.com/logo.jpg\", \"cover_url\": \"http://smallmedia.com/logo.jpg\", \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"010000000164ffcff7cce034b5b519f047c9aa5c4cf0dc52d6e5804d74174140a5c52182ab010000006b483045022100d26fb9b49ef1a48d33c8639425c1bf216127a45b416ec6a51cb4e81bc8d3a4f002205b38ec4db4ac5e80801203b10b4d31e25775f259163b3f79e78da68eb5ba2b180121036a4e85fba940e22e77dae155444e4f11fa6b9436456a668f00cee5560e48eb36ffffffff0200e1f50500000000fd1701b50b406269676368616e6e656c4ced0012740a2102354550c6c7c2b7c777410102a90b1c3ffa1073517c1f60aa43306490a64a1089121663726561746f7240736d616c6c6d656469612e636f6d1a15687474703a2f2f736d616c6c6d656469612e636f6d22202a1e687474703a2f2f736d616c6c6d656469612e636f6d2f6c6f676f2e6a7067420b426967204368616e6e656c4a1e41206368616e6e656c2077697468206c6f7473206f6620766964656f732e52202a1e687474703a2f2f736d616c6c6d656469612e636f6d2f6c6f676f2e6a70675a056d757369635a0361727462050883011820620308ab016a0208206a0908e9011a044b6979766d7576a9149205f209a3c3a7d6025acbbded70700b686f948488acd22cd305000000001976a91499bb3c59d341b872794cea2524fd2884f83c655e88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mnTTF4Sw8SGHi58DRCRSpYXJosZ61nPijQ\",\n \"amount\": \"1.9993635\",\n \"confirmations\": 1,\n \"height\": 210,\n \"nout\": 1,\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mtq45ABFdcEBnuG8VjRfyhRW35rzWZa7U9\",\n \"amount\": \"1.0\",\n \"claim_id\": \"6e536ae75030d1293b52d056a9bac51b3b02ed2a\",\n \"claim_op\": \"create\",\n \"confirmations\": -2,\n \"has_signing_key\": true,\n \"height\": -2,\n \"meta\": {},\n \"name\": \"@bigchannel\",\n \"normalized_name\": \"@bigchannel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@bigchannel#6e536ae75030d1293b52d056a9bac51b3b02ed2a\",\n \"timestamp\": null,\n \"txid\": \"acba29e905f1a37d12f8371a77e1b6f92c1a45c7ed396b1eceea97c154e9d2e3\",\n \"type\": \"claim\",\n \"value\": {\n \"cover\": {\n \"url\": \"http://smallmedia.com/logo.jpg\"\n },\n \"description\": \"A channel with lots of videos.\",\n \"email\": \"creator@smallmedia.com\",\n \"languages\": [\n \"pt-BR\",\n \"uk\"\n ],\n \"locations\": [\n {\n \"country\": \"BR\"\n },\n {\n \"city\": \"Kiyv\",\n \"country\": \"UA\"\n }\n ],\n \"public_key\": \"02354550c6c7c2b7c777410102a90b1c3ffa1073517c1f60aa43306490a64a1089\",\n \"public_key_id\": \"mpJbNo6rzLmfViJkVay6kxdqCrAfB1BDbe\",\n \"tags\": [\n \"music\",\n \"art\"\n ],\n \"thumbnail\": {\n \"url\": \"http://smallmedia.com/logo.jpg\"\n },\n \"title\": \"Big Channel\",\n \"website_url\": \"http://smallmedia.com\"\n },\n \"value_type\": \"channel\"\n },\n {\n \"address\": \"muXoymiRVLaS1FXJSgPjieSedrWPgUgEkK\",\n \"amount\": \"0.9772565\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"acba29e905f1a37d12f8371a77e1b6f92c1a45c7ed396b1eceea97c154e9d2e3\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.022107\",\n \"total_input\": \"1.9993635\",\n \"total_output\": \"1.9772565\",\n \"txid\": \"acba29e905f1a37d12f8371a77e1b6f92c1a45c7ed396b1eceea97c154e9d2e3\"\n }\n}"
}
]
},
{
"name": "channel_export",
"description": "Export channel private key.",
"arguments": [
{
"name": "channel_id",
"type": "str",
"description": "claim id of channel to export",
"is_required": true
},
{
"name": "channel_name",
"type": "str",
"description": "name of channel to export",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channels, defaults to all accounts.",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(str) serialized channel private key",
"examples": []
},
{
"name": "channel_import",
"description": "Import serialized channel private key (to allow signing new streams to the channel)",
"arguments": [
{
"name": "channel_data",
"type": "str",
"description": "serialized channel, as exported by channel export",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "import into specific wallet",
"is_required": false
}
],
"returns": "(dict) Result dictionary",
"examples": []
},
{
"name": "channel_list",
"description": "List my channel claims.",
"arguments": [
{
"name": "name",
"type": "str or list",
"description": "channel name",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "channel id",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "shows previous channel updates and abandons",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "resolve",
"type": "bool",
"description": "resolves each channel to provide additional metadata",
"is_required": false
},
{
"name": "no_totals",
"type": "bool",
"description": "do not calculate the total number of pages and items in result set (significant performance boost)",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": [
{
"title": "List your channel claims",
"curl": "curl -d'{\"method\": \"channel_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"resolve\": false, \"no_totals\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet channel list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"channel_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"resolve\": false, \"no_totals\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"create\",\n \"confirmations\": 1,\n \"has_signing_key\": true,\n \"height\": 209,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\"\n },\n \"value_type\": \"channel\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
},
{
"title": "Paginate your channel claims",
"curl": "curl -d'{\"method\": \"channel_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"page\": 1, \"page_size\": 20, \"resolve\": false, \"no_totals\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet channel list --page=1 --page_size=20",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"channel_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"page\": 1, \"page_size\": 20, \"resolve\": false, \"no_totals\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"create\",\n \"confirmations\": 1,\n \"has_signing_key\": true,\n \"height\": 209,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\"\n },\n \"value_type\": \"channel\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "channel_sign",
"description": "Signs data using the specified channel signing key.",
"arguments": [
{
"name": "channel_name",
"type": "str",
"description": "name of channel used to sign (or use channel id)",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of channel used to sign (or use channel name)",
"is_required": false
},
{
"name": "hexdata",
"type": "str",
"description": "data to sign, encoded as hexadecimal",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(dict) Signature if successfully made, (None) or an error otherwise\n {\n \"signature\": (str) The signature of the comment,\n \"signing_ts\": (str) The timestamp used to sign the comment,\n }",
"examples": []
},
{
"name": "channel_update",
"description": "Update an existing channel claim.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the channel to update",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the publication",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the publication",
"is_required": false
},
{
"name": "email",
"type": "str",
"description": "email of channel owner",
"is_required": false
},
{
"name": "website_url",
"type": "str",
"description": "website url",
"is_required": false
},
{
"name": "featured",
"type": "list",
"description": "claim_ids of featured content in channel",
"is_required": false
},
{
"name": "clear_featured",
"type": "bool",
"description": "clear existing featured content (prior to adding new ones)",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "add content tags",
"is_required": false
},
{
"name": "clear_tags",
"type": "bool",
"description": "clear existing tags (prior to adding new ones)",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the channel, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "clear_languages",
"type": "bool",
"description": "clear existing languages (prior to adding new ones)",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations of the channel, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "clear_locations",
"type": "bool",
"description": "clear existing locations (prior to adding new ones)",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "cover_url",
"type": "str",
"description": "url of cover image",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account in which to look for channel (default: all)",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the channel is sent",
"is_required": false
},
{
"name": "new_signing_key",
"type": "bool",
"description": "generate a new signing key, will invalidate all previous publishes",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
},
{
"name": "replace",
"type": "bool",
"description": "instead of modifying specific values on the channel, this will clear all existing values and only save passed in values, useful for form submissions where all values are always set",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Update a channel claim",
"curl": "curl -d'{\"method\": \"channel_update\", \"params\": {\"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"title\": \"New Channel\", \"featured\": [], \"clear_featured\": false, \"tags\": [], \"clear_tags\": false, \"languages\": [], \"clear_languages\": false, \"locations\": [], \"clear_locations\": false, \"new_signing_key\": false, \"funding_account_ids\": [], \"preview\": false, \"blocking\": false, \"replace\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet channel update 595c2e2f0c1f59188628fab7503692b0145779a2 --title=\"New Channel\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"channel_update\", \"params\": {\"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"title\": \"New Channel\", \"featured\": [], \"clear_featured\": false, \"tags\": [], \"clear_tags\": false, \"languages\": [], \"clear_languages\": false, \"locations\": [], \"clear_locations\": false, \"new_signing_key\": false, \"funding_account_ids\": [], \"preview\": false, \"blocking\": false, \"replace\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"01000000026419a92bda9f0ef64bde16f5a322d5668d68d53b4e38eb99b4738f72dbe880de000000006a47304402206e41c2509f2ff2099feb34189e34b30d1e5cb011a119c7d3a50ec48d8d49b987022035cb2af940fb1e1db687fc29e7575c22c3f1f799edd82d656b85a26fb653c78d012103687cff1a4970d2e992ba0035de7251dd822cb495c17ec0c5c4543a514227cd65ffffffffe8c0c36bed593c86544238beb9cf20d825962eb11053a24fa33a0378a87d7440000000006a47304402202b6601a98c683dfc99c2c1997156eb7bf6a45cc627d953b1b7246db450229c6e02201b74c5e5d27c7f3cf2770c7e308826d331dd75c2275e31f6828e42369fd2466b0121026dec9b64cb86838f381490252b2cfa34512e9673b65b6016734e92c1968762ecffffffff0200e1f505000000006eb708406368616e6e656c14a2795714b0923650b7fa288618591f0c2f2e5c59330012230a2103bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5420b4e6577204368616e6e656c6d6d76a914988d8d19fed515ed8e9d60a1c31c8aed750e441d88ac5ec9ea0b000000001976a9144c1f61ac2064778c7f08eda66437d2677befd3c088ac00000000\",\n \"inputs\": [\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"create\",\n \"confirmations\": 1,\n \"has_signing_key\": true,\n \"height\": 209,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\"\n },\n \"value_type\": \"channel\"\n },\n {\n \"address\": \"n1JZiGPzhiFUPTysS5PABHjdiRqfWNMSaX\",\n \"amount\": \"1.999604\",\n \"confirmations\": 1,\n \"height\": 209,\n \"nout\": 0,\n \"timestamp\": 1655141670,\n \"txid\": \"40747da878033aa34fa25310b12e9625d820cfb9be384254863c59ed6bc3c0e8\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": -2,\n \"has_signing_key\": true,\n \"height\": -2,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": null,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n {\n \"address\": \"mnTTF4Sw8SGHi58DRCRSpYXJosZ61nPijQ\",\n \"amount\": \"1.9993635\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.0002405\",\n \"total_input\": \"2.999604\",\n \"total_output\": \"2.9993635\",\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\"\n }\n}"
}
]
}
]
},
"claim": {
"doc": "List and search all types of claims.",
"commands": [
{
"name": "claim_list",
"description": "List my stream and channel claims.",
"arguments": [
{
"name": "claim_type",
"type": "str or list",
"description": "claim type: channel, stream, repost, collection",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "claim id",
"is_required": false
},
{
"name": "channel_id",
"type": "str or list",
"description": "streams in this channel",
"is_required": false
},
{
"name": "name",
"type": "str or list",
"description": "claim name",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "shows previous claim updates and abandons",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "has_source",
"type": "bool",
"description": "list claims containing a source field",
"is_required": false
},
{
"name": "has_no_source",
"type": "bool",
"description": "list claims not containing a source field",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "resolve",
"type": "bool",
"description": "resolves each claim to provide additional metadata",
"is_required": false
},
{
"name": "order_by",
"type": "str",
"description": "field to order by: 'name', 'height', 'amount'",
"is_required": false
},
{
"name": "no_totals",
"type": "bool",
"description": "do not calculate the total number of pages and items in result set (significant performance boost)",
"is_required": false
},
{
"name": "include_received_tips",
"type": "bool",
"description": "calculate the amount of tips received for claim outputs",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": [
{
"title": "List all your claims",
"curl": "curl -d'{\"method\": \"claim_list\", \"params\": {\"claim_type\": [], \"claim_id\": [], \"name\": [], \"is_spent\": false, \"channel_id\": [], \"has_source\": false, \"has_no_source\": false, \"resolve\": false, \"no_totals\": false, \"include_received_tips\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet claim list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"claim_list\", \"params\": {\"claim_type\": [], \"claim_id\": [], \"name\": [], \"is_spent\": false, \"channel_id\": [], \"has_source\": false, \"has_no_source\": false, \"resolve\": false, \"no_totals\": false, \"include_received_tips\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 1,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 2,\n \"total_pages\": 1\n }\n}"
},
{
"title": "Paginate your claims",
"curl": "curl -d'{\"method\": \"claim_list\", \"params\": {\"claim_type\": [], \"claim_id\": [], \"name\": [], \"is_spent\": false, \"channel_id\": [], \"has_source\": false, \"has_no_source\": false, \"page\": 1, \"page_size\": 20, \"resolve\": false, \"no_totals\": false, \"include_received_tips\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet claim list --page=1 --page_size=20",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"claim_list\", \"params\": {\"claim_type\": [], \"claim_id\": [], \"name\": [], \"is_spent\": false, \"channel_id\": [], \"has_source\": false, \"has_no_source\": false, \"page\": 1, \"page_size\": 20, \"resolve\": false, \"no_totals\": false, \"include_received_tips\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 1,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 2,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "claim_search",
"description": "Search for stream and channel claims on the blockchain.\n\nArguments marked with \"supports equality constraints\" allow prepending the\nvalue with an equality constraint such as '>', '>=', '<' and '<='\neg. --height=\">400000\" would limit results to only claims above 400k block height.\n\nThey also support multiple constraints passed as a list of the args described above.\neg. --release_time=[\">1000000\", \"<2000000\"]",
"arguments": [
{
"name": "name",
"type": "str",
"description": "claim name (normalized)",
"is_required": false
},
{
"name": "text",
"type": "str",
"description": "full text search",
"is_required": false
},
{
"name": "claim_id",
"type": "str",
"description": "full or partial claim id",
"is_required": false
},
{
"name": "claim_ids",
"type": "list",
"description": "list of full claim ids",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "transaction id",
"is_required": false
},
{
"name": "nout",
"type": "str",
"description": "position in the transaction",
"is_required": false
},
{
"name": "channel",
"type": "str",
"description": "claims signed by this channel (argument is a URL which automatically gets resolved), see --channel_ids if you need to filter by multiple channels at the same time, includes claims with invalid signatures, use in conjunction with --valid_channel_signature",
"is_required": false
},
{
"name": "channel_ids",
"type": "list",
"description": "claims signed by any of these channels (arguments must be claim ids of the channels), includes claims with invalid signatures, implies --has_channel_signature, use in conjunction with --valid_channel_signature",
"is_required": false
},
{
"name": "not_channel_ids",
"type": "list",
"description": "exclude claims signed by any of these channels (arguments must be claim ids of the channels)",
"is_required": false
},
{
"name": "has_channel_signature",
"type": "bool",
"description": "claims with a channel signature (valid or invalid)",
"is_required": false
},
{
"name": "valid_channel_signature",
"type": "bool",
"description": "claims with a valid channel signature or no signature, use in conjunction with --has_channel_signature to only get claims with valid signatures",
"is_required": false
},
{
"name": "invalid_channel_signature",
"type": "bool",
"description": "claims with invalid channel signature or no signature, use in conjunction with --has_channel_signature to only get claims with invalid signatures",
"is_required": false
},
{
"name": "limit_claims_per_channel",
"type": "int",
"description": "only return up to the specified number of claims per channel",
"is_required": false
},
{
"name": "is_controlling",
"type": "bool",
"description": "winning claims of their respective name",
"is_required": false
},
{
"name": "public_key_id",
"type": "str",
"description": "only return channels having this public key id, this is the same key as used in the wallet file to map channel certificate private keys: {'public_key_id': 'private key'}",
"is_required": false
},
{
"name": "height",
"type": "int",
"description": "last updated block height (supports equality constraints)",
"is_required": false
},
{
"name": "timestamp",
"type": "int",
"description": "last updated timestamp (supports equality constraints)",
"is_required": false
},
{
"name": "creation_height",
"type": "int",
"description": "created at block height (supports equality constraints)",
"is_required": false
},
{
"name": "creation_timestamp",
"type": "int",
"description": "created at timestamp (supports equality constraints)",
"is_required": false
},
{
"name": "activation_height",
"type": "int",
"description": "height at which claim starts competing for name (supports equality constraints)",
"is_required": false
},
{
"name": "expiration_height",
"type": "int",
"description": "height at which claim will expire (supports equality constraints)",
"is_required": false
},
{
"name": "release_time",
"type": "int",
"description": "limit to claims self-described as having been released to the public on or after this UTC timestamp, when claim does not provide a release time the publish time is used instead (supports equality constraints)",
"is_required": false
},
{
"name": "amount",
"type": "int",
"description": "limit by claim value (supports equality constraints)",
"is_required": false
},
{
"name": "support_amount",
"type": "int",
"description": "limit by supports and tips received (supports equality constraints)",
"is_required": false
},
{
"name": "effective_amount",
"type": "int",
"description": "limit by total value (initial claim value plus all tips and supports received), this amount is blank until claim has reached activation height (supports equality constraints)",
"is_required": false
},
{
"name": "trending_score",
"type": "int",
"description": "limit by trending score (supports equality constraints)",
"is_required": false
},
{
"name": "trending_group",
"type": "int",
"description": "DEPRECATED - instead please use trending_score",
"is_required": false
},
{
"name": "trending_mixed",
"type": "int",
"description": "DEPRECATED - instead please use trending_score",
"is_required": false
},
{
"name": "trending_local",
"type": "int",
"description": "DEPRECATED - instead please use trending_score",
"is_required": false
},
{
"name": "trending_global",
"type": "int",
"description": "DEPRECATED - instead please use trending_score",
"is_required": false
},
{
"name": "reposted_claim_id",
"type": "str",
"description": "all reposts of the specified original claim id",
"is_required": false
},
{
"name": "reposted",
"type": "int",
"description": "claims reposted this many times (supports equality constraints)",
"is_required": false
},
{
"name": "claim_type",
"type": "str",
"description": "filter by 'channel', 'stream', 'repost' or 'collection'",
"is_required": false
},
{
"name": "stream_types",
"type": "list",
"description": "filter by 'video', 'image', 'document', etc",
"is_required": false
},
{
"name": "media_types",
"type": "list",
"description": "filter by 'video/mp4', 'image/png', etc",
"is_required": false
},
{
"name": "fee_currency",
"type": "string",
"description": "specify fee currency: LBC, BTC, USD",
"is_required": false
},
{
"name": "fee_amount",
"type": "decimal",
"description": "content download fee (supports equality constraints)",
"is_required": false
},
{
"name": "duration",
"type": "int",
"description": "duration of video or audio in seconds (supports equality constraints)",
"is_required": false
},
{
"name": "any_tags",
"type": "list",
"description": "find claims containing any of the tags",
"is_required": false
},
{
"name": "all_tags",
"type": "list",
"description": "find claims containing every tag",
"is_required": false
},
{
"name": "not_tags",
"type": "list",
"description": "find claims not containing any of these tags",
"is_required": false
},
{
"name": "any_languages",
"type": "list",
"description": "find claims containing any of the languages",
"is_required": false
},
{
"name": "all_languages",
"type": "list",
"description": "find claims containing every language",
"is_required": false
},
{
"name": "not_languages",
"type": "list",
"description": "find claims not containing any of these languages",
"is_required": false
},
{
"name": "any_locations",
"type": "list",
"description": "find claims containing any of the locations",
"is_required": false
},
{
"name": "all_locations",
"type": "list",
"description": "find claims containing every location",
"is_required": false
},
{
"name": "not_locations",
"type": "list",
"description": "find claims not containing any of these locations",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "order_by",
"type": "list",
"description": "field to order by, default is descending order, to do an ascending order prepend ^ to the field name, eg. '^amount' available fields: 'name', 'height', 'release_time', 'publish_time', 'amount', 'effective_amount', 'support_amount', 'trending_group', 'trending_mixed', 'trending_local', 'trending_global', 'activation_height'",
"is_required": false
},
{
"name": "no_totals",
"type": "bool",
"description": "do not calculate the total number of pages and items in result set (significant performance boost)",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "wallet to check for claim purchase receipts",
"is_required": false
},
{
"name": "include_purchase_receipt",
"type": "bool",
"description": "lookup and include a receipt if this wallet has purchased the claim",
"is_required": false
},
{
"name": "include_is_my_output",
"type": "bool",
"description": "lookup and include a boolean indicating if claim being resolved is yours",
"is_required": false
},
{
"name": "remove_duplicates",
"type": "bool",
"description": "removes duplicated content from search by picking either the original claim or the oldest matching repost",
"is_required": false
},
{
"name": "has_source",
"type": "bool",
"description": "find claims containing a source field",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "find claims where the source stream descriptor hash matches (partially or completely) the given hexadecimal string",
"is_required": false
},
{
"name": "has_no_source",
"type": "bool",
"description": "find claims not containing a source field",
"is_required": false
},
{
"name": "new_sdk_server",
"type": "str",
"description": "URL of the new SDK server (EXPERIMENTAL)",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": [
{
"title": "Search for all claims in channel",
"curl": "curl -d'{\"method\": \"claim_search\", \"params\": {\"claim_ids\": [], \"channel\": \"@channel\", \"channel_ids\": [], \"not_channel_ids\": [], \"has_channel_signature\": false, \"valid_channel_signature\": false, \"invalid_channel_signature\": false, \"is_controlling\": false, \"stream_types\": [], \"media_types\": [], \"any_tags\": [], \"all_tags\": [], \"not_tags\": [], \"any_languages\": [], \"all_languages\": [], \"not_languages\": [], \"any_locations\": [], \"all_locations\": [], \"not_locations\": [], \"order_by\": [], \"no_totals\": false, \"include_purchase_receipt\": false, \"include_is_my_output\": false, \"remove_duplicates\": false, \"has_source\": false, \"has_no_source\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet claim search --channel=@channel",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"claim_search\", \"params\": {\"claim_ids\": [], \"channel\": \"@channel\", \"channel_ids\": [], \"not_channel_ids\": [], \"has_channel_signature\": false, \"valid_channel_signature\": false, \"invalid_channel_signature\": false, \"is_controlling\": false, \"stream_types\": [], \"media_types\": [], \"any_tags\": [], \"all_tags\": [], \"not_tags\": [], \"any_languages\": [], \"all_languages\": [], \"not_languages\": [], \"any_locations\": [], \"all_locations\": [], \"not_locations\": [], \"order_by\": [], \"no_totals\": false, \"include_purchase_receipt\": false, \"include_is_my_output\": false, \"remove_duplicates\": false, \"has_source\": false, \"has_no_source\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"blocked\": {\n \"channels\": [],\n \"total\": 0\n },\n \"items\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5/astream#a\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 1,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"meta\": {\n \"activation_height\": 214,\n \"creation_height\": 213,\n \"creation_timestamp\": 1655141671,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 714,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 213\n },\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"short_url\": \"lbry://astream#a\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": false,\n \"height\": 210,\n \"meta\": {\n \"activation_height\": 210,\n \"claims_in_channel\": 1,\n \"creation_height\": 209,\n \"creation_timestamp\": 1655141670,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 710,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 209\n },\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"short_url\": \"lbry://@channel#5\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
},
{
"title": "Search for claims matching a name",
"curl": "curl -d'{\"method\": \"claim_search\", \"params\": {\"name\": \"astream\", \"claim_ids\": [], \"channel_ids\": [], \"not_channel_ids\": [], \"has_channel_signature\": false, \"valid_channel_signature\": false, \"invalid_channel_signature\": false, \"is_controlling\": false, \"stream_types\": [], \"media_types\": [], \"any_tags\": [], \"all_tags\": [], \"not_tags\": [], \"any_languages\": [], \"all_languages\": [], \"not_languages\": [], \"any_locations\": [], \"all_locations\": [], \"not_locations\": [], \"order_by\": [], \"no_totals\": false, \"include_purchase_receipt\": false, \"include_is_my_output\": false, \"remove_duplicates\": false, \"has_source\": false, \"has_no_source\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet claim search --name=\"astream\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"claim_search\", \"params\": {\"name\": \"astream\", \"claim_ids\": [], \"channel_ids\": [], \"not_channel_ids\": [], \"has_channel_signature\": false, \"valid_channel_signature\": false, \"invalid_channel_signature\": false, \"is_controlling\": false, \"stream_types\": [], \"media_types\": [], \"any_tags\": [], \"all_tags\": [], \"not_tags\": [], \"any_languages\": [], \"all_languages\": [], \"not_languages\": [], \"any_locations\": [], \"all_locations\": [], \"not_locations\": [], \"order_by\": [], \"no_totals\": false, \"include_purchase_receipt\": false, \"include_is_my_output\": false, \"remove_duplicates\": false, \"has_source\": false, \"has_no_source\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"blocked\": {\n \"channels\": [],\n \"total\": 0\n },\n \"items\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5/astream#a\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 1,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"meta\": {\n \"activation_height\": 214,\n \"creation_height\": 213,\n \"creation_timestamp\": 1655141671,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 714,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 213\n },\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"short_url\": \"lbry://astream#a\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": false,\n \"height\": 210,\n \"meta\": {\n \"activation_height\": 210,\n \"claims_in_channel\": 1,\n \"creation_height\": 209,\n \"creation_timestamp\": 1655141670,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 710,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 209\n },\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"short_url\": \"lbry://@channel#5\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
}
]
}
]
},
"collection": {
"doc": "Create, update, list, resolve, and abandon collections.",
"commands": [
{
"name": "collection_abandon",
"description": "Abandon one of my collection claims.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the claim to abandon",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "txid of the claim to abandon",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "nout of the claim to abandon",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until abandon is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "collection_create",
"description": "Create a new collection.",
"arguments": [
{
"name": "name",
"type": "str",
"description": "name of the collection",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": true
},
{
"name": "claims",
"type": "list",
"description": "claim ids to be included in the collection",
"is_required": false
},
{
"name": "allow_duplicate_name",
"type": "bool",
"description": "create new collection even if one already exists with given name. default: false.",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the collection",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the collection",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "content tags",
"is_required": false
},
{
"name": "clear_languages",
"type": "bool",
"description": "clear existing languages (prior to adding new ones)",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the collection, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations of the collection, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the publisher channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of the publisher channel",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account to use for holding the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the collection is sent to, if not specified it will be determined automatically from the account",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Create a collection of one stream",
"curl": "curl -d'{\"method\": \"collection_create\", \"params\": {\"name\": \"tom\", \"bid\": \"1.0\", \"claims\": [\"ad25e05aa7dc5e9994869040c6103f9a8728db46\"], \"allow_duplicate_name\": false, \"tags\": [], \"languages\": [], \"locations\": [], \"channel_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet collection create --name=tom --bid=1.0 --channel_id=595c2e2f0c1f59188628fab7503692b0145779a2 --claims=ad25e05aa7dc5e9994869040c6103f9a8728db46",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"collection_create\", \"params\": {\"name\": \"tom\", \"bid\": \"1.0\", \"claims\": [\"ad25e05aa7dc5e9994869040c6103f9a8728db46\"], \"allow_duplicate_name\": false, \"tags\": [], \"languages\": [], \"locations\": [], \"channel_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"01000000012c1bc49ac50486afeda93e5b8bff99abc845c446215a05d57c24dc3d522dd304010000006b483045022100ced18ea9fe454cf44d94bb9a5d7e6915030fb6c8f79c0df2f68cbbdd30f5913702204c6d7aded8529aef37f9e800c7e4159c02ed6cf1308c4bac32726b76902733fb0121023bf6d860bb1c3b7c71d9d9c985a56bdba1c6f754fa351805f02d89a87ff0222cffffffff0200e1f5050000000091b503746f6d4c6f01a2795714b0923650b7fa288618591f0c2f2e5c591bb9bb615fa343bdb5e2194aa8b18986d5f001413697412ec4b5f1a529c971e90a63a27ebf78a5e512d5f1b87dde99e6e40aa4e9b710240536ec62c3f1b56df51a1812160a1446db28879a3f10c640908694995edca75ae025ad6d7576a914934ab2faddc49cbc3cad5a805f3e79c5c502d14888ac202c7e17000000001976a914e9d5fbcc88ada7ba2a27cdf59e70c57b339cbf9f88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"n44m2TuZKypifAkpPQqEN4VtRkCxgbgkA3\",\n \"amount\": \"4.947555\",\n \"confirmations\": 2,\n \"height\": 215,\n \"nout\": 1,\n \"timestamp\": 1655141672,\n \"txid\": \"04d32d523ddc247cd5055a2146c445c8ab99ff8b5b3ea9edaf8604c59ac41b2c\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mtwm7SVcxtxXHgkxwLfrPodkuo14wKgNrz\",\n \"amount\": \"1.0\",\n \"claim_id\": \"bd463bad61b8e420258f96e817b9540fa134ed23\",\n \"claim_op\": \"create\",\n \"confirmations\": -2,\n \"height\": -2,\n \"is_channel_signature_valid\": true,\n \"meta\": {},\n \"name\": \"tom\",\n \"normalized_name\": \"tom\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://tom#bd463bad61b8e420258f96e817b9540fa134ed23\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 7,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": null,\n \"txid\": \"a9954fce4be4598afcead007cec8f64bbcc8a899080f99bd1a0bfe07f2b501c7\",\n \"type\": \"claim\",\n \"value\": {\n \"claims\": [\n \"ad25e05aa7dc5e9994869040c6103f9a8728db46\"\n ]\n },\n \"value_type\": \"collection\"\n },\n {\n \"address\": \"n2qN5bEHEQ1keWyR6GbNd15dbHE6z1FoZb\",\n \"amount\": \"3.941448\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"a9954fce4be4598afcead007cec8f64bbcc8a899080f99bd1a0bfe07f2b501c7\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.006107\",\n \"total_input\": \"4.947555\",\n \"total_output\": \"4.941448\",\n \"txid\": \"a9954fce4be4598afcead007cec8f64bbcc8a899080f99bd1a0bfe07f2b501c7\"\n }\n}"
}
]
},
{
"name": "collection_list",
"description": "List my collection claims.",
"arguments": [
{
"name": "resolve",
"type": "bool",
"description": "resolve collection claim",
"is_required": false
},
{
"name": "resolve_claims",
"type": "int",
"description": "resolve every claim",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": [
{
"title": "List collections",
"curl": "curl -d'{\"method\": \"collection_list\", \"params\": {\"resolve_claims\": 1, \"resolve\": true}}' http://localhost:5279/",
"lbrynet": "lbrynet collection list --resolve --resolve_claims=1",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"collection_list\", \"params\": {\"resolve_claims\": 1, \"resolve\": true}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"mtwm7SVcxtxXHgkxwLfrPodkuo14wKgNrz\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5/tom#b\",\n \"claim_id\": \"bd463bad61b8e420258f96e817b9540fa134ed23\",\n \"claim_op\": \"create\",\n \"claims\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5/astream#a\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 4,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"meta\": {\n \"activation_height\": 214,\n \"creation_height\": 213,\n \"creation_timestamp\": 1655141671,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 714,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 213\n },\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"short_url\": \"lbry://astream#a\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"canonical_url\": \"lbry://@channel#5\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 8,\n \"has_signing_key\": false,\n \"height\": 210,\n \"meta\": {\n \"activation_height\": 210,\n \"claims_in_channel\": 2,\n \"creation_height\": 209,\n \"creation_timestamp\": 1655141670,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 710,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 209\n },\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"short_url\": \"lbry://@channel#5\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n ],\n \"confirmations\": 1,\n \"height\": 217,\n \"is_channel_signature_valid\": true,\n \"meta\": {\n \"activation_height\": 217,\n \"creation_height\": 217,\n \"creation_timestamp\": 1655141675,\n \"effective_amount\": \"1.0\",\n \"expiration_height\": 717,\n \"is_controlling\": true,\n \"reposted\": 0,\n \"support_amount\": \"0.0\",\n \"take_over_height\": 217\n },\n \"name\": \"tom\",\n \"normalized_name\": \"tom\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://tom#bd463bad61b8e420258f96e817b9540fa134ed23\",\n \"short_url\": \"lbry://tom#b\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 8,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141675,\n \"txid\": \"a9954fce4be4598afcead007cec8f64bbcc8a899080f99bd1a0bfe07f2b501c7\",\n \"type\": \"claim\",\n \"value\": {\n \"claims\": [\n \"ad25e05aa7dc5e9994869040c6103f9a8728db46\"\n ]\n },\n \"value_type\": \"collection\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "collection_resolve",
"description": "Resolve claims in the collection.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim id of the collection",
"is_required": false
},
{
"name": "url",
"type": "str",
"description": "url of the collection",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": []
},
{
"name": "collection_update",
"description": "Update an existing collection claim.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the collection to update",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": false
},
{
"name": "claims",
"type": "list",
"description": "claim ids",
"is_required": false
},
{
"name": "clear_claims",
"type": "bool",
"description": "clear existing claim references (prior to adding new ones)",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the collection",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the collection",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "add content tags",
"is_required": false
},
{
"name": "clear_tags",
"type": "bool",
"description": "clear existing tags (prior to adding new ones)",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the collection, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "clear_languages",
"type": "bool",
"description": "clear existing languages (prior to adding new ones)",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations of the collection, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "clear_locations",
"type": "bool",
"description": "clear existing locations (prior to adding new ones)",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the publisher channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of the publisher channel",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account in which to look for collection (default: all)",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the collection is sent",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
},
{
"name": "replace",
"type": "bool",
"description": "instead of modifying specific values on the collection, this will clear all existing values and only save passed in values, useful for form submissions where all values are always set",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
}
]
},
"file": {
"doc": "File management.",
"commands": [
{
"name": "file_delete",
"description": "Delete a LBRY file",
"arguments": [
{
"name": "delete_from_download_dir",
"type": "bool",
"description": "delete file from download directory, instead of just deleting blobs",
"is_required": false
},
{
"name": "delete_all",
"type": "bool",
"description": "if there are multiple matching files, allow the deletion of multiple files. Otherwise do not delete anything.",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "delete by file sd hash",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "delete by file name in downloads folder",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "delete by file stream hash",
"is_required": false
},
{
"name": "rowid",
"type": "int",
"description": "delete by file row id",
"is_required": false
},
{
"name": "claim_id",
"type": "str",
"description": "delete by file claim id",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "delete by file claim txid",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "delete by file claim nout",
"is_required": false
},
{
"name": "claim_name",
"type": "str",
"description": "delete by file claim name",
"is_required": false
},
{
"name": "channel_claim_id",
"type": "str",
"description": "delete by file channel claim id",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "delete by file channel claim name",
"is_required": false
}
],
"returns": "(bool) true if deletion was successful",
"examples": [
{
"title": "Delete a file",
"curl": "curl -d'{\"method\": \"file_delete\", \"params\": {\"delete_from_download_dir\": false, \"delete_all\": false, \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\"}}' http://localhost:5279/",
"lbrynet": "lbrynet file delete --claim_id=\"ad25e05aa7dc5e9994869040c6103f9a8728db46\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"file_delete\", \"params\": {\"delete_from_download_dir\": false, \"delete_all\": false, \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": true\n}"
}
]
},
{
"name": "file_list",
"description": "List files limited by optional filters",
"arguments": [
{
"name": "sd_hash",
"type": "str",
"description": "get file with matching sd hash",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "get file with matching file name in the downloads folder",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "get file with matching stream hash",
"is_required": false
},
{
"name": "rowid",
"type": "int",
"description": "get file with matching row id",
"is_required": false
},
{
"name": "added_on",
"type": "int",
"description": "get file with matching time of insertion",
"is_required": false
},
{
"name": "claim_id",
"type": "str",
"description": "get file with matching claim id(s)",
"is_required": false
},
{
"name": "outpoint",
"type": "str",
"description": "get file with matching claim outpoint(s)",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "get file with matching claim txid",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "get file with matching claim nout",
"is_required": false
},
{
"name": "channel_claim_id",
"type": "str",
"description": "get file with matching channel claim id(s)",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "get file with matching channel name",
"is_required": false
},
{
"name": "claim_name",
"type": "str",
"description": "get file with matching claim name",
"is_required": false
},
{
"name": "blobs_in_stream",
"type": "int",
"description": "get file with matching blobs in stream",
"is_required": false
},
{
"name": "download_path",
"type": "str",
"description": "get file with matching download path",
"is_required": false
},
{
"name": "uploading_to_reflector",
"type": "bool",
"description": "get files currently uploading to reflector",
"is_required": false
},
{
"name": "is_fully_reflected",
"type": "bool",
"description": "get files that have been uploaded to reflector",
"is_required": false
},
{
"name": "status",
"type": "str",
"description": "match by status, ( running | finished | stopped )",
"is_required": false
},
{
"name": "completed",
"type": "bool",
"description": "match only completed",
"is_required": false
},
{
"name": "blobs_remaining",
"type": "int",
"description": "amount of remaining blobs to download",
"is_required": false
},
{
"name": "sort",
"type": "str",
"description": "field to sort by (one of the above filter fields)",
"is_required": false
},
{
"name": "comparison",
"type": "str",
"description": "logical comparison, (eq | ne | g | ge | l | le | in)",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "add purchase receipts from this wallet",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"streaming_url\": \"(str) url to stream the file using range requests\",\n \"completed\": \"(bool) true if download is completed\",\n \"file_name\": \"(str) name of file\",\n \"download_directory\": \"(str) download directory\",\n \"points_paid\": \"(float) credit paid to download file\",\n \"stopped\": \"(bool) true if download is stopped\",\n \"stream_hash\": \"(str) stream hash of file\",\n \"stream_name\": \"(str) stream name\",\n \"suggested_file_name\": \"(str) suggested file name\",\n \"sd_hash\": \"(str) sd hash of file\",\n \"download_path\": \"(str) download path of file\",\n \"mime_type\": \"(str) mime type of file\",\n \"key\": \"(str) key attached to file\",\n \"total_bytes_lower_bound\": \"(int) lower bound file size in bytes\",\n \"total_bytes\": \"(int) file upper bound size in bytes\",\n \"written_bytes\": \"(int) written size in bytes\",\n \"blobs_completed\": \"(int) number of fully downloaded blobs\",\n \"blobs_in_stream\": \"(int) total blobs on stream\",\n \"blobs_remaining\": \"(int) total blobs remaining to download\",\n \"status\": \"(str) downloader status\",\n \"claim_id\": \"(str) None if claim is not found else the claim id\",\n \"txid\": \"(str) None if claim is not found else the transaction id\",\n \"nout\": \"(int) None if claim is not found else the transaction output index\",\n \"outpoint\": \"(str) None if claim is not found else the tx and output\",\n \"metadata\": \"(dict) None if claim is not found else the claim metadata\",\n \"channel_claim_id\": \"(str) None if claim is not found or not signed\",\n \"channel_name\": \"(str) None if claim is not found or not signed\",\n \"claim_name\": \"(str) None if claim is not found else the claim name\",\n \"reflector_progress\": \"(int) reflector upload progress, 0 to 100\",\n \"uploading_to_reflector\": \"(bool) set to True when currently uploading to reflector\"\n }\n ]\n }",
"examples": [
{
"title": "List local files",
"curl": "curl -d'{\"method\": \"file_list\", \"params\": {\"reverse\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet file list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"file_list\", \"params\": {\"reverse\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"added_on\": null,\n \"blobs_completed\": 1,\n \"blobs_in_stream\": 1,\n \"blobs_remaining\": 0,\n \"channel_claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"channel_name\": \"@channel\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_name\": \"astream\",\n \"completed\": true,\n \"confirmations\": -1,\n \"content_fee\": null,\n \"download_directory\": null,\n \"download_path\": null,\n \"file_name\": null,\n \"height\": -1,\n \"is_fully_reflected\": true,\n \"key\": \"66f888fe00cf558494c2fcbd5903d00d\",\n \"metadata\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"mime_type\": \"application/octet-stream\",\n \"nout\": 0,\n \"outpoint\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234:0\",\n \"points_paid\": 0.0,\n \"protobuf\": \"01a2795714b0923650b7fa288618591f0c2f2e5c5961baf110460a527e38b68f0653c1c79f2cae1f57ade55c009fb7578175c9d93d7edb0546d1378e1d33d99df8df0d5cfe9e7a5b63053a4e7ce003f95f5890b27f0a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d707238333268703178180b22186170706c69636174696f6e2f6f637465742d73747265616d32309ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"purchase_receipt\": null,\n \"reflector_progress\": 0,\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"status\": \"finished\",\n \"stopped\": true,\n \"stream_hash\": \"c48ff9950efbcb78b20d311467b1b0e321069ef5ece96898eb08a27a62d1ef2594cd24f1e8d6993f00c48fd6a4221490\",\n \"stream_name\": \"tmpr832hp1x\",\n \"streaming_url\": \"http://localhost:5280/stream/9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"suggested_file_name\": \"tmpr832hp1x\",\n \"timestamp\": null,\n \"total_bytes\": 16,\n \"total_bytes_lower_bound\": 0,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"uploading_to_reflector\": false,\n \"written_bytes\": 0\n },\n {\n \"added_on\": null,\n \"blobs_completed\": 1,\n \"blobs_in_stream\": 1,\n \"blobs_remaining\": 0,\n \"channel_claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"channel_name\": \"@channel\",\n \"claim_id\": \"46bff43fbdffabf12efc867c2b4c7865315b0b76\",\n \"claim_name\": \"blank-image\",\n \"completed\": false,\n \"confirmations\": -1,\n \"content_fee\": null,\n \"download_directory\": null,\n \"download_path\": null,\n \"file_name\": null,\n \"height\": -1,\n \"is_fully_reflected\": true,\n \"key\": \"b2c6c26d1094b988fbf3313086f2a256\",\n \"metadata\": {\n \"author\": \"Picaso\",\n \"description\": \"A blank PNG that is 5x7.\",\n \"fee\": {\n \"address\": \"mnHsWiDjPYw27jPzMfqUFM9scdsgHQ6NJg\",\n \"amount\": \"0.3\",\n \"currency\": \"LBC\"\n },\n \"image\": {\n \"height\": 7,\n \"width\": 5\n },\n \"languages\": [\n \"en\"\n ],\n \"license\": \"Public Domain\",\n \"license_url\": \"http://public-domain.org\",\n \"locations\": [\n {\n \"city\": \"Manchester\",\n \"country\": \"US\",\n \"state\": \"NH\"\n }\n ],\n \"release_time\": \"1655141671\",\n \"source\": {\n \"hash\": \"6c7df435d412c603390f593ef658c199817c7830ba3f16b7eadd8f99fa50e85dbd0d2b3dc61eadc33fe096e3872d1545\",\n \"media_type\": \"image/png\",\n \"name\": \"tmps0do5cfj.png\",\n \"sd_hash\": \"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\",\n \"size\": \"99\"\n },\n \"stream_type\": \"image\",\n \"tags\": [\n \"blank\",\n \"art\"\n ],\n \"thumbnail\": {\n \"url\": \"http://smallmedia.com/thumbnail.jpg\"\n },\n \"title\": \"Blank Image\"\n },\n \"mime_type\": \"image/png\",\n \"nout\": 0,\n \"outpoint\": \"04d32d523ddc247cd5055a2146c445c8ab99ff8b5b3ea9edaf8604c59ac41b2c:0\",\n \"points_paid\": 0.0,\n \"protobuf\": \"01a2795714b0923650b7fa288618591f0c2f2e5c5946580bd06bec18e960285651ca9130c75fcf4f5b3cd71fa5d97fe7013de1bf7627b08de186dc0f5c4ff33a6e7173b24f7da6a91080ffb840f6f6fbf4897851790ae6010a82010a306c7df435d412c603390f593ef658c199817c7830ba3f16b7eadd8f99fa50e85dbd0d2b3dc61eadc33fe096e3872d1545120f746d707330646f3563666a2e706e6718632209696d6167652f706e673230d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996120650696361736f1a0d5075626c696320446f6d61696e2218687474703a2f2f7075626c69632d646f6d61696e2e6f726728a7ea9d95063222080112196f4a4f7b3ab55c52fe12f9d8b9b255fb2625522e4b00415bed188087a70e520408051007420b426c616e6b20496d6167654a184120626c616e6b20504e472074686174206973203578372e52252a23687474703a2f2f736d616c6c6d656469612e636f6d2f7468756d626e61696c2e6a70675a05626c616e6b5a03617274620208016a1308ec0112024e481a0a4d616e63686573746572\",\n \"purchase_receipt\": null,\n \"reflector_progress\": 0,\n \"sd_hash\": \"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\",\n \"status\": \"finished\",\n \"stopped\": true,\n \"stream_hash\": \"eec0d9d4bf368e6e8abe11a0ceaf6f27606f11c1868d415e1470baa316400d6517f6092974ec5c58018b30b3e8e5b884\",\n \"stream_name\": \"tmps0do5cfj.png\",\n \"streaming_url\": \"http://localhost:5280/stream/d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\",\n \"suggested_file_name\": \"tmps0do5cfj.png\",\n \"timestamp\": null,\n \"total_bytes\": 112,\n \"total_bytes_lower_bound\": 96,\n \"txid\": \"04d32d523ddc247cd5055a2146c445c8ab99ff8b5b3ea9edaf8604c59ac41b2c\",\n \"uploading_to_reflector\": false,\n \"written_bytes\": 0\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 2,\n \"total_pages\": 1\n }\n}"
},
{
"title": "List files matching a parameter",
"curl": "curl -d'{\"method\": \"file_list\", \"params\": {\"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\", \"reverse\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet file list --claim_id=\"ad25e05aa7dc5e9994869040c6103f9a8728db46\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"file_list\", \"params\": {\"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\", \"reverse\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"added_on\": null,\n \"blobs_completed\": 1,\n \"blobs_in_stream\": 1,\n \"blobs_remaining\": 0,\n \"channel_claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"channel_name\": \"@channel\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_name\": \"astream\",\n \"completed\": true,\n \"confirmations\": 4,\n \"content_fee\": null,\n \"download_directory\": null,\n \"download_path\": null,\n \"file_name\": null,\n \"height\": 214,\n \"is_fully_reflected\": true,\n \"key\": \"66f888fe00cf558494c2fcbd5903d00d\",\n \"metadata\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"mime_type\": \"application/octet-stream\",\n \"nout\": 0,\n \"outpoint\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234:0\",\n \"points_paid\": 0.0,\n \"protobuf\": \"01a2795714b0923650b7fa288618591f0c2f2e5c5961baf110460a527e38b68f0653c1c79f2cae1f57ade55c009fb7578175c9d93d7edb0546d1378e1d33d99df8df0d5cfe9e7a5b63053a4e7ce003f95f5890b27f0a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d707238333268703178180b22186170706c69636174696f6e2f6f637465742d73747265616d32309ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"purchase_receipt\": null,\n \"reflector_progress\": 0,\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"status\": \"finished\",\n \"stopped\": true,\n \"stream_hash\": \"c48ff9950efbcb78b20d311467b1b0e321069ef5ece96898eb08a27a62d1ef2594cd24f1e8d6993f00c48fd6a4221490\",\n \"stream_name\": \"tmpr832hp1x\",\n \"streaming_url\": \"http://localhost:5280/stream/9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"suggested_file_name\": \"tmpr832hp1x\",\n \"timestamp\": 1655141671,\n \"total_bytes\": 16,\n \"total_bytes_lower_bound\": 0,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"uploading_to_reflector\": false,\n \"written_bytes\": 0\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "file_reflect",
"description": "Reflect all the blobs in a file matching the filter criteria",
"arguments": [
{
"name": "sd_hash",
"type": "str",
"description": "get file with matching sd hash",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "get file with matching file name in the downloads folder",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "get file with matching stream hash",
"is_required": false
},
{
"name": "rowid",
"type": "int",
"description": "get file with matching row id",
"is_required": false
},
{
"name": "reflector",
"type": "str",
"description": "reflector server, ip address or url by default choose a server from the config",
"is_required": false
}
],
"returns": "(list) list of blobs reflected",
"examples": []
},
{
"name": "file_save",
"description": "Start saving a file to disk.",
"arguments": [
{
"name": "file_name",
"type": "str",
"description": "file name to save to",
"is_required": false
},
{
"name": "download_directory",
"type": "str",
"description": "directory to save into",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "save file with matching sd hash",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "save file with matching stream hash",
"is_required": false
},
{
"name": "rowid",
"type": "int",
"description": "save file with matching row id",
"is_required": false
},
{
"name": "claim_id",
"type": "str",
"description": "save file with matching claim id",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "save file with matching claim txid",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "save file with matching claim nout",
"is_required": false
},
{
"name": "claim_name",
"type": "str",
"description": "save file with matching claim name",
"is_required": false
},
{
"name": "channel_claim_id",
"type": "str",
"description": "save file with matching channel claim id",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "save file with matching channel claim name",
"is_required": false
}
],
"returns": " {\n \"streaming_url\": \"(str) url to stream the file using range requests\",\n \"completed\": \"(bool) true if download is completed\",\n \"file_name\": \"(str) name of file\",\n \"download_directory\": \"(str) download directory\",\n \"points_paid\": \"(float) credit paid to download file\",\n \"stopped\": \"(bool) true if download is stopped\",\n \"stream_hash\": \"(str) stream hash of file\",\n \"stream_name\": \"(str) stream name\",\n \"suggested_file_name\": \"(str) suggested file name\",\n \"sd_hash\": \"(str) sd hash of file\",\n \"download_path\": \"(str) download path of file\",\n \"mime_type\": \"(str) mime type of file\",\n \"key\": \"(str) key attached to file\",\n \"total_bytes_lower_bound\": \"(int) lower bound file size in bytes\",\n \"total_bytes\": \"(int) file upper bound size in bytes\",\n \"written_bytes\": \"(int) written size in bytes\",\n \"blobs_completed\": \"(int) number of fully downloaded blobs\",\n \"blobs_in_stream\": \"(int) total blobs on stream\",\n \"blobs_remaining\": \"(int) total blobs remaining to download\",\n \"status\": \"(str) downloader status\",\n \"claim_id\": \"(str) None if claim is not found else the claim id\",\n \"txid\": \"(str) None if claim is not found else the transaction id\",\n \"nout\": \"(int) None if claim is not found else the transaction output index\",\n \"outpoint\": \"(str) None if claim is not found else the tx and output\",\n \"metadata\": \"(dict) None if claim is not found else the claim metadata\",\n \"channel_claim_id\": \"(str) None if claim is not found or not signed\",\n \"channel_name\": \"(str) None if claim is not found or not signed\",\n \"claim_name\": \"(str) None if claim is not found else the claim name\",\n \"reflector_progress\": \"(int) reflector upload progress, 0 to 100\",\n \"uploading_to_reflector\": \"(bool) set to True when currently uploading to reflector\"\n }",
"examples": [
{
"title": "Save a file to the downloads directory",
"curl": "curl -d'{\"method\": \"file_save\", \"params\": {\"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\"}}' http://localhost:5279/",
"lbrynet": "lbrynet file save --sd_hash=\"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"file_save\", \"params\": {\"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"added_on\": 1655141677,\n \"blobs_completed\": 1,\n \"blobs_in_stream\": 1,\n \"blobs_remaining\": 0,\n \"channel_claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"channel_name\": \"@channel\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_name\": \"astream\",\n \"completed\": true,\n \"confirmations\": 4,\n \"content_fee\": null,\n \"download_directory\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd\",\n \"download_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd/tmpr832hp1x_1\",\n \"file_name\": \"tmpr832hp1x_1\",\n \"height\": 214,\n \"is_fully_reflected\": false,\n \"key\": \"66f888fe00cf558494c2fcbd5903d00d\",\n \"metadata\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"mime_type\": \"application/octet-stream\",\n \"nout\": 0,\n \"outpoint\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234:0\",\n \"points_paid\": 0.0,\n \"protobuf\": \"01a2795714b0923650b7fa288618591f0c2f2e5c5961baf110460a527e38b68f0653c1c79f2cae1f57ade55c009fb7578175c9d93d7edb0546d1378e1d33d99df8df0d5cfe9e7a5b63053a4e7ce003f95f5890b27f0a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d707238333268703178180b22186170706c69636174696f6e2f6f637465742d73747265616d32309ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"purchase_receipt\": null,\n \"reflector_progress\": 0,\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"status\": \"finished\",\n \"stopped\": true,\n \"stream_hash\": \"c48ff9950efbcb78b20d311467b1b0e321069ef5ece96898eb08a27a62d1ef2594cd24f1e8d6993f00c48fd6a4221490\",\n \"stream_name\": \"tmpr832hp1x\",\n \"streaming_url\": \"http://localhost:5280/stream/9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"suggested_file_name\": \"tmpr832hp1x\",\n \"timestamp\": 1655141671,\n \"total_bytes\": 16,\n \"total_bytes_lower_bound\": 0,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"uploading_to_reflector\": false,\n \"written_bytes\": 11\n }\n}"
}
]
},
{
"name": "file_set_status",
"description": "Start or stop downloading a file",
"arguments": [
{
"name": "status",
"type": "str",
"description": "one of \"start\" or \"stop\"",
"is_required": true
},
{
"name": "sd_hash",
"type": "str",
"description": "set status of file with matching sd hash",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "set status of file with matching file name in the downloads folder",
"is_required": false
},
{
"name": "stream_hash",
"type": "str",
"description": "set status of file with matching stream hash",
"is_required": false
},
{
"name": "rowid",
"type": "int",
"description": "set status of file with matching row id",
"is_required": false
}
],
"returns": "(str) Confirmation message",
"examples": []
}
]
},
"peer": {
"doc": "DHT / Blob Exchange peer commands.",
"commands": [
{
"name": "peer_list",
"description": "Get peers for blob hash",
"arguments": [
{
"name": "blob_hash",
"type": "str",
"description": "find available peers for this blob hash",
"is_required": true
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": "(list) List of contact dictionaries {'address': , 'udp_port': , 'tcp_port': ,\n 'node_id': }",
"examples": []
},
{
"name": "peer_ping",
"description": "Send a kademlia ping to the specified peer. If address and port are provided the peer is directly pinged,\nif not provided the peer is located first.",
"arguments": [],
"returns": "(str) pong, or {'error': } if an error is encountered",
"examples": []
}
]
},
"preference": {
"doc": "Preferences management.",
"commands": [
{
"name": "preference_get",
"description": "Get preference value for key or all values if not key is passed in.",
"arguments": [
{
"name": "key",
"type": "str",
"description": "key associated with value",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(dict) Dictionary of preference(s)",
"examples": [
{
"title": "Get preferences",
"curl": "curl -d'{\"method\": \"preference_get\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet preference get",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"preference_get\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"theme\": \"dark\"\n }\n}"
}
]
},
{
"name": "preference_set",
"description": "Set preferences",
"arguments": [
{
"name": "key",
"type": "str",
"description": "key associated with value",
"is_required": true
},
{
"name": "value",
"type": "str",
"description": "key associated with value",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(dict) Dictionary with key/value of new preference",
"examples": [
{
"title": "Set preference",
"curl": "curl -d'{\"method\": \"preference_set\", \"params\": {\"key\": \"theme\", \"value\": \"dark\"}}' http://localhost:5279/",
"lbrynet": "lbrynet preference set \"theme\" \"dark\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"preference_set\", \"params\": {\"key\": \"theme\", \"value\": \"dark\"}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"theme\": \"dark\"\n }\n}"
}
]
}
]
},
"purchase": {
"doc": "List and make purchases of claims.",
"commands": [
{
"name": "purchase_create",
"description": "Purchase a claim.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim id of claim to purchase",
"is_required": false
},
{
"name": "url",
"type": "str",
"description": "lookup claim to purchase by url",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "allow_duplicate_purchase",
"type": "bool",
"description": "allow purchasing claim_id you already own",
"is_required": false
},
{
"name": "override_max_key_fee",
"type": "bool",
"description": "ignore max key fee for this purchase",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "purchase_list",
"description": "List my claim purchases.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "purchases for specific claim",
"is_required": false
},
{
"name": "resolve",
"type": "str",
"description": "include resolved claim information",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": []
}
]
},
"settings": {
"doc": "Settings management.",
"commands": [
{
"name": "settings_clear",
"description": "Clear daemon settings",
"arguments": [],
"returns": "(dict) Updated dictionary of daemon settings",
"examples": []
},
{
"name": "settings_get",
"description": "Get daemon settings",
"arguments": [],
"returns": "(dict) Dictionary of daemon settings\n See ADJUSTABLE_SETTINGS in lbry/conf.py for full list of settings",
"examples": [
{
"title": "Get settings",
"curl": "curl -d'{\"method\": \"settings_get\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet settings get",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"settings_get\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"allowed_origin\": \"\",\n \"announce_head_and_sd_only\": true,\n \"api\": \"localhost:5279\",\n \"audio_encoder\": \"aac -b:a 160k\",\n \"blob_download_timeout\": 30.0,\n \"blob_lru_cache_size\": 0,\n \"blob_storage_limit\": 0,\n \"blockchain_name\": \"lbrycrd_regtest\",\n \"coin_selection_strategy\": \"prefer_confirmed\",\n \"components_to_skip\": [\n \"dht\",\n \"upnp\",\n \"hash_announcer\",\n \"peer_protocol_server\",\n \"libtorrent_component\"\n ],\n \"concurrent_blob_announcers\": 10,\n \"concurrent_hub_requests\": 32,\n \"concurrent_reflector_uploads\": 10,\n \"config\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmp_dufqex7/daemon_settings.yml\",\n \"data_dir\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd\",\n \"download_dir\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd\",\n \"download_timeout\": 30.0,\n \"ffmpeg_path\": \"\",\n \"fixed_peer_delay\": 2.0,\n \"fixed_peers\": [\n [\n \"127.0.0.1\",\n 5567\n ]\n ],\n \"hub_timeout\": 30.0,\n \"jurisdiction\": null,\n \"known_dht_nodes\": [],\n \"lbryum_servers\": [\n [\n \"localhost\",\n 50002\n ]\n ],\n \"max_connections_per_download\": 4,\n \"max_key_fee\": {\n \"amount\": 50.0,\n \"currency\": \"USD\"\n },\n \"max_wallet_server_fee\": \"0.0\",\n \"network_interface\": \"0.0.0.0\",\n \"network_storage_limit\": 0,\n \"node_rpc_timeout\": 5.0,\n \"peer_connect_timeout\": 3.0,\n \"prometheus_port\": 0,\n \"reflect_streams\": true,\n \"reflector_servers\": [\n [\n \"127.0.0.1\",\n 5566\n ]\n ],\n \"save_blobs\": true,\n \"save_files\": true,\n \"save_resolved_claims\": true,\n \"share_usage_data\": false,\n \"split_buckets_under_index\": 2,\n \"streaming_get\": true,\n \"streaming_server\": \"localhost:5280\",\n \"tcp_port\": 4444,\n \"track_bandwidth\": true,\n \"tracker_servers\": [\n [\n \"tracker.lbry.com\",\n 9252\n ],\n [\n \"tracker.lbry.grin.io\",\n 9252\n ]\n ],\n \"transaction_cache_size\": 10000,\n \"udp_port\": 4444,\n \"use_upnp\": false,\n \"video_bitrate_maximum\": 5000000,\n \"video_encoder\": \"libx264 -crf 24 -preset faster -pix_fmt yuv420p\",\n \"video_scaler\": \"-vf \\\"scale=if(gte(iw\\\\,ih)\\\\,min(1920\\\\,iw)\\\\,-2):if(lt(iw\\\\,ih)\\\\,min(1920\\\\,ih)\\\\,-2)\\\" -maxrate 5500K -bufsize 5000K\",\n \"volume_analysis_time\": 240,\n \"volume_filter\": \"\",\n \"wallet_dir\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpfx0nk2jd\",\n \"wallets\": [\n \"default_wallet\"\n ]\n }\n}"
}
]
},
{
"name": "settings_set",
"description": "Set daemon settings",
"arguments": [],
"returns": "(dict) Updated dictionary of daemon settings",
"examples": [
{
"title": "Set settings",
"curl": "curl -d'{\"method\": \"settings_set\", \"params\": {\"key\": \"tcp_port\", \"value\": 99}}' http://localhost:5279/",
"lbrynet": "lbrynet settings set \"tcp_port\" 99",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"settings_set\", \"params\": {\"key\": \"tcp_port\", \"value\": 99}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"tcp_port\": 99\n }\n}"
}
]
}
]
},
"stream": {
"doc": "Create, update, abandon, list and inspect your stream claims.",
"commands": [
{
"name": "stream_abandon",
"description": "Abandon one of my stream claims.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the claim to abandon",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "txid of the claim to abandon",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "nout of the claim to abandon",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until abandon is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Abandon a stream claim",
"curl": "curl -d'{\"method\": \"stream_abandon\", \"params\": {\"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\", \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet stream abandon ad25e05aa7dc5e9994869040c6103f9a8728db46",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"stream_abandon\", \"params\": {\"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\", \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"010000000134627f9ba4ba51befc3e15e8f60f7151e171618922ef727e1ee5effa87c47075000000006a47304402206abd1bb758c3bac2e23a22e93bf2166d29a27215eb297fd063f823cd3c68898b0220301c9f4dd874a90a877ab135153ba7bb1d9d93de8ed1a5db6d9653ece6c8e46b01210200ae7b8f7a6220cb0f3699aeb1b2f1ce7825554f50b6cf542e63f3018e9852b6ffffffff0134b7f505000000001976a9143bd49c5746cb0cbba81dc36420123d208f30600488ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 4,\n \"height\": 214,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mkyJrpgUxpSyVdb6bA36F2cZQfZpWa7Pci\",\n \"amount\": \"0.999893\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 0,\n \"timestamp\": null,\n \"txid\": \"3ef9d8954a84d44a1b3ddf95114e6b77bcbd2cfe5af0d509b1b8a3d9aada3182\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.000107\",\n \"total_input\": \"1.0\",\n \"total_output\": \"0.999893\",\n \"txid\": \"3ef9d8954a84d44a1b3ddf95114e6b77bcbd2cfe5af0d509b1b8a3d9aada3182\"\n }\n}"
}
]
},
{
"name": "stream_cost_estimate",
"description": "Get estimated cost for a lbry stream",
"arguments": [
{
"name": "uri",
"type": "str",
"description": "uri to use",
"is_required": true
}
],
"returns": "(float) Estimated cost in lbry credits, returns None if uri is not\n resolvable",
"examples": []
},
{
"name": "stream_create",
"description": "Make a new stream claim and announce the associated file to lbrynet.",
"arguments": [
{
"name": "name",
"type": "str",
"description": "name of the content (can only consist of a-z A-Z 0-9 and -(dash))",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": true
},
{
"name": "file_path",
"type": "str",
"description": "path to file to be associated with name.",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "name of file to be associated with stream.",
"is_required": false
},
{
"name": "file_hash",
"type": "str",
"description": "hash of file to be associated with stream.",
"is_required": false
},
{
"name": "validate_file",
"type": "bool",
"description": "validate that the video container and encodings match common web browser support or that optimization succeeds if specified. FFmpeg is required",
"is_required": false
},
{
"name": "optimize_file",
"type": "bool",
"description": "transcode the video & audio if necessary to ensure common web browser support. FFmpeg is required",
"is_required": false
},
{
"name": "allow_duplicate_name",
"type": "bool",
"description": "create new claim even if one already exists with given name. default: false.",
"is_required": false
},
{
"name": "fee_currency",
"type": "string",
"description": "specify fee currency",
"is_required": false
},
{
"name": "fee_amount",
"type": "decimal",
"description": "content download fee",
"is_required": false
},
{
"name": "fee_address",
"type": "str",
"description": "address where to send fee payments, will use value from --claim_address if not provided",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the publication",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the publication",
"is_required": false
},
{
"name": "author",
"type": "str",
"description": "author of the publication. The usage for this field is not the same as for channels. The author field is used to credit an author who is not the publisher and is not represented by the channel. For example, a pdf file of 'The Odyssey' has an author of 'Homer' but may by published to a channel such as '@classics', or to no channel at all",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "add content tags",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the channel, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations relevant to the stream, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "license",
"type": "str",
"description": "publication license",
"is_required": false
},
{
"name": "license_url",
"type": "str",
"description": "publication license url",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "release_time",
"type": "int",
"description": "original public release of content, seconds since UNIX epoch",
"is_required": false
},
{
"name": "width",
"type": "int",
"description": "image/video width, automatically calculated from media file",
"is_required": false
},
{
"name": "height",
"type": "int",
"description": "image/video height, automatically calculated from media file",
"is_required": false
},
{
"name": "duration",
"type": "int",
"description": "audio/video duration in seconds, automatically calculated",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "sd_hash of stream",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the publisher channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of the publisher channel",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account to use for holding the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the claim is sent to, if not specified it will be determined automatically from the account",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Create a stream claim without metadata",
"curl": "curl -d'{\"method\": \"stream_create\", \"params\": {\"name\": \"astream\", \"bid\": \"1.0\", \"file_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpr832hp1x\", \"validate_file\": false, \"optimize_file\": false, \"tags\": [], \"languages\": [], \"locations\": [], \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet stream create astream 1.0 /var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpr832hp1x",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"stream_create\", \"params\": {\"name\": \"astream\", \"bid\": \"1.0\", \"file_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmpr832hp1x\", \"validate_file\": false, \"optimize_file\": false, \"tags\": [], \"languages\": [], \"locations\": [], \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"01000000016419a92bda9f0ef64bde16f5a322d5668d68d53b4e38eb99b4738f72dbe880de010000006b483045022100d6963914b5e07f2c89f15f743c924919f51bbccbf1382395e54ba31cf67c9d5f02205cad973318f58d2841bda1f4c27be312881d2c32974b25d612106fa99cf53828012103c1b9bc894048c2d8d7091ed0d92d1b9468343eba19470bba436c0c4ee2cccb96ffffffff0200e1f50500000000bab5076173747265616d4c94000a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d707238333268703178180b22186170706c69636174696f6e2f6f637465742d73747265616d32309ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c836d7576a9143d39ffed222af9cdea57d6d388ebba0aeda3039d88ac38fb9423000000001976a914152e48d498f73f383b15e73d6504ba35e6f4639e88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mwtvw9x13nsVo6xkkrt3RFkk5h6TAjgPdK\",\n \"amount\": \"6.983769\",\n \"confirmations\": 4,\n \"height\": 209,\n \"nout\": 1,\n \"timestamp\": 1655141670,\n \"txid\": \"de80e8db728f73b499eb384e3bd5688d66d522a3f516de4bf60e9fda2ba91964\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"create\",\n \"confirmations\": -2,\n \"height\": -2,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"timestamp\": null,\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"mhSwvz7Qfuh343S8WrXpPoPcaxbtw7QM5W\",\n \"amount\": \"5.969662\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.014107\",\n \"total_input\": \"6.983769\",\n \"total_output\": \"6.969662\",\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\"\n }\n}"
},
{
"title": "Create an image stream claim with all metadata and fee",
"curl": "curl -d'{\"method\": \"stream_create\", \"params\": {\"name\": \"blank-image\", \"bid\": \"1.0\", \"file_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmps0do5cfj.png\", \"validate_file\": false, \"optimize_file\": false, \"fee_currency\": \"LBC\", \"fee_amount\": \"0.3\", \"title\": \"Blank Image\", \"description\": \"A blank PNG that is 5x7.\", \"author\": \"Picaso\", \"tags\": [\"blank\", \"art\"], \"languages\": [\"en\"], \"locations\": [\"US:NH:Manchester\"], \"license\": \"Public Domain\", \"license_url\": \"http://public-domain.org\", \"thumbnail_url\": \"http://smallmedia.com/thumbnail.jpg\", \"release_time\": 1655141671, \"channel_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet stream create blank-image 1.0 /var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmps0do5cfj.png --tags=blank --tags=art --languages=en --locations=US:NH:Manchester --fee_currency=LBC --fee_amount=0.3 --title=\"Blank Image\" --description=\"A blank PNG that is 5x7.\" --author=Picaso --license=\"Public Domain\" --license_url=http://public-domain.org --thumbnail_url=\"http://smallmedia.com/thumbnail.jpg\" --release_time=1655141671 --channel_id=\"595c2e2f0c1f59188628fab7503692b0145779a2\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"stream_create\", \"params\": {\"name\": \"blank-image\", \"bid\": \"1.0\", \"file_path\": \"/var/folders/46/44w2zhrx16b8gsvff9dxtr640000gq/T/tmps0do5cfj.png\", \"validate_file\": false, \"optimize_file\": false, \"fee_currency\": \"LBC\", \"fee_amount\": \"0.3\", \"title\": \"Blank Image\", \"description\": \"A blank PNG that is 5x7.\", \"author\": \"Picaso\", \"tags\": [\"blank\", \"art\"], \"languages\": [\"en\"], \"locations\": [\"US:NH:Manchester\"], \"license\": \"Public Domain\", \"license_url\": \"http://public-domain.org\", \"thumbnail_url\": \"http://smallmedia.com/thumbnail.jpg\", \"release_time\": 1655141671, \"channel_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"010000000144a3bd0e72a6f87a1e2282dadf3a332eb04eba7dac715f14a6ac36288eece7f9010000006b483045022100d1327306e6b07c668e21b915e0a9bbe7e08036ffc6a197f469ff458b5108be6e02207c4160f2c7e0344a2dc30beb0697bee536118df5e6cb978de072bb745b2783d60121026a72cb1ef362624b1483427abf7cb2731c1dd582bad08b4521a1e92967cbda32ffffffff0200e1f50500000000fddc01b50b626c616e6b2d696d6167654db10101a2795714b0923650b7fa288618591f0c2f2e5c5946580bd06bec18e960285651ca9130c75fcf4f5b3cd71fa5d97fe7013de1bf7627b08de186dc0f5c4ff33a6e7173b24f7da6a91080ffb840f6f6fbf4897851790ae6010a82010a306c7df435d412c603390f593ef658c199817c7830ba3f16b7eadd8f99fa50e85dbd0d2b3dc61eadc33fe096e3872d1545120f746d707330646f3563666a2e706e6718632209696d6167652f706e673230d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996120650696361736f1a0d5075626c696320446f6d61696e2218687474703a2f2f7075626c69632d646f6d61696e2e6f726728a7ea9d95063222080112196f4a4f7b3ab55c52fe12f9d8b9b255fb2625522e4b00415bed188087a70e520408051007420b426c616e6b20496d6167654a184120626c616e6b20504e472074686174206973203578372e52252a23687474703a2f2f736d616c6c6d656469612e636f6d2f7468756d626e61696c2e6a70675a05626c616e6b5a03617274620208016a1308ec0112024e481a0a4d616e636865737465726d7576a9144a4f7b3ab55c52fe12f9d8b9b255fb2625522e4b88acac5e7d1d000000001976a914f756abdb56de34ac287ee4b85ca911e2f83894b788ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mhSwvz7Qfuh343S8WrXpPoPcaxbtw7QM5W\",\n \"amount\": \"5.969662\",\n \"confirmations\": 2,\n \"height\": 213,\n \"nout\": 1,\n \"timestamp\": 1655141671,\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mnHsWiDjPYw27jPzMfqUFM9scdsgHQ6NJg\",\n \"amount\": \"1.0\",\n \"claim_id\": \"46bff43fbdffabf12efc867c2b4c7865315b0b76\",\n \"claim_op\": \"create\",\n \"confirmations\": -2,\n \"height\": -2,\n \"is_channel_signature_valid\": true,\n \"meta\": {},\n \"name\": \"blank-image\",\n \"normalized_name\": \"blank-image\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://blank-image#46bff43fbdffabf12efc867c2b4c7865315b0b76\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": null,\n \"txid\": \"04d32d523ddc247cd5055a2146c445c8ab99ff8b5b3ea9edaf8604c59ac41b2c\",\n \"type\": \"claim\",\n \"value\": {\n \"author\": \"Picaso\",\n \"description\": \"A blank PNG that is 5x7.\",\n \"fee\": {\n \"address\": \"mnHsWiDjPYw27jPzMfqUFM9scdsgHQ6NJg\",\n \"amount\": \"0.3\",\n \"currency\": \"LBC\"\n },\n \"image\": {\n \"height\": 7,\n \"width\": 5\n },\n \"languages\": [\n \"en\"\n ],\n \"license\": \"Public Domain\",\n \"license_url\": \"http://public-domain.org\",\n \"locations\": [\n {\n \"city\": \"Manchester\",\n \"country\": \"US\",\n \"state\": \"NH\"\n }\n ],\n \"release_time\": \"1655141671\",\n \"source\": {\n \"hash\": \"6c7df435d412c603390f593ef658c199817c7830ba3f16b7eadd8f99fa50e85dbd0d2b3dc61eadc33fe096e3872d1545\",\n \"media_type\": \"image/png\",\n \"name\": \"tmps0do5cfj.png\",\n \"sd_hash\": \"d221fe243afed69b84e6cbc32448260a187449c5123bfa89d33af05af2c8f195a7bbac2bfe02d4a2ba472af217af3996\",\n \"size\": \"99\"\n },\n \"stream_type\": \"image\",\n \"tags\": [\n \"blank\",\n \"art\"\n ],\n \"thumbnail\": {\n \"url\": \"http://smallmedia.com/thumbnail.jpg\"\n },\n \"title\": \"Blank Image\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"n44m2TuZKypifAkpPQqEN4VtRkCxgbgkA3\",\n \"amount\": \"4.947555\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"04d32d523ddc247cd5055a2146c445c8ab99ff8b5b3ea9edaf8604c59ac41b2c\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.022107\",\n \"total_input\": \"5.969662\",\n \"total_output\": \"5.947555\",\n \"txid\": \"04d32d523ddc247cd5055a2146c445c8ab99ff8b5b3ea9edaf8604c59ac41b2c\"\n }\n}"
}
]
},
{
"name": "stream_list",
"description": "List my stream claims.",
"arguments": [
{
"name": "name",
"type": "str or list",
"description": "stream name",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "stream id",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "shows previous stream updates and abandons",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "resolve",
"type": "bool",
"description": "resolves each stream to provide additional metadata",
"is_required": false
},
{
"name": "no_totals",
"type": "bool",
"description": "do not calculate the total number of pages and items in result set (significant performance boost)",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": [
{
"title": "List all your stream claims",
"curl": "curl -d'{\"method\": \"stream_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"resolve\": false, \"no_totals\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet stream list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"stream_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"resolve\": false, \"no_totals\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 1,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"create\",\n \"confirmations\": 2,\n \"height\": 213,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": true,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"timestamp\": 1655141671,\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 2,\n \"total_pages\": 1\n }\n}"
},
{
"title": "Paginate your stream claims",
"curl": "curl -d'{\"method\": \"stream_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"page\": 1, \"page_size\": 20, \"resolve\": false, \"no_totals\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet stream list --page=1 --page_size=20",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"stream_list\", \"params\": {\"name\": [], \"claim_id\": [], \"is_spent\": false, \"page\": 1, \"page_size\": 20, \"resolve\": false, \"no_totals\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": 1,\n \"height\": 214,\n \"is_channel_signature_valid\": true,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": false,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 5,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": 1655141671,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"create\",\n \"confirmations\": 2,\n \"height\": 213,\n \"is_internal_transfer\": false,\n \"is_my_input\": true,\n \"is_my_output\": true,\n \"is_spent\": true,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"timestamp\": 1655141671,\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 2,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "stream_repost",
"description": "Creates a claim that references an existing stream by its claim id.",
"arguments": [
{
"name": "name",
"type": "str",
"description": "name of the content (can only consist of a-z A-Z 0-9 and -(dash))",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": true
},
{
"name": "claim_id",
"type": "str",
"description": "id of the claim being reposted",
"is_required": true
},
{
"name": "allow_duplicate_name",
"type": "bool",
"description": "create new claim even if one already exists with given name. default: false.",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the repost",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the repost",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "add repost tags",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the publisher channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of the publisher channel",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account to use for holding the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the claim is sent to, if not specified it will be determined automatically from the account",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "stream_update",
"description": "Update an existing stream claim and if a new file is provided announce it to lbrynet.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "id of the stream claim to update",
"is_required": true
},
{
"name": "bid",
"type": "decimal",
"description": "amount to back the claim",
"is_required": false
},
{
"name": "file_path",
"type": "str",
"description": "path to file to be associated with name.",
"is_required": false
},
{
"name": "validate_file",
"type": "bool",
"description": "validate that the video container and encodings match common web browser support or that optimization succeeds if specified. FFmpeg is required and file_path must be specified.",
"is_required": false
},
{
"name": "optimize_file",
"type": "bool",
"description": "transcode the video & audio if necessary to ensure common web browser support. FFmpeg is required and file_path must be specified.",
"is_required": false
},
{
"name": "file_name",
"type": "str",
"description": "override file name, defaults to name from file_path.",
"is_required": false
},
{
"name": "file_size",
"type": "str",
"description": "override file size, otherwise automatically computed.",
"is_required": false
},
{
"name": "file_hash",
"type": "str",
"description": "override file hash, otherwise automatically computed.",
"is_required": false
},
{
"name": "fee_currency",
"type": "string",
"description": "specify fee currency",
"is_required": false
},
{
"name": "fee_amount",
"type": "decimal",
"description": "content download fee",
"is_required": false
},
{
"name": "fee_address",
"type": "str",
"description": "address where to send fee payments, will use value from --claim_address if not provided",
"is_required": false
},
{
"name": "clear_fee",
"type": "bool",
"description": "clear previously set fee",
"is_required": false
},
{
"name": "title",
"type": "str",
"description": "title of the publication",
"is_required": false
},
{
"name": "description",
"type": "str",
"description": "description of the publication",
"is_required": false
},
{
"name": "author",
"type": "str",
"description": "author of the publication. The usage for this field is not the same as for channels. The author field is used to credit an author who is not the publisher and is not represented by the channel. For example, a pdf file of 'The Odyssey' has an author of 'Homer' but may by published to a channel such as '@classics', or to no channel at all",
"is_required": false
},
{
"name": "tags",
"type": "list",
"description": "add content tags",
"is_required": false
},
{
"name": "clear_tags",
"type": "bool",
"description": "clear existing tags (prior to adding new ones)",
"is_required": false
},
{
"name": "languages",
"type": "list",
"description": "languages used by the channel, using RFC 5646 format, eg: for English `--languages=en` for Spanish (Spain) `--languages=es-ES` for Spanish (Mexican) `--languages=es-MX` for Chinese (Simplified) `--languages=zh-Hans` for Chinese (Traditional) `--languages=zh-Hant`",
"is_required": false
},
{
"name": "clear_languages",
"type": "bool",
"description": "clear existing languages (prior to adding new ones)",
"is_required": false
},
{
"name": "locations",
"type": "list",
"description": "locations relevant to the stream, consisting of 2 letter `country` code and a `state`, `city` and a postal `code` along with a `latitude` and `longitude`. for JSON RPC: pass a dictionary with aforementioned attributes as keys, eg: ... \"locations\": [{'country': 'US', 'state': 'NH'}] ... for command line: pass a colon delimited list with values in the following order: \"COUNTRY:STATE:CITY:CODE:LATITUDE:LONGITUDE\" making sure to include colon for blank values, for example to provide only the city: ... --locations=\"::Manchester\" with all values set: ... --locations=\"US:NH:Manchester:03101:42.990605:-71.460989\" optionally, you can just pass the \"LATITUDE:LONGITUDE\": ... --locations=\"42.990605:-71.460989\" finally, you can also pass JSON string of dictionary on the command line as you would via JSON RPC ... --locations=\"{'country': 'US', 'state': 'NH'}\"",
"is_required": false
},
{
"name": "clear_locations",
"type": "bool",
"description": "clear existing locations (prior to adding new ones)",
"is_required": false
},
{
"name": "license",
"type": "str",
"description": "publication license",
"is_required": false
},
{
"name": "license_url",
"type": "str",
"description": "publication license url",
"is_required": false
},
{
"name": "thumbnail_url",
"type": "str",
"description": "thumbnail url",
"is_required": false
},
{
"name": "release_time",
"type": "int",
"description": "original public release of content, seconds since UNIX epoch",
"is_required": false
},
{
"name": "width",
"type": "int",
"description": "image/video width, automatically calculated from media file",
"is_required": false
},
{
"name": "height",
"type": "int",
"description": "image/video height, automatically calculated from media file",
"is_required": false
},
{
"name": "duration",
"type": "int",
"description": "audio/video duration in seconds, automatically calculated",
"is_required": false
},
{
"name": "sd_hash",
"type": "str",
"description": "sd_hash of stream",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the publisher channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of the publisher channel",
"is_required": false
},
{
"name": "clear_channel",
"type": "bool",
"description": "remove channel signature",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account in which to look for stream (default: all)",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "claim_address",
"type": "str",
"description": "address where the claim is sent to, if not specified it will be determined automatically from the account",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
},
{
"name": "replace",
"type": "bool",
"description": "instead of modifying specific values on the stream, this will clear all existing values and only save passed in values, useful for form submissions where all values are always set",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": [
{
"title": "Update a stream claim to add channel",
"curl": "curl -d'{\"method\": \"stream_update\", \"params\": {\"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\", \"validate_file\": false, \"optimize_file\": false, \"clear_fee\": false, \"tags\": [], \"clear_tags\": false, \"languages\": [], \"clear_languages\": false, \"locations\": [], \"clear_locations\": false, \"channel_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"clear_channel\": false, \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false, \"replace\": false}}' http://localhost:5279/",
"lbrynet": "lbrynet stream update ad25e05aa7dc5e9994869040c6103f9a8728db46 --channel_id=\"595c2e2f0c1f59188628fab7503692b0145779a2\"",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"stream_update\", \"params\": {\"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\", \"validate_file\": false, \"optimize_file\": false, \"clear_fee\": false, \"tags\": [], \"clear_tags\": false, \"languages\": [], \"clear_languages\": false, \"locations\": [], \"clear_locations\": false, \"channel_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\", \"clear_channel\": false, \"channel_account_id\": [], \"funding_account_ids\": [], \"preview\": false, \"blocking\": false, \"replace\": false}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"height\": -2,\n \"hex\": \"010000000244a3bd0e72a6f87a1e2282dadf3a332eb04eba7dac715f14a6ac36288eece7f9000000006a4730440220117bdbc0e547eda333409733aa38ccb4841a7873db6c4a971fcf8f87a7c1553e022007bf22f8573e95012d4918fea61ba8b04a7a10af93aea95fbbffe187ca17621101210200ae7b8f7a6220cb0f3699aeb1b2f1ce7825554f50b6cf542e63f3018e9852b6ffffffffe3d2e954c197eace1e6b39edc7451a2cf9b6e1771a37f8127da3f105e929baac010000006a473044022100e29082b0d9e24dc0a9622856ab499725155e379a323575be3ccda6472970e330021f652654fcbad99cbadbf22cf8f876a597f672ba86d4b5e53e400af23914ff200121038af0e4f3c8d08223532f5ade7d5da0203b676b3445a8791c955f8d0f868b2c18ffffffff0200e1f50500000000fd2301b7076173747265616d1446db28879a3f10c640908694995edca75ae025ad4ce801a2795714b0923650b7fa288618591f0c2f2e5c5961baf110460a527e38b68f0653c1c79f2cae1f57ade55c009fb7578175c9d93d7edb0546d1378e1d33d99df8df0d5cfe9e7a5b63053a4e7ce003f95f5890b27f0a90010a8d010a30fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd120b746d707238333268703178180b22186170706c69636174696f6e2f6f637465742d73747265616d32309ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c836d6d76a9143d39ffed222af9cdea57d6d388ebba0aeda3039d88ac22abd205000000001976a91434ab169b2f32ccd430bc73f2c2116f48b907267d88ac00000000\",\n \"inputs\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"create\",\n \"confirmations\": 1,\n \"height\": 213,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"timestamp\": 1655141671,\n \"txid\": \"f9e7ec8e2836aca6145f71ac7dba4eb02e333adfda82221e7af8a6720ebda344\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"muXoymiRVLaS1FXJSgPjieSedrWPgUgEkK\",\n \"amount\": \"0.9772565\",\n \"confirmations\": 3,\n \"height\": 211,\n \"nout\": 1,\n \"timestamp\": 1655141670,\n \"txid\": \"acba29e905f1a37d12f8371a77e1b6f92c1a45c7ed396b1eceea97c154e9d2e3\",\n \"type\": \"payment\"\n }\n ],\n \"outputs\": [\n {\n \"address\": \"mm6gzeSV7hiGxtAv3rQjo3sRYtCDGD4t2M\",\n \"amount\": \"1.0\",\n \"claim_id\": \"ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"claim_op\": \"update\",\n \"confirmations\": -2,\n \"height\": -2,\n \"is_channel_signature_valid\": true,\n \"meta\": {},\n \"name\": \"astream\",\n \"normalized_name\": \"astream\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://astream#ad25e05aa7dc5e9994869040c6103f9a8728db46\",\n \"signing_channel\": {\n \"address\": \"muRaaMs12imkZJQofvBuLbAFYAs6TiQPAQ\",\n \"amount\": \"1.0\",\n \"claim_id\": \"595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"claim_op\": \"update\",\n \"confirmations\": 4,\n \"has_signing_key\": true,\n \"height\": 210,\n \"meta\": {},\n \"name\": \"@channel\",\n \"normalized_name\": \"@channel\",\n \"nout\": 0,\n \"permanent_url\": \"lbry://@channel#595c2e2f0c1f59188628fab7503692b0145779a2\",\n \"timestamp\": 1655141670,\n \"txid\": \"ab8221c5a5404117744d80e5d652dcf04c5caac947f019b5b534e0ccf7cfff64\",\n \"type\": \"claim\",\n \"value\": {\n \"public_key\": \"03bbf11fc85401781301f36897b5b01b0aef440db5d6fb0747900b8c69e40e55e5\",\n \"public_key_id\": \"moF9EgZauGirwqpwZ7NgVsCAxddcPABTfm\",\n \"title\": \"New Channel\"\n },\n \"value_type\": \"channel\"\n },\n \"timestamp\": null,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"claim\",\n \"value\": {\n \"source\": {\n \"hash\": \"fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd\",\n \"media_type\": \"application/octet-stream\",\n \"name\": \"tmpr832hp1x\",\n \"sd_hash\": \"9ddea316b511d9f720b1f67f5958cac381fdac5d1d3beabc754b9a1220a891024e983241e2fc7549f0f89ea0636c6c83\",\n \"size\": \"11\"\n },\n \"stream_type\": \"binary\"\n },\n \"value_type\": \"stream\"\n },\n {\n \"address\": \"mkKSPTEMxFHfbYE8dYMmczn3BCJRg4LoHQ\",\n \"amount\": \"0.9769245\",\n \"confirmations\": -2,\n \"height\": -2,\n \"nout\": 1,\n \"timestamp\": null,\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\",\n \"type\": \"payment\"\n }\n ],\n \"total_fee\": \"0.000332\",\n \"total_input\": \"1.9772565\",\n \"total_output\": \"1.9769245\",\n \"txid\": \"7570c487faefe51e7e72ef22896171e151710ff6e8153efcbe51baa49b7f6234\"\n }\n}"
}
]
}
]
},
"support": {
"doc": "Create, list and abandon all types of supports.",
"commands": [
{
"name": "support_abandon",
"description": "Abandon supports, including tips, of a specific claim, optionally\nkeeping some amount as supports.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the support to abandon",
"is_required": false
},
{
"name": "txid",
"type": "str",
"description": "txid of the claim to abandon",
"is_required": false
},
{
"name": "nout",
"type": "int",
"description": "nout of the claim to abandon",
"is_required": false
},
{
"name": "keep",
"type": "decimal",
"description": "amount of lbc to keep as support",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to use",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until abandon is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "support_create",
"description": "Create a support or a tip for name claim.",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim_id of the claim to support",
"is_required": true
},
{
"name": "amount",
"type": "decimal",
"description": "amount of support",
"is_required": true
},
{
"name": "tip",
"type": "bool",
"description": "send support to claim owner, default: false.",
"is_required": false
},
{
"name": "channel_id",
"type": "str",
"description": "claim id of the supporters identity channel",
"is_required": false
},
{
"name": "channel_name",
"type": "str",
"description": "name of the supporters identity channel",
"is_required": false
},
{
"name": "channel_account_id",
"type": "str",
"description": "one or more account ids for accounts to look in for channel certificates, defaults to all accounts.",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "account to use for holding the transaction",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "list",
"description": "ids of accounts to fund this transaction",
"is_required": false
},
{
"name": "comment",
"type": "str",
"description": "add a comment to the support",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until transaction is in mempool",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "support_list",
"description": "List staked supports and sent/received tips.",
"arguments": [
{
"name": "name",
"type": "str or list",
"description": "claim name",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "claim id",
"is_required": false
},
{
"name": "received",
"type": "bool",
"description": "only show received (tips)",
"is_required": false
},
{
"name": "sent",
"type": "bool",
"description": "only show sent (tips)",
"is_required": false
},
{
"name": "staked",
"type": "bool",
"description": "only show my staked supports",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "show abandoned supports",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "no_totals",
"type": "bool",
"description": "do not calculate the total number of pages and items in result set (significant performance boost)",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": []
},
{
"name": "support_sum",
"description": "List total staked supports for a claim, grouped by the channel that signed the support.\n\nIf claim_id is a channel claim, you can use --include_channel_content to also include supports for\ncontent claims in the channel.\n\n!!!! NOTE: PAGINATION DOES NOT DO ANYTHING AT THE MOMENT !!!!!",
"arguments": [
{
"name": "claim_id",
"type": "str",
"description": "claim id",
"is_required": false
},
{
"name": "new_sdk_server",
"type": "str",
"description": "URL of the new SDK server (EXPERIMENTAL)",
"is_required": false
},
{
"name": "include_channel_content",
"type": "bool",
"description": "if claim_id is for a channel, include supports for claims in that channel",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n \"glorious data in dictionary\"\n ]\n }",
"examples": []
}
]
},
"sync": {
"doc": "Wallet synchronization.",
"commands": [
{
"name": "sync_apply",
"description": "Apply incoming synchronization data, if provided, and return a sync hash and update wallet data.\n\nWallet must be unlocked to perform this operation.\n\nIf \"encrypt-on-disk\" preference is True and supplied password is different from local password,\nor there is no local password (because local wallet was not encrypted), then the supplied password\nwill be used for local encryption (overwriting previous local encryption password).",
"arguments": [
{
"name": "password",
"type": "str",
"description": "password to decrypt incoming and encrypt outgoing data",
"is_required": false
},
{
"name": "data",
"type": "str",
"description": "incoming sync data, if any",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "wallet being sync'ed",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until any new accounts have sync'ed",
"is_required": false
}
],
"returns": "(map) sync hash and data",
"examples": []
},
{
"name": "sync_hash",
"description": "Deterministic hash of the wallet.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "wallet for which to generate hash",
"is_required": false
}
],
"returns": "(str) sha256 hash of wallet",
"examples": []
}
]
},
"tracemalloc": {
"doc": "Controls and queries tracemalloc memory tracing tools for troubleshooting.",
"commands": [
{
"name": "tracemalloc_disable",
"description": "Disable tracemalloc memory tracing",
"arguments": [],
"returns": "(bool) is it tracing?",
"examples": []
},
{
"name": "tracemalloc_enable",
"description": "Enable tracemalloc memory tracing",
"arguments": [],
"returns": "(bool) is it tracing?",
"examples": []
},
{
"name": "tracemalloc_top",
"description": "Show most common objects, the place that created them and their size.",
"arguments": [
{
"name": "items",
"type": "int",
"description": "maximum items to return, from the most common",
"is_required": true
}
],
"returns": "(dict) dictionary containing most common objects in memory\n {\n \"line\": (str) filename and line number where it was created,\n \"code\": (str) code that created it,\n \"size\": (int) size in bytes, for each \"memory block\",\n \"count\" (int) number of memory blocks\n }",
"examples": []
}
]
},
"transaction": {
"doc": "Transaction management.",
"commands": [
{
"name": "transaction_list",
"description": "List transactions belonging to wallet",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": "(list) List of transactions\n\n {\n \"claim_info\": (list) claim info if in txn [{\n \"address\": (str) address of claim,\n \"balance_delta\": (float) bid amount,\n \"amount\": (float) claim amount,\n \"claim_id\": (str) claim id,\n \"claim_name\": (str) claim name,\n \"nout\": (int) nout\n }],\n \"abandon_info\": (list) abandon info if in txn [{\n \"address\": (str) address of abandoned claim,\n \"balance_delta\": (float) returned amount,\n \"amount\": (float) claim amount,\n \"claim_id\": (str) claim id,\n \"claim_name\": (str) claim name,\n \"nout\": (int) nout\n }],\n \"confirmations\": (int) number of confirmations for the txn,\n \"date\": (str) date and time of txn,\n \"fee\": (float) txn fee,\n \"support_info\": (list) support info if in txn [{\n \"address\": (str) address of support,\n \"balance_delta\": (float) support amount,\n \"amount\": (float) support amount,\n \"claim_id\": (str) claim id,\n \"claim_name\": (str) claim name,\n \"is_tip\": (bool),\n \"nout\": (int) nout\n }],\n \"timestamp\": (int) timestamp,\n \"txid\": (str) txn id,\n \"update_info\": (list) update info if in txn [{\n \"address\": (str) address of claim,\n \"balance_delta\": (float) credited/debited\n \"amount\": (float) absolute amount,\n \"claim_id\": (str) claim id,\n \"claim_name\": (str) claim name,\n \"nout\": (int) nout\n }],\n \"value\": (float) value of txn\n }",
"examples": [
{
"title": "List your transactions",
"curl": "curl -d'{\"method\": \"transaction_list\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet transaction list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"transaction_list\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"abandon_info\": [],\n \"claim_info\": [],\n \"confirmations\": 1,\n \"date\": \"2022-06-13 13:34\",\n \"fee\": \"-0.000124\",\n \"purchase_info\": [],\n \"support_info\": [],\n \"timestamp\": 1655141670,\n \"txid\": \"f03159770e23cab4da7779dcf1a6809f6f518c61ab540d6a452d0bc5509874b8\",\n \"update_info\": [],\n \"value\": \"0.0\"\n },\n {\n \"abandon_info\": [],\n \"claim_info\": [],\n \"confirmations\": 7,\n \"date\": \"2022-06-13 13:34\",\n \"fee\": \"0.0\",\n \"purchase_info\": [],\n \"support_info\": [],\n \"timestamp\": 1655141669,\n \"txid\": \"1c584e33328e7616e4b52ab4a919267757ad58a2911e1c0954a0fd0e139c482a\",\n \"update_info\": [],\n \"value\": \"10.0\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 2,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "transaction_show",
"description": "Get a decoded transaction from a txid",
"arguments": [
{
"name": "txid",
"type": "str",
"description": "txid of the transaction",
"is_required": true
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
}
]
},
"txo": {
"doc": "List and sum transaction outputs.",
"commands": [
{
"name": "txo_list",
"description": "List my transaction outputs.",
"arguments": [
{
"name": "type",
"type": "str or list",
"description": "claim type: stream, channel, support, purchase, collection, repost, other",
"is_required": false
},
{
"name": "txid",
"type": "str or list",
"description": "transaction id of outputs",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "claim id",
"is_required": false
},
{
"name": "channel_id",
"type": "str or list",
"description": "claims in this channel",
"is_required": false
},
{
"name": "not_channel_id",
"type": "str or list",
"description": "claims not in this channel",
"is_required": false
},
{
"name": "name",
"type": "str or list",
"description": "claim name",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "only show spent txos",
"is_required": false
},
{
"name": "is_not_spent",
"type": "bool",
"description": "only show not spent txos",
"is_required": false
},
{
"name": "is_my_input_or_output",
"type": "bool",
"description": "txos which have your inputs or your outputs, if using this flag the other related flags are ignored (--is_my_output, --is_my_input, etc)",
"is_required": false
},
{
"name": "is_my_output",
"type": "bool",
"description": "show outputs controlled by you",
"is_required": false
},
{
"name": "is_not_my_output",
"type": "bool",
"description": "show outputs not controlled by you",
"is_required": false
},
{
"name": "is_my_input",
"type": "bool",
"description": "show outputs created by you",
"is_required": false
},
{
"name": "is_not_my_input",
"type": "bool",
"description": "show outputs not created by you",
"is_required": false
},
{
"name": "exclude_internal_transfers",
"type": "bool",
"description": "excludes any outputs that are exactly this combination: \"--is_my_input --is_my_output --type=other\" this allows to exclude \"change\" payments, this flag can be used in combination with any of the other flags",
"is_required": false
},
{
"name": "include_received_tips",
"type": "bool",
"description": "calculate the amount of tips received for claim outputs",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
},
{
"name": "resolve",
"type": "bool",
"description": "resolves each claim to provide additional metadata",
"is_required": false
},
{
"name": "order_by",
"type": "str",
"description": "field to order by: 'name', 'height', 'amount' and 'none'",
"is_required": false
},
{
"name": "no_totals",
"type": "bool",
"description": "do not calculate the total number of pages and items in result set (significant performance boost)",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": []
},
{
"name": "txo_plot",
"description": "Plot transaction output sum over days.",
"arguments": [
{
"name": "type",
"type": "str or list",
"description": "claim type: stream, channel, support, purchase, collection, repost, other",
"is_required": false
},
{
"name": "txid",
"type": "str or list",
"description": "transaction id of outputs",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "claim id",
"is_required": false
},
{
"name": "name",
"type": "str or list",
"description": "claim name",
"is_required": false
},
{
"name": "channel_id",
"type": "str or list",
"description": "claims in this channel",
"is_required": false
},
{
"name": "not_channel_id",
"type": "str or list",
"description": "claims not in this channel",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "only show spent txos",
"is_required": false
},
{
"name": "is_not_spent",
"type": "bool",
"description": "only show not spent txos",
"is_required": false
},
{
"name": "is_my_input_or_output",
"type": "bool",
"description": "txos which have your inputs or your outputs, if using this flag the other related flags are ignored (--is_my_output, --is_my_input, etc)",
"is_required": false
},
{
"name": "is_my_output",
"type": "bool",
"description": "show outputs controlled by you",
"is_required": false
},
{
"name": "is_not_my_output",
"type": "bool",
"description": "show outputs not controlled by you",
"is_required": false
},
{
"name": "is_my_input",
"type": "bool",
"description": "show outputs created by you",
"is_required": false
},
{
"name": "is_not_my_input",
"type": "bool",
"description": "show outputs not created by you",
"is_required": false
},
{
"name": "exclude_internal_transfers",
"type": "bool",
"description": "excludes any outputs that are exactly this combination: \"--is_my_input --is_my_output --type=other\" this allows to exclude \"change\" payments, this flag can be used in combination with any of the other flags",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "days_back",
"type": "int",
"description": "number of days back from today (not compatible with --start_day, --days_after, --end_day)",
"is_required": false
},
{
"name": "start_day",
"type": "date",
"description": "start on specific date (YYYY-MM-DD) (instead of --days_back)",
"is_required": false
},
{
"name": "days_after",
"type": "int",
"description": "end number of days after --start_day (instead of --end_day)",
"is_required": false
},
{
"name": "end_day",
"type": "date",
"description": "end on specific date (YYYY-MM-DD) (instead of --days_after)",
"is_required": false
}
],
"returns": "List[Dict]",
"examples": []
},
{
"name": "txo_spend",
"description": "Spend transaction outputs, batching into multiple transactions as necessary.",
"arguments": [
{
"name": "type",
"type": "str or list",
"description": "claim type: stream, channel, support, purchase, collection, repost, other",
"is_required": false
},
{
"name": "txid",
"type": "str or list",
"description": "transaction id of outputs",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "claim id",
"is_required": false
},
{
"name": "channel_id",
"type": "str or list",
"description": "claims in this channel",
"is_required": false
},
{
"name": "not_channel_id",
"type": "str or list",
"description": "claims not in this channel",
"is_required": false
},
{
"name": "name",
"type": "str or list",
"description": "claim name",
"is_required": false
},
{
"name": "is_my_input",
"type": "bool",
"description": "show outputs created by you",
"is_required": false
},
{
"name": "is_not_my_input",
"type": "bool",
"description": "show outputs not created by you",
"is_required": false
},
{
"name": "exclude_internal_transfers",
"type": "bool",
"description": "excludes any outputs that are exactly this combination: \"--is_my_input --is_my_output --type=other\" this allows to exclude \"change\" payments, this flag can be used in combination with any of the other flags",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until abandon is in mempool",
"is_required": false
},
{
"name": "batch_size",
"type": "int",
"description": "number of txos to spend per transactions",
"is_required": false
},
{
"name": "include_full_tx",
"type": "bool",
"description": "include entire tx in output and not just the txid",
"is_required": false
}
],
"returns": " [\n {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }\n ]",
"examples": []
},
{
"name": "txo_sum",
"description": "Sum of transaction outputs.",
"arguments": [
{
"name": "type",
"type": "str or list",
"description": "claim type: stream, channel, support, purchase, collection, repost, other",
"is_required": false
},
{
"name": "txid",
"type": "str or list",
"description": "transaction id of outputs",
"is_required": false
},
{
"name": "claim_id",
"type": "str or list",
"description": "claim id",
"is_required": false
},
{
"name": "name",
"type": "str or list",
"description": "claim name",
"is_required": false
},
{
"name": "channel_id",
"type": "str or list",
"description": "claims in this channel",
"is_required": false
},
{
"name": "not_channel_id",
"type": "str or list",
"description": "claims not in this channel",
"is_required": false
},
{
"name": "is_spent",
"type": "bool",
"description": "only show spent txos",
"is_required": false
},
{
"name": "is_not_spent",
"type": "bool",
"description": "only show not spent txos",
"is_required": false
},
{
"name": "is_my_input_or_output",
"type": "bool",
"description": "txos which have your inputs or your outputs, if using this flag the other related flags are ignored (--is_my_output, --is_my_input, etc)",
"is_required": false
},
{
"name": "is_my_output",
"type": "bool",
"description": "show outputs controlled by you",
"is_required": false
},
{
"name": "is_not_my_output",
"type": "bool",
"description": "show outputs not controlled by you",
"is_required": false
},
{
"name": "is_my_input",
"type": "bool",
"description": "show outputs created by you",
"is_required": false
},
{
"name": "is_not_my_input",
"type": "bool",
"description": "show outputs not created by you",
"is_required": false
},
{
"name": "exclude_internal_transfers",
"type": "bool",
"description": "excludes any outputs that are exactly this combination: \"--is_my_input --is_my_output --type=other\" this allows to exclude \"change\" payments, this flag can be used in combination with any of the other flags",
"is_required": false
},
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
}
],
"returns": "int",
"examples": []
}
]
},
"utxo": {
"doc": "Unspent transaction management.",
"commands": [
{
"name": "utxo_list",
"description": "List unspent transaction outputs",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict results to specific wallet",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ]\n }",
"examples": []
},
{
"name": "utxo_release",
"description": "When spending a UTXO it is locally locked to prevent double spends;\noccasionally this can result in a UTXO being locked which ultimately\ndid not get spent (failed to broadcast, spend transaction was not\naccepted by blockchain node, etc). This command releases the lock\non all UTXOs in your account.",
"arguments": [
{
"name": "account_id",
"type": "str",
"description": "id of the account to query",
"is_required": false
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "None",
"examples": []
}
]
},
"wallet": {
"doc": "Create, modify and inspect wallets.",
"commands": [
{
"name": "wallet_add",
"description": "Add existing wallet.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "wallet file name",
"is_required": true
}
],
"returns": " {\n \"id\": \"wallet_id\",\n \"name\": \"optional wallet name\"\n }",
"examples": []
},
{
"name": "wallet_balance",
"description": "Return the balance of a wallet",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "balance for specific wallet",
"is_required": false
},
{
"name": "confirmations",
"type": "int",
"description": "Only include transactions with this many confirmed blocks.",
"is_required": false
}
],
"returns": "(decimal) amount of lbry credits in wallet",
"examples": []
},
{
"name": "wallet_create",
"description": "Create a new wallet.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "wallet file name",
"is_required": true
},
{
"name": "skip_on_startup",
"type": "bool",
"description": "don't add wallet to daemon_settings.yml",
"is_required": false
},
{
"name": "create_account",
"type": "bool",
"description": "generates the default account",
"is_required": false
},
{
"name": "single_key",
"type": "bool",
"description": "used with --create_account, creates single-key account",
"is_required": false
}
],
"returns": " {\n \"id\": \"wallet_id\",\n \"name\": \"optional wallet name\"\n }",
"examples": []
},
{
"name": "wallet_decrypt",
"description": "Decrypt an encrypted wallet, this will remove the wallet password. The wallet must be unlocked to decrypt it",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(bool) true if wallet is decrypted, otherwise false",
"examples": []
},
{
"name": "wallet_encrypt",
"description": "Encrypt an unencrypted wallet with a password",
"arguments": [
{
"name": "new_password",
"type": "str",
"description": "password to encrypt account",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(bool) true if wallet is decrypted, otherwise false",
"examples": []
},
{
"name": "wallet_list",
"description": "List wallets.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "show specific wallet only",
"is_required": false
},
{
"name": "page",
"type": "int",
"description": "page to return during paginating",
"is_required": false
},
{
"name": "page_size",
"type": "int",
"description": "number of items on page during pagination",
"is_required": false
}
],
"returns": " {\n \"page\": \"Page number of the current items.\",\n \"page_size\": \"Number of items to show on a page.\",\n \"total_pages\": \"Total number of pages.\",\n \"total_items\": \"Total number of items.\",\n \"items\": [\n {\n \"id\": \"wallet_id\",\n \"name\": \"optional wallet name\"\n }\n ]\n }",
"examples": [
{
"title": "List your wallets",
"curl": "curl -d'{\"method\": \"wallet_list\", \"params\": {}}' http://localhost:5279/",
"lbrynet": "lbrynet wallet list",
"python": "requests.post(\"http://localhost:5279\", json={\"method\": \"wallet_list\", \"params\": {}}).json()",
"output": "{\n \"jsonrpc\": \"2.0\",\n \"result\": {\n \"items\": [\n {\n \"id\": \"my_wallet.json\",\n \"name\": \"Wallet\"\n }\n ],\n \"page\": 1,\n \"page_size\": 20,\n \"total_items\": 1,\n \"total_pages\": 1\n }\n}"
}
]
},
{
"name": "wallet_lock",
"description": "Lock an unlocked wallet",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(bool) true if wallet is locked, otherwise false",
"examples": []
},
{
"name": "wallet_reconnect",
"description": "Reconnects ledger network client, applying new configurations.",
"arguments": [],
"returns": "None",
"examples": []
},
{
"name": "wallet_remove",
"description": "Remove an existing wallet.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "name of wallet to remove",
"is_required": true
}
],
"returns": " {\n \"id\": \"wallet_id\",\n \"name\": \"optional wallet name\"\n }",
"examples": []
},
{
"name": "wallet_send",
"description": "Send the same number of credits to multiple addresses using all accounts in wallet to\nfund the transaction and the default account to receive any change.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
},
{
"name": "change_account_id",
"type": "str",
"description": "account where change will go",
"is_required": false
},
{
"name": "funding_account_ids",
"type": "str",
"description": "accounts to fund the transaction",
"is_required": false
},
{
"name": "preview",
"type": "bool",
"description": "do not broadcast the transaction",
"is_required": false
},
{
"name": "blocking",
"type": "bool",
"description": "wait until tx has synced",
"is_required": false
}
],
"returns": " {\n \"txid\": \"hash of transaction in hex\",\n \"height\": \"block where transaction was recorded\",\n \"inputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"outputs\": [\n {\n \"txid\": \"hash of transaction in hex\",\n \"nout\": \"position in the transaction\",\n \"height\": \"block where transaction was recorded\",\n \"amount\": \"value of the txo as a decimal\",\n \"address\": \"address of who can spend the txo\",\n \"confirmations\": \"number of confirmed blocks\",\n \"is_change\": \"payment to change address, only available when it can be determined\",\n \"is_received\": \"true if txo was sent from external account to this account\",\n \"is_spent\": \"true if txo is spent\",\n \"is_mine\": \"payment to one of your accounts, only available when it can be determined\",\n \"type\": \"one of 'claim', 'support' or 'purchase'\",\n \"name\": \"when type is 'claim' or 'support', this is the claim name\",\n \"claim_id\": \"when type is 'claim', 'support' or 'purchase', this is the claim id\",\n \"claim_op\": \"when type is 'claim', this determines if it is 'create' or 'update'\",\n \"value\": \"when type is 'claim' or 'support' with payload, this is the decoded protobuf payload\",\n \"value_type\": \"determines the type of the 'value' field: 'channel', 'stream', etc\",\n \"protobuf\": \"hex encoded raw protobuf version of 'value' field\",\n \"permanent_url\": \"when type is 'claim' or 'support', this is the long permanent claim URL\",\n \"claim\": \"for purchase outputs only, metadata of purchased claim\",\n \"reposted_claim\": \"for repost claims only, metadata of claim being reposted\",\n \"signing_channel\": \"for signed claims only, metadata of signing channel\",\n \"is_channel_signature_valid\": \"for signed claims only, whether signature is valid\",\n \"purchase_receipt\": \"metadata for the purchase transaction associated with this claim\"\n }\n ],\n \"total_input\": \"sum of inputs as a decimal\",\n \"total_output\": \"sum of outputs, sans fee, as a decimal\",\n \"total_fee\": \"fee amount\",\n \"hex\": \"entire transaction encoded in hex\"\n }",
"examples": []
},
{
"name": "wallet_status",
"description": "Status of wallet including encryption/lock state.",
"arguments": [
{
"name": "wallet_id",
"type": "str",
"description": "status of specific wallet",
"is_required": false
}
],
"returns": "Dictionary of wallet status information.",
"examples": []
},
{
"name": "wallet_unlock",
"description": "Unlock an encrypted wallet",
"arguments": [
{
"name": "password",
"type": "str",
"description": "password to use for unlocking",
"is_required": true
},
{
"name": "wallet_id",
"type": "str",
"description": "restrict operation to specific wallet",
"is_required": false
}
],
"returns": "(bool) true if wallet is unlocked, otherwise false",
"examples": []
}
]
}
}
================================================
FILE: example_daemon_settings.yml
================================================
# This is an example daemon_settings.yml file.
# See https://lbry.tech/resources/daemon-settings for all configuration keys and values
share_usage_data: True
lbryum_servers:
- lbryumx1.lbry.com:50001
- lbryumx2.lbry.com:50001
- lbryumx4.lbry.com:50001
blockchain_name: lbrycrd_main
data_dir: /home/lbry/.lbrynet
download_directory: /home/lbry/downloads
save_blobs: true
save_files: false
dht_node_port: 4444
peer_port: 3333
use_upnp: true
#components_to_skip:
# - hash_announcer
# - blob_server
# - dht
================================================
FILE: lbry/.dockerignore
================================================
.git
.tox
__pycache__
dist
lbry.egg-info
docs
tests
================================================
FILE: lbry/__init__.py
================================================
__version__ = "0.113.0"
version = tuple(map(int, __version__.split('.'))) # pylint: disable=invalid-name
================================================
FILE: lbry/blob/__init__.py
================================================
from lbry.utils import get_lbry_hash_obj
MAX_BLOB_SIZE = 2 * 2 ** 20
# digest_size is in bytes, and blob hashes are hex encoded
BLOBHASH_LENGTH = get_lbry_hash_obj().digest_size * 2
================================================
FILE: lbry/blob/blob_file.py
================================================
import os
import re
import time
import asyncio
import binascii
import logging
import typing
import contextlib
from io import BytesIO
from cryptography.hazmat.primitives.ciphers import Cipher, modes
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.padding import PKCS7
from cryptography.hazmat.backends import default_backend
from lbry.utils import get_lbry_hash_obj
from lbry.error import DownloadCancelledError, InvalidBlobHashError, InvalidDataError
from lbry.blob import MAX_BLOB_SIZE, BLOBHASH_LENGTH
from lbry.blob.blob_info import BlobInfo
from lbry.blob.writer import HashBlobWriter
log = logging.getLogger(__name__)
HEXMATCH = re.compile("^[a-f,0-9]+$")
BACKEND = default_backend()
def is_valid_blobhash(blobhash: str) -> bool:
"""Checks whether the blobhash is the correct length and contains only
valid characters (0-9, a-f)
@param blobhash: string, the blobhash to check
@return: True/False
"""
return len(blobhash) == BLOBHASH_LENGTH and HEXMATCH.match(blobhash)
def encrypt_blob_bytes(key: bytes, iv: bytes, unencrypted: bytes) -> typing.Tuple[bytes, str]:
cipher = Cipher(AES(key), modes.CBC(iv), backend=BACKEND)
padder = PKCS7(AES.block_size).padder()
encryptor = cipher.encryptor()
encrypted = encryptor.update(padder.update(unencrypted) + padder.finalize()) + encryptor.finalize()
digest = get_lbry_hash_obj()
digest.update(encrypted)
return encrypted, digest.hexdigest()
def decrypt_blob_bytes(data: bytes, length: int, key: bytes, iv: bytes) -> bytes:
if len(data) != length:
raise ValueError("unexpected length")
cipher = Cipher(AES(key), modes.CBC(iv), backend=BACKEND)
unpadder = PKCS7(AES.block_size).unpadder()
decryptor = cipher.decryptor()
return unpadder.update(decryptor.update(data) + decryptor.finalize()) + unpadder.finalize()
class AbstractBlob:
"""
A chunk of data (up to 2MB) available on the network which is specified by a sha384 hash
This class is non-io specific
"""
__slots__ = [
'loop',
'blob_hash',
'length',
'blob_completed_callback',
'blob_directory',
'writers',
'verified',
'writing',
'readers',
'added_on',
'is_mine',
]
def __init__(
self, loop: asyncio.AbstractEventLoop, blob_hash: str, length: typing.Optional[int] = None,
blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None,
blob_directory: typing.Optional[str] = None, added_on: typing.Optional[int] = None, is_mine: bool = False,
):
self.loop = loop
self.blob_hash = blob_hash
self.length = length
self.blob_completed_callback = blob_completed_callback
self.blob_directory = blob_directory
self.writers: typing.Dict[typing.Tuple[typing.Optional[str], typing.Optional[int]], HashBlobWriter] = {}
self.verified: asyncio.Event = asyncio.Event()
self.writing: asyncio.Event = asyncio.Event()
self.readers: typing.List[typing.BinaryIO] = []
self.added_on = added_on or time.time()
self.is_mine = is_mine
if not is_valid_blobhash(blob_hash):
raise InvalidBlobHashError(blob_hash)
def __del__(self):
if self.writers or self.readers:
log.warning("%s not closed before being garbage collected", self.blob_hash)
self.close()
@contextlib.contextmanager
def _reader_context(self) -> typing.ContextManager[typing.BinaryIO]:
raise NotImplementedError()
@contextlib.contextmanager
def reader_context(self) -> typing.ContextManager[typing.BinaryIO]:
if not self.is_readable():
raise OSError(f"{str(type(self))} not readable, {len(self.readers)} readers {len(self.writers)} writers")
with self._reader_context() as reader:
try:
self.readers.append(reader)
yield reader
finally:
if reader in self.readers:
self.readers.remove(reader)
def _write_blob(self, blob_bytes: bytes) -> asyncio.Task:
raise NotImplementedError()
def set_length(self, length) -> None:
if self.length is not None and length == self.length:
return
if self.length is None and 0 <= length <= MAX_BLOB_SIZE:
self.length = length
return
log.warning("Got an invalid length. Previous length: %s, Invalid length: %s", self.length, length)
def get_length(self) -> typing.Optional[int]:
return self.length
def get_is_verified(self) -> bool:
return self.verified.is_set()
def is_readable(self) -> bool:
return self.verified.is_set()
def is_writeable(self) -> bool:
return not self.writing.is_set()
def write_blob(self, blob_bytes: bytes):
if not self.is_writeable():
raise OSError("cannot open blob for writing")
try:
self.writing.set()
self._write_blob(blob_bytes)
finally:
self.writing.clear()
def close(self):
while self.writers:
_, writer = self.writers.popitem()
if writer and writer.finished and not writer.finished.done() and not self.loop.is_closed():
writer.finished.cancel()
while self.readers:
reader = self.readers.pop()
if reader:
reader.close()
def delete(self):
self.close()
self.verified.clear()
self.length = None
async def sendfile(self, writer: asyncio.StreamWriter) -> int:
"""
Read and send the file to the writer and return the number of bytes sent
"""
if not self.is_readable():
raise OSError('blob files cannot be read')
with self.reader_context() as handle:
try:
return await self.loop.sendfile(writer.transport, handle, count=self.get_length())
except (ConnectionError, BrokenPipeError, RuntimeError, OSError, AttributeError):
return -1
def decrypt(self, key: bytes, iv: bytes) -> bytes:
"""
Decrypt a BlobFile to plaintext bytes
"""
with self.reader_context() as reader:
return decrypt_blob_bytes(reader.read(), self.length, key, iv)
@classmethod
async def create_from_unencrypted(
cls, loop: asyncio.AbstractEventLoop, blob_dir: typing.Optional[str], key: bytes, iv: bytes,
unencrypted: bytes, blob_num: int, added_on: int, is_mine: bool,
blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], None]] = None,
) -> BlobInfo:
"""
Create an encrypted BlobFile from plaintext bytes
"""
blob_bytes, blob_hash = encrypt_blob_bytes(key, iv, unencrypted)
length = len(blob_bytes)
blob = cls(loop, blob_hash, length, blob_completed_callback, blob_dir, added_on, is_mine)
writer = blob.get_blob_writer()
writer.write(blob_bytes)
await blob.verified.wait()
return BlobInfo(blob_num, length, binascii.hexlify(iv).decode(), added_on, blob_hash, is_mine)
def save_verified_blob(self, verified_bytes: bytes):
if self.verified.is_set():
return
def update_events(_):
self.verified.set()
self.writing.clear()
if self.is_writeable():
self.writing.set()
task = self._write_blob(verified_bytes)
task.add_done_callback(update_events)
if self.blob_completed_callback:
task.add_done_callback(lambda _: self.blob_completed_callback(self))
def get_blob_writer(self, peer_address: typing.Optional[str] = None,
peer_port: typing.Optional[int] = None) -> HashBlobWriter:
if (peer_address, peer_port) in self.writers and not self.writers[(peer_address, peer_port)].closed():
raise OSError(f"attempted to download blob twice from {peer_address}:{peer_port}")
fut = asyncio.Future()
writer = HashBlobWriter(self.blob_hash, self.get_length, fut)
self.writers[(peer_address, peer_port)] = writer
def remove_writer(_):
if (peer_address, peer_port) in self.writers:
del self.writers[(peer_address, peer_port)]
fut.add_done_callback(remove_writer)
def writer_finished_callback(finished: asyncio.Future):
try:
err = finished.exception()
if err:
raise err
verified_bytes = finished.result()
while self.writers:
_, other = self.writers.popitem()
if other is not writer:
other.close_handle()
self.save_verified_blob(verified_bytes)
except (InvalidBlobHashError, InvalidDataError) as error:
log.warning("writer error downloading %s: %s", self.blob_hash[:8], str(error))
except (DownloadCancelledError, asyncio.CancelledError, asyncio.TimeoutError):
pass
fut.add_done_callback(writer_finished_callback)
return writer
class BlobBuffer(AbstractBlob):
"""
An in-memory only blob
"""
def __init__(
self, loop: asyncio.AbstractEventLoop, blob_hash: str, length: typing.Optional[int] = None,
blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None,
blob_directory: typing.Optional[str] = None, added_on: typing.Optional[int] = None, is_mine: bool = False
):
self._verified_bytes: typing.Optional[BytesIO] = None
super().__init__(loop, blob_hash, length, blob_completed_callback, blob_directory, added_on, is_mine)
@contextlib.contextmanager
def _reader_context(self) -> typing.ContextManager[typing.BinaryIO]:
if not self.is_readable():
raise OSError("cannot open blob for reading")
try:
yield self._verified_bytes
finally:
if self._verified_bytes:
self._verified_bytes.close()
self._verified_bytes = None
self.verified.clear()
def _write_blob(self, blob_bytes: bytes):
async def write():
if self._verified_bytes:
raise OSError("already have bytes for blob")
self._verified_bytes = BytesIO(blob_bytes)
return self.loop.create_task(write())
def delete(self):
if self._verified_bytes:
self._verified_bytes.close()
self._verified_bytes = None
return super().delete()
def __del__(self):
super().__del__()
if self._verified_bytes:
self.delete()
class BlobFile(AbstractBlob):
"""
A blob existing on the local file system
"""
def __init__(
self, loop: asyncio.AbstractEventLoop, blob_hash: str, length: typing.Optional[int] = None,
blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None,
blob_directory: typing.Optional[str] = None, added_on: typing.Optional[int] = None, is_mine: bool = False
):
super().__init__(loop, blob_hash, length, blob_completed_callback, blob_directory, added_on, is_mine)
if not blob_directory or not os.path.isdir(blob_directory):
raise OSError(f"invalid blob directory '{blob_directory}'")
self.file_path = os.path.join(self.blob_directory, self.blob_hash)
if self.file_exists:
file_size = int(os.stat(self.file_path).st_size)
if length and length != file_size:
log.warning("expected %s to be %s bytes, file has %s", self.blob_hash, length, file_size)
self.delete()
else:
self.length = file_size
self.verified.set()
@property
def file_exists(self):
return os.path.isfile(self.file_path)
def is_writeable(self) -> bool:
return super().is_writeable() and not os.path.isfile(self.file_path)
def get_blob_writer(self, peer_address: typing.Optional[str] = None,
peer_port: typing.Optional[str] = None) -> HashBlobWriter:
if self.file_exists:
raise OSError(f"File already exists '{self.file_path}'")
return super().get_blob_writer(peer_address, peer_port)
@contextlib.contextmanager
def _reader_context(self) -> typing.ContextManager[typing.BinaryIO]:
handle = open(self.file_path, 'rb')
try:
yield handle
finally:
handle.close()
def _write_blob(self, blob_bytes: bytes):
def _write_blob():
with open(self.file_path, 'wb') as f:
f.write(blob_bytes)
async def write_blob():
await self.loop.run_in_executor(None, _write_blob)
return self.loop.create_task(write_blob())
def delete(self):
super().delete()
if os.path.isfile(self.file_path):
os.remove(self.file_path)
@classmethod
async def create_from_unencrypted(
cls, loop: asyncio.AbstractEventLoop, blob_dir: typing.Optional[str], key: bytes, iv: bytes,
unencrypted: bytes, blob_num: int, added_on: float, is_mine: bool,
blob_completed_callback: typing.Optional[typing.Callable[['AbstractBlob'], asyncio.Task]] = None
) -> BlobInfo:
if not blob_dir or not os.path.isdir(blob_dir):
raise OSError(f"cannot create blob in directory: '{blob_dir}'")
return await super().create_from_unencrypted(
loop, blob_dir, key, iv, unencrypted, blob_num, added_on, is_mine, blob_completed_callback
)
================================================
FILE: lbry/blob/blob_info.py
================================================
import typing
class BlobInfo:
__slots__ = [
'blob_hash',
'blob_num',
'length',
'iv',
'added_on',
'is_mine'
]
def __init__(
self, blob_num: int, length: int, iv: str, added_on,
blob_hash: typing.Optional[str] = None, is_mine=False):
self.blob_hash = blob_hash
self.blob_num = blob_num
self.length = length
self.iv = iv
self.added_on = added_on
self.is_mine = is_mine
def as_dict(self) -> typing.Dict:
d = {
'length': self.length,
'blob_num': self.blob_num,
'iv': self.iv,
}
if self.blob_hash: # non-terminator blobs have a blob hash
d['blob_hash'] = self.blob_hash
return d
================================================
FILE: lbry/blob/blob_manager.py
================================================
import os
import typing
import asyncio
import logging
from lbry.utils import LRUCacheWithMetrics
from lbry.blob.blob_file import is_valid_blobhash, BlobFile, BlobBuffer, AbstractBlob
from lbry.stream.descriptor import StreamDescriptor
from lbry.connection_manager import ConnectionManager
if typing.TYPE_CHECKING:
from lbry.conf import Config
from lbry.dht.protocol.data_store import DictDataStore
from lbry.extras.daemon.storage import SQLiteStorage
log = logging.getLogger(__name__)
class BlobManager:
def __init__(self, loop: asyncio.AbstractEventLoop, blob_dir: str, storage: 'SQLiteStorage', config: 'Config',
node_data_store: typing.Optional['DictDataStore'] = None):
"""
This class stores blobs on the hard disk
blob_dir - directory where blobs are stored
storage - SQLiteStorage object
"""
self.loop = loop
self.blob_dir = blob_dir
self.storage = storage
self._node_data_store = node_data_store
self.completed_blob_hashes: typing.Set[str] = set() if not self._node_data_store\
else self._node_data_store.completed_blobs
self.blobs: typing.Dict[str, AbstractBlob] = {}
self.config = config
self.decrypted_blob_lru_cache = None if not self.config.blob_lru_cache_size else LRUCacheWithMetrics(
self.config.blob_lru_cache_size)
self.connection_manager = ConnectionManager(loop)
def _get_blob(self, blob_hash: str, length: typing.Optional[int] = None, is_mine: bool = False):
if self.config.save_blobs or (
is_valid_blobhash(blob_hash) and os.path.isfile(os.path.join(self.blob_dir, blob_hash))):
return BlobFile(
self.loop, blob_hash, length, self.blob_completed, self.blob_dir, is_mine=is_mine
)
return BlobBuffer(
self.loop, blob_hash, length, self.blob_completed, self.blob_dir, is_mine=is_mine
)
def get_blob(self, blob_hash, length: typing.Optional[int] = None, is_mine: bool = False):
if blob_hash in self.blobs:
if self.config.save_blobs and isinstance(self.blobs[blob_hash], BlobBuffer):
buffer = self.blobs.pop(blob_hash)
if blob_hash in self.completed_blob_hashes:
self.completed_blob_hashes.remove(blob_hash)
self.blobs[blob_hash] = self._get_blob(blob_hash, length, is_mine)
if buffer.is_readable():
with buffer.reader_context() as reader:
self.blobs[blob_hash].write_blob(reader.read())
if length and self.blobs[blob_hash].length is None:
self.blobs[blob_hash].set_length(length)
else:
self.blobs[blob_hash] = self._get_blob(blob_hash, length, is_mine)
return self.blobs[blob_hash]
def is_blob_verified(self, blob_hash: str, length: typing.Optional[int] = None) -> bool:
if not is_valid_blobhash(blob_hash):
raise ValueError(blob_hash)
if not os.path.isfile(os.path.join(self.blob_dir, blob_hash)):
return False
if blob_hash in self.blobs:
return self.blobs[blob_hash].get_is_verified()
return self._get_blob(blob_hash, length).get_is_verified()
async def setup(self) -> bool:
def get_files_in_blob_dir() -> typing.Set[str]:
if not self.blob_dir:
return set()
return {
item.name for item in os.scandir(self.blob_dir) if is_valid_blobhash(item.name)
}
in_blobfiles_dir = await self.loop.run_in_executor(None, get_files_in_blob_dir)
to_add = await self.storage.sync_missing_blobs(in_blobfiles_dir)
if to_add:
self.completed_blob_hashes.update(to_add)
# check blobs that aren't set as finished but were seen on disk
await self.ensure_completed_blobs_status(in_blobfiles_dir - to_add)
if self.config.track_bandwidth:
self.connection_manager.start()
return True
def stop(self):
self.connection_manager.stop()
while self.blobs:
_, blob = self.blobs.popitem()
blob.close()
self.completed_blob_hashes.clear()
def get_stream_descriptor(self, sd_hash):
return StreamDescriptor.from_stream_descriptor_blob(self.loop, self.blob_dir, self.get_blob(sd_hash))
def blob_completed(self, blob: AbstractBlob) -> asyncio.Task:
if blob.blob_hash is None:
raise Exception("Blob hash is None")
if not blob.length:
raise Exception("Blob has a length of 0")
if isinstance(blob, BlobFile):
if blob.blob_hash not in self.completed_blob_hashes:
self.completed_blob_hashes.add(blob.blob_hash)
return self.loop.create_task(self.storage.add_blobs(
(blob.blob_hash, blob.length, blob.added_on, blob.is_mine), finished=True)
)
else:
return self.loop.create_task(self.storage.add_blobs(
(blob.blob_hash, blob.length, blob.added_on, blob.is_mine), finished=False)
)
async def ensure_completed_blobs_status(self, blob_hashes: typing.Iterable[str]):
"""Ensures that completed blobs from a given list of blob hashes are set as 'finished' in the database."""
to_add = []
for blob_hash in blob_hashes:
if not self.is_blob_verified(blob_hash):
continue
blob = self.get_blob(blob_hash)
to_add.append((blob.blob_hash, blob.length, blob.added_on, blob.is_mine))
if len(to_add) > 500:
await self.storage.add_blobs(*to_add, finished=True)
to_add.clear()
return await self.storage.add_blobs(*to_add, finished=True)
def delete_blob(self, blob_hash: str):
if not is_valid_blobhash(blob_hash):
raise Exception("invalid blob hash to delete")
if blob_hash not in self.blobs:
if self.blob_dir and os.path.isfile(os.path.join(self.blob_dir, blob_hash)):
os.remove(os.path.join(self.blob_dir, blob_hash))
else:
self.blobs.pop(blob_hash).delete()
if blob_hash in self.completed_blob_hashes:
self.completed_blob_hashes.remove(blob_hash)
async def delete_blobs(self, blob_hashes: typing.List[str], delete_from_db: typing.Optional[bool] = True):
for blob_hash in blob_hashes:
self.delete_blob(blob_hash)
if delete_from_db:
await self.storage.delete_blobs_from_db(blob_hashes)
================================================
FILE: lbry/blob/disk_space_manager.py
================================================
import asyncio
import logging
log = logging.getLogger(__name__)
class DiskSpaceManager:
def __init__(self, config, db, blob_manager, cleaning_interval=30 * 60, analytics=None):
self.config = config
self.db = db
self.blob_manager = blob_manager
self.cleaning_interval = cleaning_interval
self.running = False
self.task = None
self.analytics = analytics
self._used_space_bytes = None
async def get_free_space_mb(self, is_network_blob=False):
limit_mb = self.config.network_storage_limit if is_network_blob else self.config.blob_storage_limit
space_used_mb = await self.get_space_used_mb()
space_used_mb = space_used_mb['network_storage'] if is_network_blob else space_used_mb['content_storage']
return max(0, limit_mb - space_used_mb)
async def get_space_used_bytes(self):
self._used_space_bytes = await self.db.get_stored_blob_disk_usage()
return self._used_space_bytes
async def get_space_used_mb(self, cached=True):
cached = cached and self._used_space_bytes is not None
space_used_bytes = self._used_space_bytes if cached else await self.get_space_used_bytes()
return {key: int(value/1024.0/1024.0) for key, value in space_used_bytes.items()}
async def clean(self):
await self._clean(False)
await self._clean(True)
async def _clean(self, is_network_blob=False):
space_used_mb = await self.get_space_used_mb(cached=False)
if is_network_blob:
space_used_mb = space_used_mb['network_storage']
else:
space_used_mb = space_used_mb['content_storage'] + space_used_mb['private_storage']
storage_limit_mb = self.config.network_storage_limit if is_network_blob else self.config.blob_storage_limit
if self.analytics:
asyncio.create_task(
self.analytics.send_disk_space_used(space_used_mb, storage_limit_mb, is_network_blob)
)
delete = []
available = storage_limit_mb - space_used_mb
if storage_limit_mb == 0 if not is_network_blob else available >= 0:
return 0
for blob_hash, file_size, _ in await self.db.get_stored_blobs(is_mine=False, is_network_blob=is_network_blob):
delete.append(blob_hash)
available += int(file_size/1024.0/1024.0)
if available >= 0:
break
if delete:
await self.db.stop_all_files()
await self.blob_manager.delete_blobs(delete, delete_from_db=True)
self._used_space_bytes = None
return len(delete)
async def cleaning_loop(self):
while self.running:
await asyncio.sleep(self.cleaning_interval)
await self.clean()
async def start(self):
self.running = True
self.task = asyncio.create_task(self.cleaning_loop())
self.task.add_done_callback(lambda _: log.info("Stopping blob cleanup service."))
async def stop(self):
if self.running:
self.running = False
self.task.cancel()
================================================
FILE: lbry/blob/writer.py
================================================
import typing
import logging
import asyncio
from io import BytesIO
from lbry.error import InvalidBlobHashError, InvalidDataError
from lbry.utils import get_lbry_hash_obj
log = logging.getLogger(__name__)
class HashBlobWriter:
def __init__(self, expected_blob_hash: str, get_length: typing.Callable[[], int],
finished: asyncio.Future):
self.expected_blob_hash = expected_blob_hash
self.get_length = get_length
self.buffer = BytesIO()
self.finished = finished
self.finished.add_done_callback(lambda *_: self.close_handle())
self._hashsum = get_lbry_hash_obj()
self.len_so_far = 0
def __del__(self):
if self.buffer is not None:
log.warning("Garbage collection was called, but writer was not closed yet")
self.close_handle()
def calculate_blob_hash(self) -> str:
return self._hashsum.hexdigest()
def closed(self):
return self.buffer is None or self.buffer.closed
def write(self, data: bytes):
expected_length = self.get_length()
if not expected_length:
raise OSError("unknown blob length")
if self.buffer is None:
log.warning("writer has already been closed")
if not self.finished.done():
self.finished.cancel()
return
raise OSError('I/O operation on closed file')
self._hashsum.update(data)
self.len_so_far += len(data)
if self.len_so_far > expected_length:
self.finished.set_exception(InvalidDataError(
f'Length so far is greater than the expected length. {self.len_so_far} to {expected_length}'
))
self.close_handle()
return
self.buffer.write(data)
if self.len_so_far == expected_length:
blob_hash = self.calculate_blob_hash()
if blob_hash != self.expected_blob_hash:
self.finished.set_exception(InvalidBlobHashError(
f"blob hash is {blob_hash} vs expected {self.expected_blob_hash}"
))
elif self.finished and not (self.finished.done() or self.finished.cancelled()):
self.finished.set_result(self.buffer.getvalue())
self.close_handle()
def close_handle(self):
if not self.finished.done():
self.finished.cancel()
if self.buffer is not None:
self.buffer.close()
self.buffer = None
================================================
FILE: lbry/blob_exchange/__init__.py
================================================
================================================
FILE: lbry/blob_exchange/client.py
================================================
import asyncio
import time
import logging
import typing
import binascii
from typing import Optional
from lbry.error import InvalidBlobHashError, InvalidDataError
from lbry.blob_exchange.serialization import BlobResponse, BlobRequest
from lbry.utils import cache_concurrent
if typing.TYPE_CHECKING:
from lbry.blob.blob_file import AbstractBlob
from lbry.blob.writer import HashBlobWriter
from lbry.connection_manager import ConnectionManager
log = logging.getLogger(__name__)
class BlobExchangeClientProtocol(asyncio.Protocol):
def __init__(self, loop: asyncio.AbstractEventLoop, peer_timeout: typing.Optional[float] = 10,
connection_manager: typing.Optional['ConnectionManager'] = None):
self.loop = loop
self.peer_port: typing.Optional[int] = None
self.peer_address: typing.Optional[str] = None
self.transport: typing.Optional[asyncio.Transport] = None
self.peer_timeout = peer_timeout
self.connection_manager = connection_manager
self.writer: typing.Optional['HashBlobWriter'] = None
self.blob: typing.Optional['AbstractBlob'] = None
self._blob_bytes_received = 0
self._response_fut: typing.Optional[asyncio.Future] = None
self.buf = b''
# this is here to handle the race when the downloader is closed right as response_fut gets a result
self.closed = asyncio.Event()
def data_received(self, data: bytes):
if self.connection_manager:
if not self.peer_address:
addr_info = self.transport.get_extra_info('peername')
self.peer_address, self.peer_port = addr_info
# assert self.peer_address is not None
self.connection_manager.received_data(f"{self.peer_address}:{self.peer_port}", len(data))
if not self.transport or self.transport.is_closing():
log.warning("transport closing, but got more bytes from %s:%i\n%s", self.peer_address, self.peer_port,
binascii.hexlify(data))
if self._response_fut and not self._response_fut.done():
self._response_fut.cancel()
return
if not self._response_fut:
log.warning("Protocol received data before expected, probable race on keep alive. Closing transport.")
return self.close()
if self._blob_bytes_received and not self.writer.closed():
return self._write(data)
response = BlobResponse.deserialize(self.buf + data)
if not response.responses and not self._response_fut.done():
self.buf += data
return
else:
self.buf = b''
if response.responses and self.blob:
blob_response = response.get_blob_response()
if blob_response and not blob_response.error and blob_response.blob_hash == self.blob.blob_hash:
# set the expected length for the incoming blob if we didn't know it
self.blob.set_length(blob_response.length)
elif blob_response and not blob_response.error and self.blob.blob_hash != blob_response.blob_hash:
# the server started sending a blob we didn't request
log.warning("%s started sending blob we didn't request %s instead of %s", self.peer_address,
blob_response.blob_hash, self.blob.blob_hash)
return
if response.responses:
log.debug("got response from %s:%i <- %s", self.peer_address, self.peer_port, response.to_dict())
# fire the Future with the response to our request
self._response_fut.set_result(response)
if response.blob_data and self.writer and not self.writer.closed():
# log.debug("got %i blob bytes from %s:%i", len(response.blob_data), self.peer_address, self.peer_port)
# write blob bytes if we're writing a blob and have blob bytes to write
self._write(response.blob_data)
def _write(self, data: bytes):
if len(data) > (self.blob.get_length() - self._blob_bytes_received):
data = data[:(self.blob.get_length() - self._blob_bytes_received)]
log.warning("got more than asked from %s:%d, probable sendfile bug", self.peer_address, self.peer_port)
self._blob_bytes_received += len(data)
try:
self.writer.write(data)
except OSError as err:
log.error("error downloading blob from %s:%i: %s", self.peer_address, self.peer_port, err)
if self._response_fut and not self._response_fut.done():
self._response_fut.set_exception(err)
except asyncio.TimeoutError as err:
log.error("%s downloading blob from %s:%i", str(err), self.peer_address, self.peer_port)
if self._response_fut and not self._response_fut.done():
self._response_fut.set_exception(err)
async def _download_blob(self) -> typing.Tuple[int, Optional['BlobExchangeClientProtocol']]: # pylint: disable=too-many-return-statements
"""
:return: download success (bool), connected protocol (BlobExchangeClientProtocol)
"""
start_time = time.perf_counter()
request = BlobRequest.make_request_for_blob_hash(self.blob.blob_hash)
blob_hash = self.blob.blob_hash
if not self.peer_address:
addr_info = self.transport.get_extra_info('peername')
self.peer_address, self.peer_port = addr_info
try:
msg = request.serialize()
log.debug("send request to %s:%i -> %s", self.peer_address, self.peer_port, msg.decode())
self.transport.write(msg)
if self.connection_manager:
self.connection_manager.sent_data(f"{self.peer_address}:{self.peer_port}", len(msg))
response: BlobResponse = await asyncio.wait_for(self._response_fut, self.peer_timeout)
availability_response = response.get_availability_response()
price_response = response.get_price_response()
blob_response = response.get_blob_response()
if self.closed.is_set():
msg = f"cancelled blob request for {blob_hash} immediately after we got a response"
log.warning(msg)
raise asyncio.CancelledError(msg)
if (not blob_response or blob_response.error) and\
(not availability_response or not availability_response.available_blobs):
log.warning("%s not in availability response from %s:%i", self.blob.blob_hash, self.peer_address,
self.peer_port)
log.warning(response.to_dict())
return self._blob_bytes_received, self.close()
elif availability_response and availability_response.available_blobs and \
availability_response.available_blobs != [self.blob.blob_hash]:
log.warning("blob availability response doesn't match our request from %s:%i",
self.peer_address, self.peer_port)
return self._blob_bytes_received, self.close()
elif not availability_response:
log.warning("response from %s:%i did not include an availability response (we requested %s)",
self.peer_address, self.peer_port, blob_hash)
return self._blob_bytes_received, self.close()
if not price_response or price_response.blob_data_payment_rate != 'RATE_ACCEPTED':
log.warning("data rate rejected by %s:%i", self.peer_address, self.peer_port)
return self._blob_bytes_received, self.close()
if not blob_response or blob_response.error:
log.warning("blob can't be downloaded from %s:%i", self.peer_address, self.peer_port)
return self._blob_bytes_received, self.close()
if not blob_response.error and blob_response.blob_hash != self.blob.blob_hash:
log.warning("incoming blob hash mismatch from %s:%i", self.peer_address, self.peer_port)
return self._blob_bytes_received, self.close()
if self.blob.length is not None and self.blob.length != blob_response.length:
log.warning("incoming blob unexpected length from %s:%i", self.peer_address, self.peer_port)
return self._blob_bytes_received, self.close()
msg = f"downloading {self.blob.blob_hash[:8]} from {self.peer_address}:{self.peer_port}," \
f" timeout in {self.peer_timeout}"
log.debug(msg)
msg = f"downloaded {self.blob.blob_hash[:8]} from {self.peer_address}:{self.peer_port}"
await asyncio.wait_for(self.writer.finished, self.peer_timeout)
# wait for the io to finish
await self.blob.verified.wait()
log.info("%s at %fMB/s", msg,
round((float(self._blob_bytes_received) /
float(time.perf_counter() - start_time)) / 1000000.0, 2))
# await self.blob.finished_writing.wait() not necessary, but a dangerous change. TODO: is it needed?
return self._blob_bytes_received, self
except asyncio.TimeoutError:
return self._blob_bytes_received, self.close()
except (InvalidBlobHashError, InvalidDataError):
log.warning("invalid blob from %s:%i", self.peer_address, self.peer_port)
return self._blob_bytes_received, self.close()
def close(self):
self.closed.set()
if self._response_fut and not self._response_fut.done():
self._response_fut.cancel()
if self.writer and not self.writer.closed():
self.writer.close_handle()
self._response_fut = None
self.writer = None
self.blob = None
if self.transport:
self.transport.close()
self.transport = None
self.buf = b''
async def download_blob(self, blob: 'AbstractBlob') -> typing.Tuple[int, Optional['BlobExchangeClientProtocol']]:
self.closed.clear()
blob_hash = blob.blob_hash
if blob.get_is_verified() or not blob.is_writeable():
return 0, self
try:
self._blob_bytes_received = 0
self.blob, self.writer = blob, blob.get_blob_writer(self.peer_address, self.peer_port)
self._response_fut = asyncio.Future()
return await self._download_blob()
except OSError:
# i'm not sure how to fix this race condition - jack
log.warning("race happened downloading %s from %s:%s", blob_hash, self.peer_address, self.peer_port)
# return self._blob_bytes_received, self.transport
raise
except asyncio.TimeoutError:
if self._response_fut and not self._response_fut.done():
self._response_fut.cancel()
self.close()
return self._blob_bytes_received, None
except asyncio.CancelledError:
self.close()
raise
finally:
if self.writer and not self.writer.closed():
self.writer.close_handle()
self.writer = None
def connection_made(self, transport: asyncio.Transport):
addr = transport.get_extra_info('peername')
self.peer_address, self.peer_port = addr[0], addr[1]
self.transport = transport
if self.connection_manager:
self.connection_manager.connection_made(f"{self.peer_address}:{self.peer_port}")
log.debug("connection made to %s:%i", self.peer_address, self.peer_port)
def connection_lost(self, exc):
if self.connection_manager:
self.connection_manager.outgoing_connection_lost(f"{self.peer_address}:{self.peer_port}")
log.debug("connection lost to %s:%i (reason: %s, %s)", self.peer_address, self.peer_port, str(exc),
str(type(exc)))
self.close()
@cache_concurrent
async def request_blob(loop: asyncio.AbstractEventLoop, blob: Optional['AbstractBlob'], address: str,
tcp_port: int, peer_connect_timeout: float, blob_download_timeout: float,
connected_protocol: Optional['BlobExchangeClientProtocol'] = None,
connection_id: int = 0, connection_manager: Optional['ConnectionManager'] = None)\
-> typing.Tuple[int, Optional['BlobExchangeClientProtocol']]:
"""
Returns [, ]
"""
protocol = connected_protocol
if not connected_protocol or not connected_protocol.transport or connected_protocol.transport.is_closing():
connected_protocol = None
protocol = BlobExchangeClientProtocol(
loop, blob_download_timeout, connection_manager
)
else:
log.debug("reusing connection for %s:%d", address, tcp_port)
try:
if not connected_protocol:
await asyncio.wait_for(loop.create_connection(lambda: protocol, address, tcp_port),
peer_connect_timeout)
connected_protocol = protocol
if blob is None or blob.get_is_verified() or not blob.is_writeable():
# blob is None happens when we are just opening a connection
# file exists but not verified means someone is writing right now, give it time, come back later
return 0, connected_protocol
return await connected_protocol.download_blob(blob)
except (asyncio.TimeoutError, ConnectionRefusedError, ConnectionAbortedError, OSError):
return 0, None
================================================
FILE: lbry/blob_exchange/downloader.py
================================================
import asyncio
import typing
import logging
from lbry.utils import cache_concurrent
from lbry.blob_exchange.client import request_blob
from lbry.dht.node import get_kademlia_peers_from_hosts
if typing.TYPE_CHECKING:
from lbry.conf import Config
from lbry.dht.node import Node
from lbry.dht.peer import KademliaPeer
from lbry.blob.blob_manager import BlobManager
from lbry.blob.blob_file import AbstractBlob
from lbry.blob_exchange.client import BlobExchangeClientProtocol
log = logging.getLogger(__name__)
class BlobDownloader:
BAN_FACTOR = 2.0 # fixme: when connection manager gets implemented, move it out from here
def __init__(self, loop: asyncio.AbstractEventLoop, config: 'Config', blob_manager: 'BlobManager',
peer_queue: asyncio.Queue):
self.loop = loop
self.config = config
self.blob_manager = blob_manager
self.peer_queue = peer_queue
self.active_connections: typing.Dict['KademliaPeer', asyncio.Task] = {} # active request_blob calls
self.ignored: typing.Dict['KademliaPeer', int] = {}
self.scores: typing.Dict['KademliaPeer', int] = {}
self.failures: typing.Dict['KademliaPeer', int] = {}
self.connection_failures: typing.Set['KademliaPeer'] = set()
self.connections: typing.Dict['KademliaPeer', 'BlobExchangeClientProtocol'] = {}
self.is_running = asyncio.Event()
def should_race_continue(self, blob: 'AbstractBlob'):
max_probes = self.config.max_connections_per_download * (1 if self.connections else 10)
if len(self.active_connections) >= max_probes:
return False
return not (blob.get_is_verified() or not blob.is_writeable())
async def request_blob_from_peer(self, blob: 'AbstractBlob', peer: 'KademliaPeer', connection_id: int = 0,
just_probe: bool = False):
if blob.get_is_verified():
return
start = self.loop.time()
bytes_received, protocol = await request_blob(
self.loop, blob if not just_probe else None, peer.address, peer.tcp_port, self.config.peer_connect_timeout,
self.config.blob_download_timeout, connected_protocol=self.connections.get(peer),
connection_id=connection_id, connection_manager=self.blob_manager.connection_manager
)
if not bytes_received and not protocol and peer not in self.connection_failures:
self.connection_failures.add(peer)
if not protocol and peer not in self.ignored:
self.ignored[peer] = self.loop.time()
log.debug("drop peer %s:%i", peer.address, peer.tcp_port)
self.failures[peer] = self.failures.get(peer, 0) + 1
if peer in self.connections:
del self.connections[peer]
elif protocol:
log.debug("keep peer %s:%i", peer.address, peer.tcp_port)
self.failures[peer] = 0
self.connections[peer] = protocol
elapsed = self.loop.time() - start
self.scores[peer] = bytes_received / elapsed if bytes_received and elapsed else 1
async def new_peer_or_finished(self):
active_tasks = list(self.active_connections.values()) + [asyncio.create_task(asyncio.sleep(1))]
await asyncio.wait(active_tasks, return_when='FIRST_COMPLETED')
def cleanup_active(self):
if not self.active_connections and not self.connections:
self.clearbanned()
to_remove = [peer for (peer, task) in self.active_connections.items() if task.done()]
for peer in to_remove:
del self.active_connections[peer]
def clearbanned(self):
now = self.loop.time()
self.ignored = {
peer: when for (peer, when) in self.ignored.items()
if (now - when) < min(30.0, (self.failures.get(peer, 0) ** self.BAN_FACTOR))
}
@cache_concurrent
async def download_blob(self, blob_hash: str, length: typing.Optional[int] = None,
connection_id: int = 0) -> 'AbstractBlob':
blob = self.blob_manager.get_blob(blob_hash, length)
if blob.get_is_verified():
return blob
self.is_running.set()
try:
while not blob.get_is_verified() and self.is_running.is_set():
batch: typing.Set['KademliaPeer'] = set(self.connections.keys())
while not self.peer_queue.empty():
batch.update(self.peer_queue.get_nowait())
log.debug(
"%s running, %d peers, %d ignored, %d active, %s connections", blob_hash[:6],
len(batch), len(self.ignored), len(self.active_connections), len(self.connections)
)
for peer in sorted(batch, key=lambda peer: self.scores.get(peer, 0), reverse=True):
if peer in self.ignored:
continue
if peer in self.active_connections or not self.should_race_continue(blob):
continue
log.debug("request %s from %s:%i", blob_hash[:8], peer.address, peer.tcp_port)
t = self.loop.create_task(self.request_blob_from_peer(blob, peer, connection_id))
self.active_connections[peer] = t
self.peer_queue.put_nowait(list(batch))
await self.new_peer_or_finished()
self.cleanup_active()
log.debug("downloaded %s", blob_hash[:8])
return blob
finally:
blob.close()
if self.loop.is_running():
self.loop.call_soon(self.cleanup_active)
def close(self):
self.connection_failures.clear()
self.scores.clear()
self.ignored.clear()
self.is_running.clear()
for protocol in self.connections.values():
protocol.close()
async def download_blob(loop, config: 'Config', blob_manager: 'BlobManager', dht_node: 'Node',
blob_hash: str) -> 'AbstractBlob':
search_queue = asyncio.Queue(maxsize=config.max_connections_per_download)
search_queue.put_nowait(blob_hash)
peer_queue, accumulate_task = dht_node.accumulate_peers(search_queue)
fixed_peers = None if not config.fixed_peers else await get_kademlia_peers_from_hosts(config.fixed_peers)
if fixed_peers:
loop.call_later(config.fixed_peer_delay, peer_queue.put_nowait, fixed_peers)
downloader = BlobDownloader(loop, config, blob_manager, peer_queue)
try:
return await downloader.download_blob(blob_hash)
finally:
if accumulate_task and not accumulate_task.done():
accumulate_task.cancel()
downloader.close()
================================================
FILE: lbry/blob_exchange/serialization.py
================================================
import typing
import json
import logging
log = logging.getLogger(__name__)
class BlobMessage:
key = ''
def to_dict(self) -> typing.Dict:
raise NotImplementedError()
class BlobPriceRequest(BlobMessage):
key = 'blob_data_payment_rate'
def __init__(self, blob_data_payment_rate: float, **kwargs) -> None:
self.blob_data_payment_rate = blob_data_payment_rate
def to_dict(self) -> typing.Dict:
return {
self.key: self.blob_data_payment_rate
}
class BlobPriceResponse(BlobMessage):
key = 'blob_data_payment_rate'
rate_accepted = 'RATE_ACCEPTED'
rate_too_low = 'RATE_TOO_LOW'
rate_unset = 'RATE_UNSET'
def __init__(self, blob_data_payment_rate: str, **kwargs) -> None:
if blob_data_payment_rate not in (self.rate_accepted, self.rate_too_low, self.rate_unset):
raise ValueError(blob_data_payment_rate)
self.blob_data_payment_rate = blob_data_payment_rate
def to_dict(self) -> typing.Dict:
return {
self.key: self.blob_data_payment_rate
}
class BlobAvailabilityRequest(BlobMessage):
key = 'requested_blobs'
def __init__(self, requested_blobs: typing.List[str], lbrycrd_address: typing.Optional[bool] = True,
**kwargs) -> None:
assert len(requested_blobs) > 0
self.requested_blobs = requested_blobs
self.lbrycrd_address = lbrycrd_address
def to_dict(self) -> typing.Dict:
return {
self.key: self.requested_blobs,
'lbrycrd_address': self.lbrycrd_address
}
class BlobAvailabilityResponse(BlobMessage):
key = 'available_blobs'
def __init__(self, available_blobs: typing.List[str], lbrycrd_address: typing.Optional[str] = True,
**kwargs) -> None:
self.available_blobs = available_blobs
self.lbrycrd_address = lbrycrd_address
def to_dict(self) -> typing.Dict:
d = {
self.key: self.available_blobs
}
if self.lbrycrd_address:
d['lbrycrd_address'] = self.lbrycrd_address
return d
class BlobDownloadRequest(BlobMessage):
key = 'requested_blob'
def __init__(self, requested_blob: str, **kwargs) -> None:
self.requested_blob = requested_blob
def to_dict(self) -> typing.Dict:
return {
self.key: self.requested_blob
}
class BlobDownloadResponse(BlobMessage):
key = 'incoming_blob'
def __init__(self, **response: typing.Dict) -> None:
incoming_blob = response[self.key]
self.error = None
self.incoming_blob = None
if 'error' in incoming_blob:
self.error = incoming_blob['error']
else:
self.incoming_blob = {'blob_hash': incoming_blob['blob_hash'], 'length': incoming_blob['length']}
self.length = None if not self.incoming_blob else self.incoming_blob['length']
self.blob_hash = None if not self.incoming_blob else self.incoming_blob['blob_hash']
def to_dict(self) -> typing.Dict:
return {
self.key if not self.error else 'error': self.incoming_blob or self.error,
}
class BlobPaymentAddressRequest(BlobMessage):
key = 'lbrycrd_address'
def __init__(self, lbrycrd_address: str, **kwargs) -> None:
self.lbrycrd_address = lbrycrd_address
def to_dict(self) -> typing.Dict:
return {
self.key: self.lbrycrd_address
}
class BlobPaymentAddressResponse(BlobPaymentAddressRequest):
pass
class BlobErrorResponse(BlobMessage):
key = 'error'
def __init__(self, error: str, **kwargs) -> None:
self.error = error
def to_dict(self) -> typing.Dict:
return {
self.key: self.error
}
blob_request_types = typing.Union[BlobPriceRequest, BlobAvailabilityRequest, BlobDownloadRequest, # pylint: disable=invalid-name
BlobPaymentAddressRequest]
blob_response_types = typing.Union[BlobPriceResponse, BlobAvailabilityResponse, BlobDownloadResponse, # pylint: disable=invalid-name
BlobErrorResponse, BlobPaymentAddressResponse]
def _parse_blob_response(response_msg: bytes) -> typing.Tuple[typing.Optional[typing.Dict], bytes]:
# scenarios:
#
#
#
curr_pos = 0
while True:
next_close_paren = response_msg.find(b'}', curr_pos)
if next_close_paren == -1:
return None, response_msg
curr_pos = next_close_paren + 1
try:
response = json.loads(response_msg[:curr_pos])
except ValueError:
continue
possible_response_keys = {
BlobPaymentAddressResponse.key,
BlobAvailabilityResponse.key,
BlobPriceResponse.key,
BlobDownloadResponse.key
}
if isinstance(response, dict) and response.keys():
if set(response.keys()).issubset(possible_response_keys):
return response, response_msg[curr_pos:]
return None, response_msg
class BlobRequest:
def __init__(self, requests: typing.List[blob_request_types]) -> None:
self.requests = requests
def to_dict(self):
d = {}
for request in self.requests:
d.update(request.to_dict())
return d
def _get_request(self, request_type: blob_request_types):
request = tuple(filter(lambda r: type(r) == request_type, self.requests)) # pylint: disable=unidiomatic-typecheck
if request:
return request[0]
def get_availability_request(self) -> typing.Optional[BlobAvailabilityRequest]:
response = self._get_request(BlobAvailabilityRequest)
if response:
return response
def get_price_request(self) -> typing.Optional[BlobPriceRequest]:
response = self._get_request(BlobPriceRequest)
if response:
return response
def get_blob_request(self) -> typing.Optional[BlobDownloadRequest]:
response = self._get_request(BlobDownloadRequest)
if response:
return response
def get_address_request(self) -> typing.Optional[BlobPaymentAddressRequest]:
response = self._get_request(BlobPaymentAddressRequest)
if response:
return response
def serialize(self) -> bytes:
return json.dumps(self.to_dict()).encode()
@classmethod
def deserialize(cls, data: bytes) -> 'BlobRequest':
request = json.loads(data)
return cls([
request_type(**request)
for request_type in (BlobPriceRequest, BlobAvailabilityRequest, BlobDownloadRequest,
BlobPaymentAddressRequest)
if request_type.key in request
])
@classmethod
def make_request_for_blob_hash(cls, blob_hash: str) -> 'BlobRequest':
return cls(
[BlobAvailabilityRequest([blob_hash]), BlobPriceRequest(0.0), BlobDownloadRequest(blob_hash)]
)
class BlobResponse:
def __init__(self, responses: typing.List[blob_response_types], blob_data: typing.Optional[bytes] = None) -> None:
self.responses = responses
self.blob_data = blob_data
def to_dict(self):
d = {}
for response in self.responses:
d.update(response.to_dict())
return d
def _get_response(self, response_type: blob_response_types):
response = tuple(filter(lambda r: type(r) == response_type, self.responses)) # pylint: disable=unidiomatic-typecheck
if response:
return response[0]
def get_error_response(self) -> typing.Optional[BlobErrorResponse]:
error = self._get_response(BlobErrorResponse)
if error:
log.error(error)
return error
def get_availability_response(self) -> typing.Optional[BlobAvailabilityResponse]:
response = self._get_response(BlobAvailabilityResponse)
if response:
return response
def get_price_response(self) -> typing.Optional[BlobPriceResponse]:
response = self._get_response(BlobPriceResponse)
if response:
return response
def get_blob_response(self) -> typing.Optional[BlobDownloadResponse]:
response = self._get_response(BlobDownloadResponse)
if response:
return response
def get_address_response(self) -> typing.Optional[BlobPaymentAddressResponse]:
response = self._get_response(BlobPaymentAddressResponse)
if response:
return response
def serialize(self) -> bytes:
return json.dumps(self.to_dict()).encode()
@classmethod
def deserialize(cls, data: bytes) -> 'BlobResponse':
response, extra = _parse_blob_response(data)
requests = []
if response:
requests.extend([
response_type(**response)
for response_type in (BlobPriceResponse, BlobAvailabilityResponse, BlobDownloadResponse,
BlobErrorResponse, BlobPaymentAddressResponse)
if response_type.key in response
])
return cls(requests, extra)
================================================
FILE: lbry/blob_exchange/server.py
================================================
import asyncio
import binascii
import logging
import socket
import typing
from json.decoder import JSONDecodeError
from lbry.blob_exchange.serialization import BlobResponse, BlobRequest, blob_response_types
from lbry.blob_exchange.serialization import BlobAvailabilityResponse, BlobPriceResponse, BlobDownloadResponse, \
BlobPaymentAddressResponse
if typing.TYPE_CHECKING:
from lbry.blob.blob_manager import BlobManager
log = logging.getLogger(__name__)
# a standard request will be 295 bytes
MAX_REQUEST_SIZE = 1200
class BlobServerProtocol(asyncio.Protocol):
def __init__(self, loop: asyncio.AbstractEventLoop, blob_manager: 'BlobManager', lbrycrd_address: str,
idle_timeout: float = 30.0, transfer_timeout: float = 60.0):
self.loop = loop
self.blob_manager = blob_manager
self.idle_timeout = idle_timeout
self.transfer_timeout = transfer_timeout
self.server_task: typing.Optional[asyncio.Task] = None
self.started_listening = asyncio.Event()
self.buf = b''
self.transport: typing.Optional[asyncio.Transport] = None
self.lbrycrd_address = lbrycrd_address
self.peer_address_and_port: typing.Optional[str] = None
self.started_transfer = asyncio.Event()
self.transfer_finished = asyncio.Event()
self.close_on_idle_task: typing.Optional[asyncio.Task] = None
async def close_on_idle(self):
while self.transport:
try:
await asyncio.wait_for(self.started_transfer.wait(), self.idle_timeout)
except asyncio.TimeoutError:
log.debug("closing idle connection from %s", self.peer_address_and_port)
return self.close()
self.started_transfer.clear()
await self.transfer_finished.wait()
self.transfer_finished.clear()
def close(self):
if self.transport:
self.transport.close()
def connection_made(self, transport):
self.transport = transport
self.close_on_idle_task = self.loop.create_task(self.close_on_idle())
self.peer_address_and_port = "%s:%i" % self.transport.get_extra_info('peername')
self.blob_manager.connection_manager.connection_received(self.peer_address_and_port)
log.debug("received connection from %s", self.peer_address_and_port)
def connection_lost(self, exc: typing.Optional[Exception]) -> None:
log.debug("lost connection from %s", self.peer_address_and_port)
self.blob_manager.connection_manager.incoming_connection_lost(self.peer_address_and_port)
self.transport = None
if self.close_on_idle_task and not self.close_on_idle_task.done():
self.close_on_idle_task.cancel()
self.close_on_idle_task = None
def send_response(self, responses: typing.List[blob_response_types]):
to_send = []
while responses:
to_send.append(responses.pop())
serialized = BlobResponse(to_send).serialize()
self.transport.write(serialized)
self.blob_manager.connection_manager.sent_data(self.peer_address_and_port, len(serialized))
async def handle_request(self, request: BlobRequest):
addr = self.transport.get_extra_info('peername')
peer_address, peer_port = addr
responses = []
address_request = request.get_address_request()
if address_request:
responses.append(BlobPaymentAddressResponse(lbrycrd_address=self.lbrycrd_address))
availability_request = request.get_availability_request()
if availability_request:
responses.append(BlobAvailabilityResponse(available_blobs=list(set(
filter(lambda blob_hash: blob_hash in self.blob_manager.completed_blob_hashes,
availability_request.requested_blobs)
))))
price_request = request.get_price_request()
if price_request:
responses.append(BlobPriceResponse(blob_data_payment_rate='RATE_ACCEPTED'))
download_request = request.get_blob_request()
if download_request:
blob = self.blob_manager.get_blob(download_request.requested_blob)
if blob.get_is_verified():
incoming_blob = {'blob_hash': blob.blob_hash, 'length': blob.length}
responses.append(BlobDownloadResponse(incoming_blob=incoming_blob))
self.send_response(responses)
blob_hash = blob.blob_hash[:8]
log.debug("send %s to %s:%i", blob_hash, peer_address, peer_port)
self.started_transfer.set()
try:
sent = await asyncio.wait_for(blob.sendfile(self), self.transfer_timeout)
if sent and sent > 0:
self.blob_manager.connection_manager.sent_data(self.peer_address_and_port, sent)
log.info("sent %s (%i bytes) to %s:%i", blob_hash, sent, peer_address, peer_port)
else:
self.close()
log.debug("stopped sending %s to %s:%i", blob_hash, peer_address, peer_port)
return
except (OSError, ValueError, asyncio.TimeoutError) as err:
if isinstance(err, asyncio.TimeoutError):
log.debug("timed out sending blob %s to %s", blob_hash, peer_address)
else:
log.warning("could not read blob %s to send %s:%i", blob_hash, peer_address, peer_port)
self.close()
return
finally:
self.transfer_finished.set()
else:
log.info("don't have %s to send %s:%i", blob.blob_hash[:8], peer_address, peer_port)
if responses and not self.transport.is_closing():
self.send_response(responses)
def data_received(self, data):
request = None
if len(self.buf) + len(data or b'') >= MAX_REQUEST_SIZE:
log.warning("request from %s is too large", self.peer_address_and_port)
self.close()
return
if data:
self.blob_manager.connection_manager.received_data(self.peer_address_and_port, len(data))
_, separator, remainder = data.rpartition(b'}')
if not separator:
self.buf += data
return
try:
request = BlobRequest.deserialize(self.buf + data)
self.buf = remainder
except (UnicodeDecodeError, JSONDecodeError):
log.error("request from %s is not valid json (%i bytes): %s", self.peer_address_and_port,
len(self.buf + data), '' if not data else binascii.hexlify(self.buf + data).decode())
self.close()
return
if not request.requests:
log.error("failed to decode request from %s (%i bytes): %s", self.peer_address_and_port,
len(self.buf + data), '' if not data else binascii.hexlify(self.buf + data).decode())
self.close()
return
self.loop.create_task(self.handle_request(request))
class BlobServer:
def __init__(self, loop: asyncio.AbstractEventLoop, blob_manager: 'BlobManager', lbrycrd_address: str,
idle_timeout: float = 30.0, transfer_timeout: float = 60.0):
self.loop = loop
self.blob_manager = blob_manager
self.server_task: typing.Optional[asyncio.Task] = None
self.started_listening = asyncio.Event()
self.lbrycrd_address = lbrycrd_address
self.idle_timeout = idle_timeout
self.transfer_timeout = transfer_timeout
self.server_protocol_class = BlobServerProtocol
def start_server(self, port: int, interface: typing.Optional[str] = '0.0.0.0'):
if self.server_task is not None:
raise Exception("already running")
async def _start_server():
# checking if the port is in use
# thx https://stackoverflow.com/a/52872579
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(('localhost', port)) == 0:
# the port is already in use!
log.error("Failed to bind TCP %s:%d", interface, port)
server = await self.loop.create_server(
lambda: self.server_protocol_class(self.loop, self.blob_manager, self.lbrycrd_address,
self.idle_timeout, self.transfer_timeout),
interface, port
)
self.started_listening.set()
log.info("Blob server listening on TCP %s:%i", interface, port)
async with server:
await server.serve_forever()
self.server_task = self.loop.create_task(_start_server())
def stop_server(self):
if self.server_task:
self.server_task.cancel()
self.server_task = None
log.info("Stopped blob server")
================================================
FILE: lbry/build_info.py
================================================
# don't touch this. CI server changes this during build/deployment
BUILD = "dev"
COMMIT_HASH = "none"
DOCKER_TAG = "none"
================================================
FILE: lbry/conf.py
================================================
import os
import re
import sys
import logging
from typing import List, Dict, Tuple, Union, TypeVar, Generic, Optional
from argparse import ArgumentParser
from contextlib import contextmanager
from appdirs import user_data_dir, user_config_dir
import yaml
from lbry.error import InvalidCurrencyError
from lbry.dht import constants
from lbry.wallet.coinselection import STRATEGIES
log = logging.getLogger(__name__)
NOT_SET = type('NOT_SET', (object,), {}) # pylint: disable=invalid-name
T = TypeVar('T')
CURRENCIES = {
'BTC': {'type': 'crypto'},
'LBC': {'type': 'crypto'},
'USD': {'type': 'fiat'},
}
class Setting(Generic[T]):
def __init__(self, doc: str, default: Optional[T] = None,
previous_names: Optional[List[str]] = None,
metavar: Optional[str] = None):
self.doc = doc
self.default = default
self.previous_names = previous_names or []
self.metavar = metavar
def __set_name__(self, owner, name):
self.name = name # pylint: disable=attribute-defined-outside-init
@property
def cli_name(self):
return f"--{self.name.replace('_', '-')}"
@property
def no_cli_name(self):
return f"--no-{self.name.replace('_', '-')}"
def __get__(self, obj: Optional['BaseConfig'], owner) -> T:
if obj is None:
return self
for location in obj.search_order:
if self.name in location:
return location[self.name]
return self.default
def __set__(self, obj: 'BaseConfig', val: Union[T, NOT_SET]):
if val == NOT_SET:
for location in obj.modify_order:
if self.name in location:
del location[self.name]
else:
self.validate(val)
for location in obj.modify_order:
location[self.name] = val
def is_set(self, obj: 'BaseConfig') -> bool:
for location in obj.search_order:
if self.name in location:
return True
return False
def is_set_to_default(self, obj: 'BaseConfig') -> bool:
for location in obj.search_order:
if self.name in location:
return location[self.name] == self.default
return False
def validate(self, value):
raise NotImplementedError()
def deserialize(self, value): # pylint: disable=no-self-use
return value
def serialize(self, value): # pylint: disable=no-self-use
return value
def contribute_to_argparse(self, parser: ArgumentParser):
parser.add_argument(
self.cli_name,
help=self.doc,
metavar=self.metavar,
default=NOT_SET
)
class String(Setting[str]):
def validate(self, value):
assert isinstance(value, str), \
f"Setting '{self.name}' must be a string."
# TODO: removes this after pylint starts to understand generics
def __get__(self, obj: Optional['BaseConfig'], owner) -> str: # pylint: disable=useless-super-delegation
return super().__get__(obj, owner)
class Integer(Setting[int]):
def validate(self, value):
assert isinstance(value, int), \
f"Setting '{self.name}' must be an integer."
def deserialize(self, value):
return int(value)
class Float(Setting[float]):
def validate(self, value):
assert isinstance(value, float), \
f"Setting '{self.name}' must be a decimal."
def deserialize(self, value):
return float(value)
class Toggle(Setting[bool]):
def validate(self, value):
assert isinstance(value, bool), \
f"Setting '{self.name}' must be a true/false value."
def contribute_to_argparse(self, parser: ArgumentParser):
parser.add_argument(
self.cli_name,
help=self.doc,
action="store_true",
default=NOT_SET
)
parser.add_argument(
self.no_cli_name,
help=f"Opposite of {self.cli_name}",
dest=self.name,
action="store_false",
default=NOT_SET
)
class Path(String):
def __init__(self, doc: str, *args, default: str = '', **kwargs):
super().__init__(doc, default, *args, **kwargs)
def __get__(self, obj, owner) -> str:
value = super().__get__(obj, owner)
if isinstance(value, str):
return os.path.expanduser(os.path.expandvars(value))
return value
class MaxKeyFee(Setting[dict]):
def validate(self, value):
if value is not None:
assert isinstance(value, dict) and set(value) == {'currency', 'amount'}, \
f"Setting '{self.name}' must be a dict like \"{{'amount': 50.0, 'currency': 'USD'}}\"."
if value["currency"] not in CURRENCIES:
raise InvalidCurrencyError(value["currency"])
@staticmethod
def _parse_list(l):
if l == ['null']:
return None
assert len(l) == 2, (
'Max key fee is made up of either two values: '
'"AMOUNT CURRENCY", or "null" (to set no limit)'
)
try:
amount = float(l[0])
except ValueError:
raise AssertionError('First value in max key fee is a decimal: "AMOUNT CURRENCY"')
currency = str(l[1]).upper()
if currency not in CURRENCIES:
raise InvalidCurrencyError(currency)
return {'amount': amount, 'currency': currency}
def deserialize(self, value):
if value is None:
return
if isinstance(value, dict):
return {
'currency': value['currency'],
'amount': float(value['amount']),
}
if isinstance(value, str):
value = value.split()
if isinstance(value, list):
return self._parse_list(value)
raise AssertionError('Invalid max key fee.')
def contribute_to_argparse(self, parser: ArgumentParser):
parser.add_argument(
self.cli_name,
help=self.doc,
nargs='+',
metavar=('AMOUNT', 'CURRENCY'),
default=NOT_SET
)
parser.add_argument(
self.no_cli_name,
help="Disable maximum key fee check.",
dest=self.name,
const=None,
action="store_const",
default=NOT_SET
)
class StringChoice(String):
def __init__(self, doc: str, valid_values: List[str], default: str, *args, **kwargs):
super().__init__(doc, default, *args, **kwargs)
if not valid_values:
raise ValueError("No valid values provided")
if default not in valid_values:
raise ValueError(f"Default value must be one of: {', '.join(valid_values)}")
self.valid_values = valid_values
def validate(self, value):
super().validate(value)
if value not in self.valid_values:
raise ValueError(f"Setting '{self.name}' value must be one of: {', '.join(self.valid_values)}")
class ListSetting(Setting[list]):
def validate(self, value):
assert isinstance(value, (tuple, list)), \
f"Setting '{self.name}' must be a tuple or list."
def contribute_to_argparse(self, parser: ArgumentParser):
parser.add_argument(
self.cli_name,
help=self.doc,
action='append'
)
class Servers(ListSetting):
def validate(self, value):
assert isinstance(value, (tuple, list)), \
f"Setting '{self.name}' must be a tuple or list of servers."
for idx, server in enumerate(value):
assert isinstance(server, (tuple, list)) and len(server) == 2, \
f"Server defined '{server}' at index {idx} in setting " \
f"'{self.name}' must be a tuple or list of two items."
assert isinstance(server[0], str), \
f"Server defined '{server}' at index {idx} in setting " \
f"'{self.name}' must be have hostname as string in first position."
assert isinstance(server[1], int), \
f"Server defined '{server}' at index {idx} in setting " \
f"'{self.name}' must be have port as int in second position."
def deserialize(self, value):
servers = []
if isinstance(value, list):
for server in value:
if isinstance(server, str) and server.count(':') == 1:
host, port = server.split(':')
try:
servers.append((host, int(port)))
except ValueError:
pass
return servers
def serialize(self, value):
if value:
return [f"{host}:{port}" for host, port in value]
return value
class Strings(ListSetting):
def validate(self, value):
assert isinstance(value, (tuple, list)), \
f"Setting '{self.name}' must be a tuple or list of strings."
for idx, string in enumerate(value):
assert isinstance(string, str), \
f"Value of '{string}' at index {idx} in setting " \
f"'{self.name}' must be a string."
class KnownHubsList:
def __init__(self, config: 'Config' = None, file_name: str = 'known_hubs.yml'):
self.file_name = file_name
self.path = os.path.join(config.wallet_dir, self.file_name) if config else None
self.hubs: Dict[Tuple[str, int], Dict] = {}
if self.exists:
self.load()
@property
def exists(self):
return self.path and os.path.exists(self.path)
@property
def serialized(self) -> Dict[str, Dict]:
return {f"{host}:{port}": details for (host, port), details in self.hubs.items()}
def filter(self, match_none=False, **kwargs):
if not kwargs:
return self.hubs
result = {}
for hub, details in self.hubs.items():
for key, constraint in kwargs.items():
value = details.get(key)
if value == constraint or (match_none and value is None):
result[hub] = details
break
return result
def load(self):
if self.path:
with open(self.path, 'r') as known_hubs_file:
raw = known_hubs_file.read()
for hub, details in yaml.safe_load(raw).items():
self.set(hub, details)
def save(self):
if self.path:
with open(self.path, 'w') as known_hubs_file:
known_hubs_file.write(yaml.safe_dump(self.serialized, default_flow_style=False))
def set(self, hub: str, details: Dict):
if hub and hub.count(':') == 1:
host, port = hub.split(':')
hub_parts = (host, int(port))
if hub_parts not in self.hubs:
self.hubs[hub_parts] = details
return hub
def add_hubs(self, hubs: List[str]):
added = False
for hub in hubs:
if self.set(hub, {}) is not None:
added = True
return added
def items(self):
return self.hubs.items()
def __bool__(self):
return len(self) > 0
def __len__(self):
return self.hubs.__len__()
def __iter__(self):
return iter(self.hubs)
class EnvironmentAccess:
PREFIX = 'LBRY_'
def __init__(self, config: 'BaseConfig', environ: dict):
self.configuration = config
self.data = {}
if environ:
self.load(environ)
def load(self, environ):
for setting in self.configuration.get_settings():
value = environ.get(f'{self.PREFIX}{setting.name.upper()}', NOT_SET)
if value != NOT_SET and not (isinstance(setting, ListSetting) and value is None):
self.data[setting.name] = setting.deserialize(value)
def __contains__(self, item: str):
return item in self.data
def __getitem__(self, item: str):
return self.data[item]
class ArgumentAccess:
def __init__(self, config: 'BaseConfig', args: dict):
self.configuration = config
self.args = {}
if args:
self.load(args)
def load(self, args):
for setting in self.configuration.get_settings():
value = getattr(args, setting.name, NOT_SET)
if value != NOT_SET and not (isinstance(setting, ListSetting) and value is None):
self.args[setting.name] = setting.deserialize(value)
def __contains__(self, item: str):
return item in self.args
def __getitem__(self, item: str):
return self.args[item]
class ConfigFileAccess:
def __init__(self, config: 'BaseConfig', path: str):
self.configuration = config
self.path = path
self.data = {}
if self.exists:
self.load()
@property
def exists(self):
return self.path and os.path.exists(self.path)
def load(self):
cls = type(self.configuration)
with open(self.path, 'r') as config_file:
raw = config_file.read()
serialized = yaml.safe_load(raw) or {}
for key, value in serialized.items():
attr = getattr(cls, key, None)
if attr is None:
for setting in self.configuration.settings:
if key in setting.previous_names:
attr = setting
break
if attr is not None:
self.data[key] = attr.deserialize(value)
def save(self):
cls = type(self.configuration)
serialized = {}
for key, value in self.data.items():
attr = getattr(cls, key)
serialized[key] = attr.serialize(value)
with open(self.path, 'w') as config_file:
config_file.write(yaml.safe_dump(serialized, default_flow_style=False))
def upgrade(self) -> bool:
upgraded = False
for key in list(self.data):
for setting in self.configuration.settings:
if key in setting.previous_names:
self.data[setting.name] = self.data[key]
del self.data[key]
upgraded = True
break
return upgraded
def __contains__(self, item: str):
return item in self.data
def __getitem__(self, item: str):
return self.data[item]
def __setitem__(self, key, value):
self.data[key] = value
def __delitem__(self, key):
del self.data[key]
TBC = TypeVar('TBC', bound='BaseConfig')
class BaseConfig:
config = Path("Path to configuration file.", metavar='FILE')
def __init__(self, **kwargs):
self.runtime = {} # set internally or by various API calls
self.arguments = {} # from command line arguments
self.environment = {} # from environment variables
self.persisted = {} # from config file
self._updating_config = False
for key, value in kwargs.items():
setattr(self, key, value)
@contextmanager
def update_config(self):
self._updating_config = True
yield self
self._updating_config = False
if isinstance(self.persisted, ConfigFileAccess):
self.persisted.save()
@property
def modify_order(self):
locations = [self.runtime]
if self._updating_config:
locations.append(self.persisted)
return locations
@property
def search_order(self):
return [
self.runtime,
self.arguments,
self.environment,
self.persisted
]
@classmethod
def get_settings(cls):
for attr in dir(cls):
setting = getattr(cls, attr)
if isinstance(setting, Setting):
yield setting
@property
def settings(self):
return self.get_settings()
@property
def settings_dict(self):
return {
setting.name: getattr(self, setting.name) for setting in self.settings
}
@classmethod
def create_from_arguments(cls, args) -> TBC:
conf = cls()
conf.set_arguments(args)
conf.set_environment()
conf.set_persisted()
return conf
@classmethod
def contribute_to_argparse(cls, parser: ArgumentParser):
for setting in cls.get_settings():
setting.contribute_to_argparse(parser)
def set_arguments(self, args):
self.arguments = ArgumentAccess(self, args)
def set_environment(self, environ=None):
self.environment = EnvironmentAccess(self, environ or os.environ)
def set_persisted(self, config_file_path=None):
if config_file_path is None:
config_file_path = self.config
if not config_file_path:
return
ext = os.path.splitext(config_file_path)[1]
assert ext in ('.yml', '.yaml'),\
f"File extension '{ext}' is not supported, " \
f"configuration file must be in YAML (.yaml)."
self.persisted = ConfigFileAccess(self, config_file_path)
if self.persisted.upgrade():
self.persisted.save()
class TranscodeConfig(BaseConfig):
ffmpeg_path = String('A list of places to check for ffmpeg and ffprobe. '
f'$data_dir/ffmpeg/bin and $PATH are checked afterward. Separator: {os.pathsep}',
'', previous_names=['ffmpeg_folder'])
video_encoder = String('FFmpeg codec and parameters for the video encoding. '
'Example: libaom-av1 -crf 25 -b:v 0 -strict experimental',
'libx264 -crf 24 -preset faster -pix_fmt yuv420p')
video_bitrate_maximum = Integer('Maximum bits per second allowed for video streams (0 to disable).', 5_000_000)
video_scaler = String('FFmpeg scaling parameters for reducing bitrate. '
'Example: -vf "scale=-2:720,fps=24" -maxrate 5M -bufsize 3M',
r'-vf "scale=if(gte(iw\,ih)\,min(1920\,iw)\,-2):if(lt(iw\,ih)\,min(1920\,ih)\,-2)" '
r'-maxrate 5500K -bufsize 5000K')
audio_encoder = String('FFmpeg codec and parameters for the audio encoding. '
'Example: libopus -b:a 128k',
'aac -b:a 160k')
volume_filter = String('FFmpeg filter for audio normalization. Exmple: -af loudnorm', '')
volume_analysis_time = Integer('Maximum seconds into the file that we examine audio volume (0 to disable).', 240)
class CLIConfig(TranscodeConfig):
api = String('Host name and port for lbrynet daemon API.', 'localhost:5279', metavar='HOST:PORT')
@property
def api_connection_url(self) -> str:
return f"http://{self.api}/lbryapi"
@property
def api_host(self):
return self.api.split(':')[0]
@property
def api_port(self):
return int(self.api.split(':')[1])
class Config(CLIConfig):
jurisdiction = String("Limit interactions to wallet server in this jurisdiction.")
# directories
data_dir = Path("Directory path to store blobs.", metavar='DIR')
download_dir = Path(
"Directory path to place assembled files downloaded from LBRY.",
previous_names=['download_directory'], metavar='DIR'
)
wallet_dir = Path(
"Directory containing a 'wallets' subdirectory with 'default_wallet' file.",
previous_names=['lbryum_wallet_dir'], metavar='DIR'
)
wallets = Strings(
"Wallet files in 'wallet_dir' to load at startup.",
['default_wallet']
)
# network
use_upnp = Toggle(
"Use UPnP to setup temporary port redirects for the DHT and the hosting of blobs. If you manually forward"
"ports or have firewall rules you likely want to disable this.", True
)
udp_port = Integer("UDP port for communicating on the LBRY DHT", 4444, previous_names=['dht_node_port'])
tcp_port = Integer("TCP port to listen for incoming blob requests", 4444, previous_names=['peer_port'])
prometheus_port = Integer("Port to expose prometheus metrics (off by default)", 0)
network_interface = String("Interface to use for the DHT and blob exchange", '0.0.0.0')
# routing table
split_buckets_under_index = Integer(
"Routing table bucket index below which we always split the bucket if given a new key to add to it and "
"the bucket is full. As this value is raised the depth of the routing table (and number of peers in it) "
"will increase. This setting is used by seed nodes, you probably don't want to change it during normal "
"use.", 2
)
is_bootstrap_node = Toggle(
"When running as a bootstrap node, disable all logic related to balancing the routing table, so we can "
"add as many peers as possible and better help first-runs.", False
)
# protocol timeouts
download_timeout = Float("Cumulative timeout for a stream to begin downloading before giving up", 30.0)
blob_download_timeout = Float("Timeout to download a blob from a peer", 30.0)
hub_timeout = Float("Timeout when making a hub request", 30.0)
peer_connect_timeout = Float("Timeout to establish a TCP connection to a peer", 3.0)
node_rpc_timeout = Float("Timeout when making a DHT request", constants.RPC_TIMEOUT)
# blob announcement and download
save_blobs = Toggle("Save encrypted blob files for hosting, otherwise download blobs to memory only.", True)
network_storage_limit = Integer("Disk space in MB to be allocated for helping the P2P network. 0 = disable", 0)
blob_storage_limit = Integer("Disk space in MB to be allocated for blob storage. 0 = no limit", 0)
blob_lru_cache_size = Integer(
"LRU cache size for decrypted downloaded blobs used to minimize re-downloading the same blobs when "
"replying to a range request. Set to 0 to disable.", 32
)
announce_head_and_sd_only = Toggle(
"Announce only the descriptor and first (rather than all) data blob for a stream to the DHT", True,
previous_names=['announce_head_blobs_only']
)
concurrent_blob_announcers = Integer(
"Number of blobs to iteratively announce at once, set to 0 to disable", 10,
previous_names=['concurrent_announcers']
)
max_connections_per_download = Integer(
"Maximum number of peers to connect to while downloading a blob", 4,
previous_names=['max_connections_per_stream']
)
concurrent_hub_requests = Integer("Maximum number of concurrent hub requests", 32)
fixed_peer_delay = Float(
"Amount of seconds before adding the reflector servers as potential peers to download from in case dht"
"peers are not found or are slow", 2.0
)
max_key_fee = MaxKeyFee(
"Don't download streams with fees exceeding this amount. When set to "
"null, the amount is unbounded.", {'currency': 'USD', 'amount': 50.0}
)
max_wallet_server_fee = String("Maximum daily LBC amount allowed as payment for wallet servers.", "0.0")
# reflector settings
reflect_streams = Toggle(
"Upload completed streams (published and downloaded) reflector in order to re-host them", True,
previous_names=['reflect_uploads']
)
concurrent_reflector_uploads = Integer(
"Maximum number of streams to upload to a reflector server at a time", 10
)
# servers
reflector_servers = Servers("Reflector re-hosting servers for mirroring publishes", [
('reflector.lbry.com', 5566)
])
fixed_peers = Servers("Fixed peers to fall back to if none are found on P2P for a blob", [
('cdn.reflector.lbry.com', 5567)
])
tracker_servers = Servers("BitTorrent-compatible (BEP15) UDP trackers for helping P2P discovery", [
('tracker.lbry.com', 9252),
('tracker.lbry.grin.io', 9252),
('tracker.lbry.pigg.es', 9252),
('tracker.lizard.technology', 9252),
('s1.lbry.network', 9252),
])
lbryum_servers = Servers("SPV wallet servers", [
('spv11.lbry.com', 50001),
('spv12.lbry.com', 50001),
('spv13.lbry.com', 50001),
('spv14.lbry.com', 50001),
('spv15.lbry.com', 50001),
('spv16.lbry.com', 50001),
('spv17.lbry.com', 50001),
('spv18.lbry.com', 50001),
('spv19.lbry.com', 50001),
('hub.lbry.grin.io', 50001),
('hub.lizard.technology', 50001),
('s1.lbry.network', 50001),
])
known_dht_nodes = Servers("Known nodes for bootstrapping connection to the DHT", [
('dht.lbry.grin.io', 4444), # Grin
('dht.lbry.madiator.com', 4444), # Madiator
('dht.lbry.pigg.es', 4444), # Pigges
('lbrynet1.lbry.com', 4444), # US EAST
('lbrynet2.lbry.com', 4444), # US WEST
('lbrynet3.lbry.com', 4444), # EU
('lbrynet4.lbry.com', 4444), # ASIA
('dht.lizard.technology', 4444), # Jack
('s2.lbry.network', 4444),
])
# blockchain
blockchain_name = String("Blockchain name - lbrycrd_main, lbrycrd_regtest, or lbrycrd_testnet", 'lbrycrd_main')
# daemon
save_files = Toggle("Save downloaded files when calling `get` by default", False)
components_to_skip = Strings("components which will be skipped during start-up of daemon", [])
share_usage_data = Toggle(
"Whether to share usage stats and diagnostic info with LBRY.", False,
previous_names=['upload_log', 'upload_log', 'share_debug_info']
)
track_bandwidth = Toggle("Track bandwidth usage", True)
allowed_origin = String(
"Allowed `Origin` header value for API request (sent by browser), use * to allow "
"all hosts; default is to only allow API requests with no `Origin` value.", "")
# media server
streaming_server = String('Host name and port to serve streaming media over range requests',
'localhost:5280', metavar='HOST:PORT')
streaming_get = Toggle("Enable the /get endpoint for the streaming media server. "
"Disable to prevent new streams from being added.", True)
coin_selection_strategy = StringChoice(
"Strategy to use when selecting UTXOs for a transaction",
STRATEGIES, "prefer_confirmed"
)
transaction_cache_size = Integer("Transaction cache size", 2 ** 17)
save_resolved_claims = Toggle(
"Save content claims to the database when they are resolved to keep file_list up to date, "
"only disable this if file_x commands are not needed", True
)
@property
def streaming_host(self):
return self.streaming_server.split(':')[0]
@property
def streaming_port(self):
return int(self.streaming_server.split(':')[1])
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_default_paths()
self.known_hubs = KnownHubsList(self)
def set_default_paths(self):
if 'darwin' in sys.platform.lower():
get_directories = get_darwin_directories
elif 'win' in sys.platform.lower():
get_directories = get_windows_directories
elif 'linux' in sys.platform.lower():
get_directories = get_linux_directories
else:
return
cls = type(self)
cls.data_dir.default, cls.wallet_dir.default, cls.download_dir.default = get_directories()
cls.config.default = os.path.join(
self.data_dir, 'daemon_settings.yml'
)
@property
def log_file_path(self):
return os.path.join(self.data_dir, 'lbrynet.log')
def get_windows_directories() -> Tuple[str, str, str]:
from lbry.winpaths import get_path, FOLDERID, UserHandle, \
PathNotFoundException # pylint: disable=import-outside-toplevel
try:
download_dir = get_path(FOLDERID.Downloads, UserHandle.current)
except PathNotFoundException:
download_dir = os.getcwd()
# old
appdata = get_path(FOLDERID.RoamingAppData, UserHandle.current)
data_dir = os.path.join(appdata, 'lbrynet')
lbryum_dir = os.path.join(appdata, 'lbryum')
if os.path.isdir(data_dir) or os.path.isdir(lbryum_dir):
return data_dir, lbryum_dir, download_dir
# new
data_dir = user_data_dir('lbrynet', 'lbry')
lbryum_dir = user_data_dir('lbryum', 'lbry')
return data_dir, lbryum_dir, download_dir
def get_darwin_directories() -> Tuple[str, str, str]:
data_dir = user_data_dir('LBRY')
lbryum_dir = os.path.expanduser('~/.lbryum')
download_dir = os.path.expanduser('~/Downloads')
return data_dir, lbryum_dir, download_dir
def get_linux_directories() -> Tuple[str, str, str]:
try:
with open(os.path.join(user_config_dir(), 'user-dirs.dirs'), 'r') as xdg:
down_dir = re.search(r'XDG_DOWNLOAD_DIR=(.+)', xdg.read())
if down_dir:
down_dir = re.sub(r'\$HOME', os.getenv('HOME') or os.path.expanduser("~/"), down_dir.group(1))
download_dir = re.sub('\"', '', down_dir)
except OSError:
download_dir = os.getenv('XDG_DOWNLOAD_DIR')
if not download_dir:
download_dir = os.path.expanduser('~/Downloads')
# old
data_dir = os.path.expanduser('~/.lbrynet')
lbryum_dir = os.path.expanduser('~/.lbryum')
if os.path.isdir(data_dir) or os.path.isdir(lbryum_dir):
return data_dir, lbryum_dir, download_dir
# new
return user_data_dir('lbry/lbrynet'), user_data_dir('lbry/lbryum'), download_dir
================================================
FILE: lbry/connection_manager.py
================================================
import time
import asyncio
import typing
import collections
import logging
log = logging.getLogger(__name__)
CONNECTED_EVENT = "connected"
DISCONNECTED_EVENT = "disconnected"
TRANSFERRED_EVENT = "transferred"
class ConnectionManager:
def __init__(self, loop: asyncio.AbstractEventLoop):
self.loop = loop
self.incoming_connected: typing.Set[str] = set()
self.incoming: typing.DefaultDict[str, int] = collections.defaultdict(int)
self.outgoing_connected: typing.Set[str] = set()
self.outgoing: typing.DefaultDict[str, int] = collections.defaultdict(int)
self._max_incoming_mbs = 0.0
self._max_outgoing_mbs = 0.0
self._status = {}
self._running = False
self._task: typing.Optional[asyncio.Task] = None
@property
def status(self):
return self._status
def sent_data(self, host_and_port: str, size: int):
if self._running:
self.outgoing[host_and_port] += size
def received_data(self, host_and_port: str, size: int):
if self._running:
self.incoming[host_and_port] += size
def connection_made(self, host_and_port: str):
if self._running:
self.outgoing_connected.add(host_and_port)
def connection_received(self, host_and_port: str):
# self.incoming_connected.add(host_and_port)
pass
def outgoing_connection_lost(self, host_and_port: str):
if self._running and host_and_port in self.outgoing_connected:
self.outgoing_connected.remove(host_and_port)
def incoming_connection_lost(self, host_and_port: str):
if self._running and host_and_port in self.incoming_connected:
self.incoming_connected.remove(host_and_port)
async def _update(self):
self._status = {
'incoming_bps': {},
'outgoing_bps': {},
'total_incoming_mbs': 0.0,
'total_outgoing_mbs': 0.0,
'total_sent': 0,
'total_received': 0,
'max_incoming_mbs': 0.0,
'max_outgoing_mbs': 0.0
}
while True:
last = time.perf_counter()
await asyncio.sleep(0.1)
self._status['incoming_bps'].clear()
self._status['outgoing_bps'].clear()
now = time.perf_counter()
while self.outgoing:
k, sent = self.outgoing.popitem()
self._status['total_sent'] += sent
self._status['outgoing_bps'][k] = sent / (now - last)
while self.incoming:
k, received = self.incoming.popitem()
self._status['total_received'] += received
self._status['incoming_bps'][k] = received / (now - last)
self._status['total_outgoing_mbs'] = int(sum(list(self._status['outgoing_bps'].values())
)) / 1000000.0
self._status['total_incoming_mbs'] = int(sum(list(self._status['incoming_bps'].values())
)) / 1000000.0
self._max_incoming_mbs = max(self._max_incoming_mbs, self._status['total_incoming_mbs'])
self._max_outgoing_mbs = max(self._max_outgoing_mbs, self._status['total_outgoing_mbs'])
self._status['max_incoming_mbs'] = self._max_incoming_mbs
self._status['max_outgoing_mbs'] = self._max_outgoing_mbs
def stop(self):
if self._task:
self._task.cancel()
self._task = None
self.outgoing.clear()
self.outgoing_connected.clear()
self.incoming.clear()
self.incoming_connected.clear()
self._status.clear()
self._running = False
def start(self):
self.stop()
self._running = True
self._task = self.loop.create_task(self._update())
================================================
FILE: lbry/constants.py
================================================
CENT = 1000000
COIN = 100*CENT
================================================
FILE: lbry/crypto/__init__.py
================================================
================================================
FILE: lbry/crypto/base58.py
================================================
from lbry.crypto.hash import double_sha256
from lbry.crypto.util import bytes_to_int, int_to_bytes
class Base58Error(Exception):
""" Exception used for Base58 errors. """
class Base58:
""" Class providing base 58 functionality. """
chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
assert len(chars) == 58
char_map = {c: n for n, c in enumerate(chars)}
@classmethod
def char_value(cls, c):
val = cls.char_map.get(c)
if val is None:
raise Base58Error(f'invalid base 58 character "{c}"')
return val
@classmethod
def decode(cls, txt):
""" Decodes txt into a big-endian bytearray. """
if isinstance(txt, memoryview):
txt = str(txt)
if isinstance(txt, bytes):
txt = txt.decode()
if not isinstance(txt, str):
raise TypeError('a string is required')
if not txt:
raise Base58Error('string cannot be empty')
value = 0
for c in txt:
value = value * 58 + cls.char_value(c)
result = int_to_bytes(value)
# Prepend leading zero bytes if necessary
count = 0
for c in txt:
if c != '1':
break
count += 1
if count:
result = bytes((0,)) * count + result
return result
@classmethod
def encode(cls, be_bytes):
"""Converts a big-endian bytearray into a base58 string."""
value = bytes_to_int(be_bytes)
txt = ''
while value:
value, mod = divmod(value, 58)
txt += cls.chars[mod]
for byte in be_bytes:
if byte != 0:
break
txt += '1'
return txt[::-1]
@classmethod
def decode_check(cls, txt, hash_fn=double_sha256):
""" Decodes a Base58Check-encoded string to a payload. The version prefixes it. """
be_bytes = cls.decode(txt)
result, check = be_bytes[:-4], be_bytes[-4:]
if check != hash_fn(result)[:4]:
raise Base58Error(f'invalid base 58 checksum for {txt}')
return result
@classmethod
def encode_check(cls, payload, hash_fn=double_sha256):
""" Encodes a payload bytearray (which includes the version byte(s))
into a Base58Check string."""
be_bytes = payload + hash_fn(payload)[:4]
return cls.encode(be_bytes)
================================================
FILE: lbry/crypto/crypt.py
================================================
import os
import base64
import typing
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.primitives.ciphers import Cipher, modes
from cryptography.hazmat.primitives.ciphers.algorithms import AES
from cryptography.hazmat.primitives.padding import PKCS7
from cryptography.hazmat.backends import default_backend
from lbry.error import InvalidPasswordError
from lbry.crypto.hash import double_sha256
def aes_encrypt(secret: str, value: str, init_vector: bytes = None) -> str:
if init_vector is not None:
assert len(init_vector) == 16
else:
init_vector = os.urandom(16)
key = double_sha256(secret.encode())
encryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).encryptor()
padder = PKCS7(AES.block_size).padder()
padded_data = padder.update(value.encode()) + padder.finalize()
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(init_vector + encrypted_data).decode()
def aes_decrypt(secret: str, value: str) -> typing.Tuple[str, bytes]:
try:
data = base64.b64decode(value.encode())
key = double_sha256(secret.encode())
init_vector, data = data[:16], data[16:]
decryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).decryptor()
unpadder = PKCS7(AES.block_size).unpadder()
result = unpadder.update(decryptor.update(data)) + unpadder.finalize()
return result.decode(), init_vector
except UnicodeDecodeError:
raise InvalidPasswordError()
except ValueError as e:
if e.args[0] == 'Invalid padding bytes.':
raise InvalidPasswordError()
raise
def better_aes_encrypt(secret: str, value: bytes) -> bytes:
init_vector = os.urandom(16)
key = scrypt(secret.encode(), salt=init_vector)
encryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).encryptor()
padder = PKCS7(AES.block_size).padder()
padded_data = padder.update(value) + padder.finalize()
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(b's:8192:16:1:' + init_vector + encrypted_data)
def better_aes_decrypt(secret: str, value: bytes) -> bytes:
try:
data = base64.b64decode(value)
_, scryp_n, scrypt_r, scrypt_p, data = data.split(b':', maxsplit=4)
init_vector, data = data[:16], data[16:]
key = scrypt(secret.encode(), init_vector, int(scryp_n), int(scrypt_r), int(scrypt_p))
decryptor = Cipher(AES(key), modes.CBC(init_vector), default_backend()).decryptor()
unpadder = PKCS7(AES.block_size).unpadder()
return unpadder.update(decryptor.update(data)) + unpadder.finalize()
except ValueError as e:
if e.args[0] == 'Invalid padding bytes.':
raise InvalidPasswordError()
raise
def scrypt(passphrase, salt, scrypt_n=1<<13, scrypt_r=16, scrypt_p=1):
kdf = Scrypt(salt, length=32, n=scrypt_n, r=scrypt_r, p=scrypt_p, backend=default_backend())
return kdf.derive(passphrase)
================================================
FILE: lbry/crypto/hash.py
================================================
import hashlib
import hmac
from binascii import hexlify, unhexlify
def sha256(x):
""" Simple wrapper of hashlib sha256. """
return hashlib.sha256(x).digest()
def sha512(x):
""" Simple wrapper of hashlib sha512. """
return hashlib.sha512(x).digest()
def ripemd160(x):
""" Simple wrapper of hashlib ripemd160. """
h = hashlib.new('ripemd160')
h.update(x)
return h.digest()
def double_sha256(x):
""" SHA-256 of SHA-256, as used extensively in bitcoin. """
return sha256(sha256(x))
def hmac_sha512(key, msg):
""" Use SHA-512 to provide an HMAC. """
return hmac.new(key, msg, hashlib.sha512).digest()
def hash160(x):
""" RIPEMD-160 of SHA-256.
Used to make bitcoin addresses from pubkeys. """
return ripemd160(sha256(x))
def hash_to_hex_str(x):
""" Convert a big-endian binary hash to displayed hex string.
Display form of a binary hash is reversed and converted to hex. """
return hexlify(reversed(x))
def hex_str_to_hash(x):
""" Convert a displayed hex string to a binary hash. """
return reversed(unhexlify(x))
================================================
FILE: lbry/crypto/util.py
================================================
from binascii import unhexlify, hexlify
def bytes_to_int(be_bytes):
""" Interprets a big-endian sequence of bytes as an integer. """
return int(hexlify(be_bytes), 16)
def int_to_bytes(value):
""" Converts an integer to a big-endian sequence of bytes. """
length = (value.bit_length() + 7) // 8
s = '%x' % value
return unhexlify(('0' * (len(s) % 2) + s).zfill(length * 2))
================================================
FILE: lbry/dht/__init__.py
================================================
================================================
FILE: lbry/dht/blob_announcer.py
================================================
import asyncio
import typing
import logging
from prometheus_client import Counter, Gauge
if typing.TYPE_CHECKING:
from lbry.dht.node import Node
from lbry.extras.daemon.storage import SQLiteStorage
log = logging.getLogger(__name__)
class BlobAnnouncer:
announcements_sent_metric = Counter(
"announcements_sent", "Number of announcements sent and their respective status.", namespace="dht_node",
labelnames=("peers", "error"),
)
announcement_queue_size_metric = Gauge(
"announcement_queue_size", "Number of hashes waiting to be announced.", namespace="dht_node",
labelnames=("scope",)
)
def __init__(self, loop: asyncio.AbstractEventLoop, node: 'Node', storage: 'SQLiteStorage'):
self.loop = loop
self.node = node
self.storage = storage
self.announce_task: asyncio.Task = None
self.announce_queue: typing.List[str] = []
self._done = asyncio.Event()
self.announced = set()
async def _run_consumer(self):
while self.announce_queue:
try:
blob_hash = self.announce_queue.pop()
peers = len(await self.node.announce_blob(blob_hash))
self.announcements_sent_metric.labels(peers=peers, error=False).inc()
if peers > 4:
self.announced.add(blob_hash)
else:
log.debug("failed to announce %s, could only find %d peers, retrying soon.", blob_hash[:8], peers)
except Exception as err:
self.announcements_sent_metric.labels(peers=0, error=True).inc()
log.warning("error announcing %s: %s", blob_hash[:8], str(err))
async def _announce(self, batch_size: typing.Optional[int] = 10):
while batch_size:
if not self.node.joined.is_set():
await self.node.joined.wait()
await asyncio.sleep(60)
if not self.node.protocol.routing_table.get_peers():
log.warning("No peers in DHT, announce round skipped")
continue
self.announce_queue.extend(await self.storage.get_blobs_to_announce())
self.announcement_queue_size_metric.labels(scope="global").set(len(self.announce_queue))
log.debug("announcer task wake up, %d blobs to announce", len(self.announce_queue))
while len(self.announce_queue) > 0:
log.info("%i blobs to announce", len(self.announce_queue))
await asyncio.gather(*[self._run_consumer() for _ in range(batch_size)])
announced = list(filter(None, self.announced))
if announced:
await self.storage.update_last_announced_blobs(announced)
log.info("announced %i blobs", len(announced))
self.announced.clear()
self._done.set()
self._done.clear()
def start(self, batch_size: typing.Optional[int] = 10):
assert not self.announce_task or self.announce_task.done(), "already running"
self.announce_task = self.loop.create_task(self._announce(batch_size))
def stop(self):
if self.announce_task and not self.announce_task.done():
self.announce_task.cancel()
def wait(self):
return self._done.wait()
================================================
FILE: lbry/dht/constants.py
================================================
import hashlib
import os
HASH_CLASS = hashlib.sha384 # pylint: disable=invalid-name
HASH_LENGTH = HASH_CLASS().digest_size
HASH_BITS = HASH_LENGTH * 8
ALPHA = 5
K = 8
SPLIT_BUCKETS_UNDER_INDEX = 1
REPLACEMENT_CACHE_SIZE = 8
RPC_TIMEOUT = 5.0
RPC_ATTEMPTS = 5
RPC_ATTEMPTS_PRUNING_WINDOW = 600
ITERATIVE_LOOKUP_DELAY = RPC_TIMEOUT / 2.0 # TODO: use config val / 2 if rpc timeout is provided
REFRESH_INTERVAL = 3600 # 1 hour
REPLICATE_INTERVAL = REFRESH_INTERVAL
DATA_EXPIRATION = 86400 # 24 hours
TOKEN_SECRET_REFRESH_INTERVAL = 300 # 5 minutes
MAYBE_PING_DELAY = 300 # 5 minutes
CHECK_REFRESH_INTERVAL = REFRESH_INTERVAL / 5
RPC_ID_LENGTH = 20
PROTOCOL_VERSION = 1
MSG_SIZE_LIMIT = 1400
def digest(data: bytes) -> bytes:
h = HASH_CLASS()
h.update(data)
return h.digest()
def generate_id(num=None) -> bytes:
if num is not None:
return digest(str(num).encode())
else:
return digest(os.urandom(32))
def generate_rpc_id(num=None) -> bytes:
return generate_id(num)[:RPC_ID_LENGTH]
================================================
FILE: lbry/dht/error.py
================================================
class BaseKademliaException(Exception):
pass
class DecodeError(BaseKademliaException):
"""
Should be raised by an C{Encoding} implementation if decode operation
fails
"""
class BucketFull(BaseKademliaException):
"""
Raised when the bucket is full
"""
class RemoteException(BaseKademliaException):
pass
class TransportNotConnected(BaseKademliaException):
pass
================================================
FILE: lbry/dht/node.py
================================================
import logging
import asyncio
import typing
import socket
from prometheus_client import Gauge
from lbry.utils import aclosing, resolve_host
from lbry.dht import constants
from lbry.dht.peer import make_kademlia_peer
from lbry.dht.protocol.distance import Distance
from lbry.dht.protocol.iterative_find import IterativeNodeFinder, IterativeValueFinder
from lbry.dht.protocol.protocol import KademliaProtocol
if typing.TYPE_CHECKING:
from lbry.dht.peer import PeerManager
from lbry.dht.peer import KademliaPeer
log = logging.getLogger(__name__)
class Node:
storing_peers_metric = Gauge(
"storing_peers", "Number of peers storing blobs announced to this node", namespace="dht_node",
labelnames=("scope",),
)
stored_blob_with_x_bytes_colliding = Gauge(
"stored_blobs_x_bytes_colliding", "Number of blobs with at least X bytes colliding with this node id prefix",
namespace="dht_node", labelnames=("amount",)
)
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', node_id: bytes, udp_port: int,
internal_udp_port: int, peer_port: int, external_ip: str, rpc_timeout: float = constants.RPC_TIMEOUT,
split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_bootstrap_node: bool = False,
storage: typing.Optional['SQLiteStorage'] = None):
self.loop = loop
self.internal_udp_port = internal_udp_port
self.protocol = KademliaProtocol(loop, peer_manager, node_id, external_ip, udp_port, peer_port, rpc_timeout,
split_buckets_under_index, is_bootstrap_node)
self.listening_port: asyncio.DatagramTransport = None
self.joined = asyncio.Event()
self._join_task: asyncio.Task = None
self._refresh_task: asyncio.Task = None
self._storage = storage
@property
def stored_blob_hashes(self):
return self.protocol.data_store.keys()
async def refresh_node(self, force_once=False):
while True:
# remove peers with expired blob announcements from the datastore
self.protocol.data_store.removed_expired_peers()
total_peers: typing.List['KademliaPeer'] = []
# add all peers in the routing table
total_peers.extend(self.protocol.routing_table.get_peers())
# add all the peers who have announced blobs to us
storing_peers = self.protocol.data_store.get_storing_contacts()
self.storing_peers_metric.labels("global").set(len(storing_peers))
total_peers.extend(storing_peers)
counts = {0: 0, 1: 0, 2: 0}
node_id = self.protocol.node_id
for blob_hash in self.protocol.data_store.keys():
bytes_colliding = 0 if blob_hash[0] != node_id[0] else 2 if blob_hash[1] == node_id[1] else 1
counts[bytes_colliding] += 1
self.stored_blob_with_x_bytes_colliding.labels(amount=0).set(counts[0])
self.stored_blob_with_x_bytes_colliding.labels(amount=1).set(counts[1])
self.stored_blob_with_x_bytes_colliding.labels(amount=2).set(counts[2])
# get ids falling in the midpoint of each bucket that hasn't been recently updated
node_ids = self.protocol.routing_table.get_refresh_list(0, True)
if self.protocol.routing_table.get_peers():
# if we have node ids to look up, perform the iterative search until we have k results
while node_ids:
peers = await self.peer_search(node_ids.pop())
total_peers.extend(peers)
else:
if force_once:
break
fut = asyncio.Future()
self.loop.call_later(constants.REFRESH_INTERVAL // 4, fut.set_result, None)
await fut
continue
# ping the set of peers; upon success/failure the routing able and last replied/failed time will be updated
to_ping = [peer for peer in set(total_peers) if self.protocol.peer_manager.peer_is_good(peer) is not True]
if to_ping:
self.protocol.ping_queue.enqueue_maybe_ping(*to_ping, delay=0)
if self._storage:
await self._storage.save_kademlia_peers(self.protocol.routing_table.get_peers())
if force_once:
break
fut = asyncio.Future()
self.loop.call_later(constants.REFRESH_INTERVAL, fut.set_result, None)
await fut
async def announce_blob(self, blob_hash: str) -> typing.List[bytes]:
hash_value = bytes.fromhex(blob_hash)
assert len(hash_value) == constants.HASH_LENGTH
peers = await self.peer_search(hash_value)
if not self.protocol.external_ip:
raise Exception("Cannot determine external IP")
log.debug("Store to %i peers", len(peers))
for peer in peers:
log.debug("store to %s %s %s", peer.address, peer.udp_port, peer.tcp_port)
stored_to_tup = await asyncio.gather(
*(self.protocol.store_to_peer(hash_value, peer) for peer in peers)
)
stored_to = [node_id for node_id, contacted in stored_to_tup if contacted]
if stored_to:
log.debug(
"Stored %s to %i of %i attempted peers", hash_value.hex()[:8],
len(stored_to), len(peers)
)
else:
log.debug("Failed announcing %s, stored to 0 peers", blob_hash[:8])
return stored_to
def stop(self) -> None:
if self.joined.is_set():
self.joined.clear()
if self._join_task:
self._join_task.cancel()
if self._refresh_task and not (self._refresh_task.done() or self._refresh_task.cancelled()):
self._refresh_task.cancel()
if self.protocol and self.protocol.ping_queue.running:
self.protocol.ping_queue.stop()
self.protocol.stop()
if self.listening_port is not None:
self.listening_port.close()
self._join_task = None
self.listening_port = None
log.info("Stopped DHT node")
async def start_listening(self, interface: str = '0.0.0.0') -> None:
if not self.listening_port:
self.listening_port, _ = await self.loop.create_datagram_endpoint(
lambda: self.protocol, (interface, self.internal_udp_port)
)
log.info("DHT node listening on UDP %s:%i", interface, self.internal_udp_port)
self.protocol.start()
else:
log.warning("Already bound to port %s", self.listening_port)
async def join_network(self, interface: str = '0.0.0.0',
known_node_urls: typing.Optional[typing.List[typing.Tuple[str, int]]] = None):
def peers_from_urls(urls: typing.Optional[typing.List[typing.Tuple[bytes, str, int, int]]]):
peer_addresses = []
for node_id, address, udp_port, tcp_port in urls:
if (node_id, address, udp_port, tcp_port) not in peer_addresses and \
(address, udp_port) != (self.protocol.external_ip, self.protocol.udp_port):
peer_addresses.append((node_id, address, udp_port, tcp_port))
return [make_kademlia_peer(*peer_address) for peer_address in peer_addresses]
if not self.listening_port:
await self.start_listening(interface)
self.protocol.ping_queue.start()
self._refresh_task = self.loop.create_task(self.refresh_node())
while True:
if self.protocol.routing_table.get_peers():
if not self.joined.is_set():
self.joined.set()
log.info(
"joined dht, %i peers known in %i buckets", len(self.protocol.routing_table.get_peers()),
self.protocol.routing_table.buckets_with_contacts()
)
else:
if self.joined.is_set():
self.joined.clear()
seed_peers = peers_from_urls(
await self._storage.get_persisted_kademlia_peers()
) if self._storage else []
if not seed_peers:
try:
seed_peers.extend(peers_from_urls([
(None, await resolve_host(address, udp_port, 'udp'), udp_port, None)
for address, udp_port in known_node_urls or []
]))
except socket.gaierror:
await asyncio.sleep(30)
continue
self.protocol.peer_manager.reset()
self.protocol.ping_queue.enqueue_maybe_ping(*seed_peers, delay=0.0)
await self.peer_search(self.protocol.node_id, shortlist=seed_peers, count=32)
await asyncio.sleep(1)
def start(self, interface: str, known_node_urls: typing.Optional[typing.List[typing.Tuple[str, int]]] = None):
self._join_task = self.loop.create_task(self.join_network(interface, known_node_urls))
def get_iterative_node_finder(self, key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']] = None,
max_results: int = constants.K) -> IterativeNodeFinder:
shortlist = shortlist or self.protocol.routing_table.find_close_peers(key)
return IterativeNodeFinder(self.loop, self.protocol, key, max_results, shortlist)
def get_iterative_value_finder(self, key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']] = None,
max_results: int = -1) -> IterativeValueFinder:
shortlist = shortlist or self.protocol.routing_table.find_close_peers(key)
return IterativeValueFinder(self.loop, self.protocol, key, max_results, shortlist)
async def peer_search(self, node_id: bytes, count=constants.K, max_results=constants.K * 2,
shortlist: typing.Optional[typing.List['KademliaPeer']] = None
) -> typing.List['KademliaPeer']:
peers = []
async with aclosing(self.get_iterative_node_finder(
node_id, shortlist=shortlist, max_results=max_results)) as node_finder:
async for iteration_peers in node_finder:
peers.extend(iteration_peers)
distance = Distance(node_id)
peers.sort(key=lambda peer: distance(peer.node_id))
return peers[:count]
async def _accumulate_peers_for_value(self, search_queue: asyncio.Queue, result_queue: asyncio.Queue):
tasks = []
try:
while True:
blob_hash = await search_queue.get()
tasks.append(self.loop.create_task(self._peers_for_value_producer(blob_hash, result_queue)))
finally:
for task in tasks:
task.cancel()
async def _peers_for_value_producer(self, blob_hash: str, result_queue: asyncio.Queue):
async def put_into_result_queue_after_pong(_peer):
try:
await self.protocol.get_rpc_peer(_peer).ping()
result_queue.put_nowait([_peer])
log.debug("pong from %s:%i for %s", _peer.address, _peer.udp_port, blob_hash)
except asyncio.TimeoutError:
pass
# prioritize peers who reply to a dht ping first
# this minimizes attempting to make tcp connections that won't work later to dead or unreachable peers
async with aclosing(self.get_iterative_value_finder(bytes.fromhex(blob_hash))) as value_finder:
async for results in value_finder:
to_put = []
for peer in results:
if peer.address == self.protocol.external_ip and self.protocol.peer_port == peer.tcp_port:
continue
is_good = self.protocol.peer_manager.peer_is_good(peer)
if is_good:
# the peer has replied recently over UDP, it can probably be reached on the TCP port
to_put.append(peer)
elif is_good is None:
if not peer.udp_port:
# TODO: use the same port for TCP and UDP
# the udp port must be guessed
# default to the ports being the same. if the TCP port appears to be <=0.48.0 default,
# including on a network with several nodes, then assume the udp port is proportionately
# based on a starting port of 4444
udp_port_to_try = peer.tcp_port
if 3400 > peer.tcp_port > 3332:
udp_port_to_try = (peer.tcp_port - 3333) + 4444
self.loop.create_task(put_into_result_queue_after_pong(
make_kademlia_peer(peer.node_id, peer.address, udp_port_to_try, peer.tcp_port)
))
else:
self.loop.create_task(put_into_result_queue_after_pong(peer))
else:
# the peer is known to be bad/unreachable, skip trying to connect to it over TCP
log.debug("skip bad peer %s:%i for %s", peer.address, peer.tcp_port, blob_hash)
if to_put:
result_queue.put_nowait(to_put)
def accumulate_peers(self, search_queue: asyncio.Queue,
peer_queue: typing.Optional[asyncio.Queue] = None
) -> typing.Tuple[asyncio.Queue, asyncio.Task]:
queue = peer_queue or asyncio.Queue()
return queue, self.loop.create_task(self._accumulate_peers_for_value(search_queue, queue))
async def get_kademlia_peers_from_hosts(peer_list: typing.List[typing.Tuple[str, int]]) -> typing.List['KademliaPeer']:
peer_address_list = [(await resolve_host(url, port, proto='tcp'), port) for url, port in peer_list]
kademlia_peer_list = [make_kademlia_peer(None, address, None, tcp_port=port, allow_localhost=True)
for address, port in peer_address_list]
return kademlia_peer_list
================================================
FILE: lbry/dht/peer.py
================================================
import typing
import asyncio
import logging
from dataclasses import dataclass, field
from functools import lru_cache
from prometheus_client import Gauge
from lbry.utils import is_valid_public_ipv4 as _is_valid_public_ipv4, LRUCache
from lbry.dht import constants
from lbry.dht.serialization.datagram import make_compact_address, make_compact_ip, decode_compact_address
ALLOW_LOCALHOST = False
CACHE_SIZE = 16384
log = logging.getLogger(__name__)
@lru_cache(CACHE_SIZE)
def make_kademlia_peer(node_id: typing.Optional[bytes], address: typing.Optional[str],
udp_port: typing.Optional[int] = None,
tcp_port: typing.Optional[int] = None,
allow_localhost: bool = False) -> 'KademliaPeer':
return KademliaPeer(address, node_id, udp_port, tcp_port=tcp_port, allow_localhost=allow_localhost)
def is_valid_public_ipv4(address, allow_localhost: bool = False):
allow_localhost = bool(allow_localhost or ALLOW_LOCALHOST)
return _is_valid_public_ipv4(address, allow_localhost)
class PeerManager:
peer_manager_keys_metric = Gauge(
"peer_manager_keys", "Number of keys tracked by PeerManager dicts (sum)", namespace="dht_node",
labelnames=("scope",)
)
def __init__(self, loop: asyncio.AbstractEventLoop):
self._loop = loop
self._rpc_failures: typing.Dict[
typing.Tuple[str, int], typing.Tuple[typing.Optional[float], typing.Optional[float]]
] = LRUCache(CACHE_SIZE)
self._last_replied: typing.Dict[typing.Tuple[str, int], float] = LRUCache(CACHE_SIZE)
self._last_sent: typing.Dict[typing.Tuple[str, int], float] = LRUCache(CACHE_SIZE)
self._last_requested: typing.Dict[typing.Tuple[str, int], float] = LRUCache(CACHE_SIZE)
self._node_id_mapping: typing.Dict[typing.Tuple[str, int], bytes] = LRUCache(CACHE_SIZE)
self._node_id_reverse_mapping: typing.Dict[bytes, typing.Tuple[str, int]] = LRUCache(CACHE_SIZE)
self._node_tokens: typing.Dict[bytes, (float, bytes)] = LRUCache(CACHE_SIZE)
def count_cache_keys(self):
return len(self._rpc_failures) + len(self._last_replied) + len(self._last_sent) + len(
self._last_requested) + len(self._node_id_mapping) + len(self._node_id_reverse_mapping) + len(
self._node_tokens)
def reset(self):
for statistic in (self._rpc_failures, self._last_replied, self._last_sent, self._last_requested):
statistic.clear()
def report_failure(self, address: str, udp_port: int):
now = self._loop.time()
_, previous = self._rpc_failures.pop((address, udp_port), (None, None))
self._rpc_failures[(address, udp_port)] = (previous, now)
def report_last_sent(self, address: str, udp_port: int):
now = self._loop.time()
self._last_sent[(address, udp_port)] = now
def report_last_replied(self, address: str, udp_port: int):
now = self._loop.time()
self._last_replied[(address, udp_port)] = now
def report_last_requested(self, address: str, udp_port: int):
now = self._loop.time()
self._last_requested[(address, udp_port)] = now
def clear_token(self, node_id: bytes):
self._node_tokens.pop(node_id, None)
def update_token(self, node_id: bytes, token: bytes):
now = self._loop.time()
self._node_tokens[node_id] = (now, token)
def get_node_token(self, node_id: bytes) -> typing.Optional[bytes]:
ts, token = self._node_tokens.get(node_id, (0, None))
if ts and ts > self._loop.time() - constants.TOKEN_SECRET_REFRESH_INTERVAL:
return token
def get_last_replied(self, address: str, udp_port: int) -> typing.Optional[float]:
return self._last_replied.get((address, udp_port))
def update_contact_triple(self, node_id: bytes, address: str, udp_port: int):
"""
Update the mapping of node_id -> address tuple and that of address tuple -> node_id
This is to handle peers changing addresses and ids while assuring that the we only ever have
one node id / address tuple mapped to each other
"""
if (address, udp_port) in self._node_id_mapping:
self._node_id_reverse_mapping.pop(self._node_id_mapping.pop((address, udp_port)))
if node_id in self._node_id_reverse_mapping:
self._node_id_mapping.pop(self._node_id_reverse_mapping.pop(node_id))
self._node_id_mapping[(address, udp_port)] = node_id
self._node_id_reverse_mapping[node_id] = (address, udp_port)
self.peer_manager_keys_metric.labels("global").set(self.count_cache_keys())
def get_node_id_for_endpoint(self, address, port):
return self._node_id_mapping.get((address, port))
def prune(self): # TODO: periodically call this
now = self._loop.time()
to_pop = []
for (address, udp_port), (_, last_failure) in self._rpc_failures.items():
if last_failure and last_failure < now - constants.RPC_ATTEMPTS_PRUNING_WINDOW:
to_pop.append((address, udp_port))
while to_pop:
del self._rpc_failures[to_pop.pop()]
to_pop = []
for node_id, (age, token) in self._node_tokens.items(): # pylint: disable=unused-variable
if age < now - constants.TOKEN_SECRET_REFRESH_INTERVAL:
to_pop.append(node_id)
while to_pop:
del self._node_tokens[to_pop.pop()]
def contact_triple_is_good(self, node_id: bytes, address: str, udp_port: int): # pylint: disable=too-many-return-statements
"""
:return: False if peer is bad, None if peer is unknown, or True if peer is good
"""
delay = self._loop.time() - constants.CHECK_REFRESH_INTERVAL
# fixme: find a way to re-enable that without breaking other parts
# if node_id not in self._node_id_reverse_mapping or (address, udp_port) not in self._node_id_mapping:
# return
# addr_tup = (address, udp_port)
# if self._node_id_reverse_mapping[node_id] != addr_tup or self._node_id_mapping[addr_tup] != node_id:
# return
previous_failure, most_recent_failure = self._rpc_failures.get((address, udp_port), (None, None))
last_requested = self._last_requested.get((address, udp_port))
last_replied = self._last_replied.get((address, udp_port))
if node_id is None:
return None
if most_recent_failure and last_replied:
if delay < last_replied > most_recent_failure:
return True
elif last_replied > most_recent_failure:
return
return False
elif previous_failure and most_recent_failure and most_recent_failure > delay:
return False
elif last_replied and last_replied > delay:
return True
elif last_requested and last_requested > delay:
return None
return
def peer_is_good(self, peer: 'KademliaPeer'):
return self.contact_triple_is_good(peer.node_id, peer.address, peer.udp_port)
def decode_tcp_peer_from_compact_address(compact_address: bytes) -> 'KademliaPeer': # pylint: disable=no-self-use
node_id, address, tcp_port = decode_compact_address(compact_address)
return make_kademlia_peer(node_id, address, udp_port=None, tcp_port=tcp_port)
@dataclass(unsafe_hash=True)
class KademliaPeer:
address: str = field(hash=True)
_node_id: typing.Optional[bytes] = field(hash=True)
udp_port: typing.Optional[int] = field(hash=True)
tcp_port: typing.Optional[int] = field(compare=False, hash=False)
protocol_version: typing.Optional[int] = field(default=1, compare=False, hash=False)
allow_localhost: bool = field(default=False, compare=False, hash=False)
def __post_init__(self):
if self._node_id is not None:
if not len(self._node_id) == constants.HASH_LENGTH:
raise ValueError("invalid node_id: {}".format(self._node_id.hex()))
if self.udp_port is not None and not 1024 <= self.udp_port <= 65535:
raise ValueError(f"invalid udp port: {self.address}:{self.udp_port}")
if self.tcp_port is not None and not 1024 <= self.tcp_port <= 65535:
raise ValueError(f"invalid tcp port: {self.address}:{self.tcp_port}")
if not is_valid_public_ipv4(self.address, self.allow_localhost):
raise ValueError(f"invalid ip address: '{self.address}'")
def update_tcp_port(self, tcp_port: int):
self.tcp_port = tcp_port
@property
def node_id(self) -> bytes:
return self._node_id
def compact_address_udp(self) -> bytearray:
return make_compact_address(self.node_id, self.address, self.udp_port)
def compact_address_tcp(self) -> bytearray:
return make_compact_address(self.node_id, self.address, self.tcp_port)
def compact_ip(self):
return make_compact_ip(self.address)
def __str__(self):
return f"{self.__class__.__name__}({self.node_id.hex()[:8]}@{self.address}:{self.udp_port}-{self.tcp_port})"
================================================
FILE: lbry/dht/protocol/__init__.py
================================================
================================================
FILE: lbry/dht/protocol/data_store.py
================================================
import asyncio
import typing
from lbry.dht import constants
if typing.TYPE_CHECKING:
from lbry.dht.peer import KademliaPeer, PeerManager
class DictDataStore:
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager'):
# Dictionary format:
# { : [(, ), ...] }
self._data_store: typing.Dict[bytes, typing.List[typing.Tuple['KademliaPeer', float]]] = {}
self.loop = loop
self._peer_manager = peer_manager
self.completed_blobs: typing.Set[str] = set()
def keys(self):
return self._data_store.keys()
def __len__(self):
return self._data_store.__len__()
def removed_expired_peers(self):
now = self.loop.time()
keys = list(self._data_store.keys())
for key in keys:
to_remove = []
for (peer, ts) in self._data_store[key]:
if ts + constants.DATA_EXPIRATION < now or self._peer_manager.peer_is_good(peer) is False:
to_remove.append((peer, ts))
for item in to_remove:
self._data_store[key].remove(item)
if not self._data_store[key]:
del self._data_store[key]
def filter_bad_and_expired_peers(self, key: bytes) -> typing.Iterator['KademliaPeer']:
"""
Returns only non-expired and unknown/good peers
"""
for peer in self.filter_expired_peers(key):
if self._peer_manager.peer_is_good(peer) is not False:
yield peer
def filter_expired_peers(self, key: bytes) -> typing.Iterator['KademliaPeer']:
"""
Returns only non-expired peers
"""
now = self.loop.time()
for (peer, ts) in self._data_store.get(key, []):
if ts + constants.DATA_EXPIRATION > now:
yield peer
def has_peers_for_blob(self, key: bytes) -> bool:
return key in self._data_store
def add_peer_to_blob(self, contact: 'KademliaPeer', key: bytes) -> None:
now = self.loop.time()
if key in self._data_store:
current = list(filter(lambda x: x[0] == contact, self._data_store[key]))
if len(current) > 0:
self._data_store[key][self._data_store[key].index(current[0])] = contact, now
else:
self._data_store[key].append((contact, now))
else:
self._data_store[key] = [(contact, now)]
def get_peers_for_blob(self, key: bytes) -> typing.List['KademliaPeer']:
return list(self.filter_bad_and_expired_peers(key))
def get_storing_contacts(self) -> typing.List['KademliaPeer']:
peers = set()
for _, stored in self._data_store.items():
peers.update(set(map(lambda tup: tup[0], stored)))
return list(peers)
================================================
FILE: lbry/dht/protocol/distance.py
================================================
from lbry.dht import constants
class Distance:
"""Calculate the XOR result between two string variables.
Frequently we re-use one of the points so as an optimization
we pre-calculate the value of that point.
"""
def __init__(self, key: bytes):
if len(key) != constants.HASH_LENGTH:
raise ValueError(f"invalid key length: {len(key)}")
self.key = key
self.val_key_one = int.from_bytes(key, 'big')
def __call__(self, key_two: bytes) -> int:
if len(key_two) != constants.HASH_LENGTH:
raise ValueError(f"invalid length of key to compare: {len(key_two)}")
val_key_two = int.from_bytes(key_two, 'big')
return self.val_key_one ^ val_key_two
def is_closer(self, key_a: bytes, key_b: bytes) -> bool:
"""Returns true is `key_a` is closer to `key` than `key_b` is"""
return self(key_a) < self(key_b)
================================================
FILE: lbry/dht/protocol/iterative_find.py
================================================
import asyncio
from itertools import chain
from collections import defaultdict, OrderedDict
from collections.abc import AsyncIterator
import typing
import logging
from typing import TYPE_CHECKING
from lbry.dht import constants
from lbry.dht.error import RemoteException, TransportNotConnected
from lbry.dht.protocol.distance import Distance
from lbry.dht.peer import make_kademlia_peer, decode_tcp_peer_from_compact_address
from lbry.dht.serialization.datagram import PAGE_KEY
if TYPE_CHECKING:
from lbry.dht.protocol.protocol import KademliaProtocol
from lbry.dht.peer import PeerManager, KademliaPeer
log = logging.getLogger(__name__)
class FindResponse:
@property
def found(self) -> bool:
raise NotImplementedError()
def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:
raise NotImplementedError()
def get_close_kademlia_peers(self, peer_info) -> typing.Generator[typing.Iterator['KademliaPeer'], None, None]:
for contact_triple in self.get_close_triples():
node_id, address, udp_port = contact_triple
try:
yield make_kademlia_peer(node_id, address, udp_port)
except ValueError:
log.warning("misbehaving peer %s:%i returned peer with reserved ip %s:%i", peer_info.address,
peer_info.udp_port, address, udp_port)
class FindNodeResponse(FindResponse):
def __init__(self, key: bytes, close_triples: typing.List[typing.Tuple[bytes, str, int]]):
self.key = key
self.close_triples = close_triples
@property
def found(self) -> bool:
return self.key in [triple[0] for triple in self.close_triples]
def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:
return self.close_triples
class FindValueResponse(FindResponse):
def __init__(self, key: bytes, result_dict: typing.Dict):
self.key = key
self.token = result_dict[b'token']
self.close_triples: typing.List[typing.Tuple[bytes, bytes, int]] = result_dict.get(b'contacts', [])
self.found_compact_addresses = result_dict.get(key, [])
self.pages = int(result_dict.get(PAGE_KEY, 0))
@property
def found(self) -> bool:
return len(self.found_compact_addresses) > 0
def get_close_triples(self) -> typing.List[typing.Tuple[bytes, str, int]]:
return [(node_id, address.decode(), port) for node_id, address, port in self.close_triples]
class IterativeFinder(AsyncIterator):
def __init__(self, loop: asyncio.AbstractEventLoop,
protocol: 'KademliaProtocol', key: bytes,
max_results: typing.Optional[int] = constants.K,
shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
if len(key) != constants.HASH_LENGTH:
raise ValueError("invalid key length: %i" % len(key))
self.loop = loop
self.peer_manager = protocol.peer_manager
self.protocol = protocol
self.key = key
self.max_results = max(constants.K, max_results)
self.active: typing.Dict['KademliaPeer', int] = OrderedDict() # peer: distance, sorted
self.contacted: typing.Set['KademliaPeer'] = set()
self.distance = Distance(key)
self.iteration_queue = asyncio.Queue()
self.running_probes: typing.Dict['KademliaPeer', asyncio.Task] = {}
self.iteration_count = 0
self.running = False
self.tasks: typing.List[asyncio.Task] = []
for peer in shortlist:
if peer.node_id:
self._add_active(peer, force=True)
else:
# seed nodes
self._schedule_probe(peer)
async def send_probe(self, peer: 'KademliaPeer') -> FindResponse:
"""
Send the rpc request to the peer and return an object with the FindResponse interface
"""
raise NotImplementedError()
def search_exhausted(self):
"""
This method ends the iterator due no more peers to contact.
Override to provide last time results.
"""
self.iteration_queue.put_nowait(None)
def check_result_ready(self, response: FindResponse):
"""
Called after adding peers from an rpc result to the shortlist.
This method is responsible for putting a result for the generator into the Queue
"""
raise NotImplementedError()
def get_initial_result(self) -> typing.List['KademliaPeer']: #pylint: disable=no-self-use
"""
Get an initial or cached result to be put into the Queue. Used for findValue requests where the blob
has peers in the local data store of blobs announced to us
"""
return []
def _add_active(self, peer, force=False):
if not force and self.peer_manager.peer_is_good(peer) is False:
return
if peer in self.contacted:
return
if peer not in self.active and peer.node_id and peer.node_id != self.protocol.node_id:
self.active[peer] = self.distance(peer.node_id)
self.active = OrderedDict(sorted(self.active.items(), key=lambda item: item[1]))
async def _handle_probe_result(self, peer: 'KademliaPeer', response: FindResponse):
self._add_active(peer)
for new_peer in response.get_close_kademlia_peers(peer):
self._add_active(new_peer)
self.check_result_ready(response)
self._log_state(reason="check result")
def _reset_closest(self, peer):
if peer in self.active:
del self.active[peer]
async def _send_probe(self, peer: 'KademliaPeer'):
try:
response = await self.send_probe(peer)
except asyncio.TimeoutError:
self._reset_closest(peer)
return
except asyncio.CancelledError:
log.debug("%s[%x] cancelled probe",
type(self).__name__, id(self))
raise
except ValueError as err:
log.warning(str(err))
self._reset_closest(peer)
return
except TransportNotConnected:
await self._aclose(reason="not connected")
return
except RemoteException:
self._reset_closest(peer)
return
return await self._handle_probe_result(peer, response)
def _search_round(self):
"""
Send up to constants.alpha (5) probes to closest active peers
"""
added = 0
for index, peer in enumerate(self.active.keys()):
if index == 0:
log.debug("%s[%x] closest to probe: %s",
type(self).__name__, id(self),
peer.node_id.hex()[:8])
if peer in self.contacted:
continue
if len(self.running_probes) >= constants.ALPHA:
break
if index > (constants.K + len(self.running_probes)):
break
origin_address = (peer.address, peer.udp_port)
if peer.node_id == self.protocol.node_id:
continue
if origin_address == (self.protocol.external_ip, self.protocol.udp_port):
continue
self._schedule_probe(peer)
added += 1
log.debug("%s[%x] running %d probes for key %s",
type(self).__name__, id(self),
len(self.running_probes), self.key.hex()[:8])
if not added and not self.running_probes:
log.debug("%s[%x] search for %s exhausted",
type(self).__name__, id(self),
self.key.hex()[:8])
self.search_exhausted()
def _schedule_probe(self, peer: 'KademliaPeer'):
self.contacted.add(peer)
t = self.loop.create_task(self._send_probe(peer))
def callback(_):
self.running_probes.pop(peer, None)
if self.running:
self._search_round()
t.add_done_callback(callback)
self.running_probes[peer] = t
def _log_state(self, reason="?"):
log.debug("%s[%x] [%s] %s: %i active nodes %i contacted %i produced %i queued",
type(self).__name__, id(self), self.key.hex()[:8],
reason, len(self.active), len(self.contacted),
self.iteration_count, self.iteration_queue.qsize())
def __aiter__(self):
if self.running:
raise Exception("already running")
self.running = True
self.loop.call_soon(self._search_round)
return self
async def __anext__(self) -> typing.List['KademliaPeer']:
try:
if self.iteration_count == 0:
result = self.get_initial_result() or await self.iteration_queue.get()
else:
result = await self.iteration_queue.get()
if not result:
raise StopAsyncIteration
self.iteration_count += 1
return result
except asyncio.CancelledError:
await self._aclose(reason="cancelled")
raise
except StopAsyncIteration:
await self._aclose(reason="no more results")
raise
async def _aclose(self, reason="?"):
log.debug("%s[%x] [%s] shutdown because %s: %i active nodes %i contacted %i produced %i queued",
type(self).__name__, id(self), self.key.hex()[:8],
reason, len(self.active), len(self.contacted),
self.iteration_count, self.iteration_queue.qsize())
self.running = False
self.iteration_queue.put_nowait(None)
for task in chain(self.tasks, self.running_probes.values()):
task.cancel()
self.tasks.clear()
self.running_probes.clear()
async def aclose(self):
if self.running:
await self._aclose(reason="aclose")
log.debug("%s[%x] [%s] async close completed",
type(self).__name__, id(self), self.key.hex()[:8])
class IterativeNodeFinder(IterativeFinder):
def __init__(self, loop: asyncio.AbstractEventLoop,
protocol: 'KademliaProtocol', key: bytes,
max_results: typing.Optional[int] = constants.K,
shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
super().__init__(loop, protocol, key, max_results, shortlist)
self.yielded_peers: typing.Set['KademliaPeer'] = set()
async def send_probe(self, peer: 'KademliaPeer') -> FindNodeResponse:
log.debug("probe %s:%d (%s) for NODE %s",
peer.address, peer.udp_port, peer.node_id.hex()[:8] if peer.node_id else '', self.key.hex()[:8])
response = await self.protocol.get_rpc_peer(peer).find_node(self.key)
return FindNodeResponse(self.key, response)
def search_exhausted(self):
self.put_result(self.active.keys(), finish=True)
def put_result(self, from_iter: typing.Iterable['KademliaPeer'], finish=False):
not_yet_yielded = [
peer for peer in from_iter
if peer not in self.yielded_peers
and peer.node_id != self.protocol.node_id
and self.peer_manager.peer_is_good(peer) is True # return only peers who answered
]
not_yet_yielded.sort(key=lambda peer: self.distance(peer.node_id))
to_yield = not_yet_yielded[:max(constants.K, self.max_results)]
if to_yield:
self.yielded_peers.update(to_yield)
self.iteration_queue.put_nowait(to_yield)
if finish:
self.iteration_queue.put_nowait(None)
def check_result_ready(self, response: FindNodeResponse):
found = response.found and self.key != self.protocol.node_id
if found:
log.debug("found")
return self.put_result(self.active.keys(), finish=True)
class IterativeValueFinder(IterativeFinder):
def __init__(self, loop: asyncio.AbstractEventLoop,
protocol: 'KademliaProtocol', key: bytes,
max_results: typing.Optional[int] = constants.K,
shortlist: typing.Optional[typing.List['KademliaPeer']] = None):
super().__init__(loop, protocol, key, max_results, shortlist)
self.blob_peers: typing.Set['KademliaPeer'] = set()
# this tracks the index of the most recent page we requested from each peer
self.peer_pages: typing.DefaultDict['KademliaPeer', int] = defaultdict(int)
# this tracks the set of blob peers returned by each peer
self.discovered_peers: typing.Dict['KademliaPeer', typing.Set['KademliaPeer']] = defaultdict(set)
async def send_probe(self, peer: 'KademliaPeer') -> FindValueResponse:
log.debug("probe %s:%d (%s) for VALUE %s",
peer.address, peer.udp_port, peer.node_id.hex()[:8], self.key.hex()[:8])
page = self.peer_pages[peer]
response = await self.protocol.get_rpc_peer(peer).find_value(self.key, page=page)
parsed = FindValueResponse(self.key, response)
if not parsed.found:
return parsed
already_known = len(self.discovered_peers[peer])
decoded_peers = set()
for compact_addr in parsed.found_compact_addresses:
try:
decoded_peers.add(decode_tcp_peer_from_compact_address(compact_addr))
except ValueError:
log.warning("misbehaving peer %s:%i returned invalid peer for blob",
peer.address, peer.udp_port)
self.peer_manager.report_failure(peer.address, peer.udp_port)
parsed.found_compact_addresses.clear()
return parsed
self.discovered_peers[peer].update(decoded_peers)
log.debug("probed %s:%i page %i, %i known", peer.address, peer.udp_port, page,
already_known + len(parsed.found_compact_addresses))
if len(self.discovered_peers[peer]) != already_known + len(parsed.found_compact_addresses):
log.warning("misbehaving peer %s:%i returned duplicate peers for blob", peer.address, peer.udp_port)
elif len(parsed.found_compact_addresses) >= constants.K and self.peer_pages[peer] < parsed.pages:
# the peer returned a full page and indicates it has more
self.peer_pages[peer] += 1
if peer in self.contacted:
# the peer must be removed from self.contacted so that it will be probed for the next page
self.contacted.remove(peer)
return parsed
def check_result_ready(self, response: FindValueResponse):
if response.found:
blob_peers = [decode_tcp_peer_from_compact_address(compact_addr)
for compact_addr in response.found_compact_addresses]
to_yield = []
for blob_peer in blob_peers:
if blob_peer not in self.blob_peers:
self.blob_peers.add(blob_peer)
to_yield.append(blob_peer)
if to_yield:
self.iteration_queue.put_nowait(to_yield)
def get_initial_result(self) -> typing.List['KademliaPeer']:
if self.protocol.data_store.has_peers_for_blob(self.key):
return self.protocol.data_store.get_peers_for_blob(self.key)
return []
================================================
FILE: lbry/dht/protocol/protocol.py
================================================
import logging
import socket
import functools
import hashlib
import asyncio
import time
import typing
import random
from asyncio.protocols import DatagramProtocol
from asyncio.transports import DatagramTransport
from prometheus_client import Gauge, Counter, Histogram
from lbry.dht import constants
from lbry.dht.serialization.bencoding import DecodeError
from lbry.dht.serialization.datagram import decode_datagram, ErrorDatagram, ResponseDatagram, RequestDatagram
from lbry.dht.serialization.datagram import RESPONSE_TYPE, ERROR_TYPE, PAGE_KEY
from lbry.dht.error import RemoteException, TransportNotConnected
from lbry.dht.protocol.routing_table import TreeRoutingTable
from lbry.dht.protocol.data_store import DictDataStore
from lbry.dht.peer import make_kademlia_peer
if typing.TYPE_CHECKING:
from lbry.dht.peer import PeerManager, KademliaPeer
log = logging.getLogger(__name__)
OLD_PROTOCOL_ERRORS = {
"findNode() takes exactly 2 arguments (5 given)": "0.19.1",
"findValue() takes exactly 2 arguments (5 given)": "0.19.1"
}
class KademliaRPC:
stored_blob_metric = Gauge(
"stored_blobs", "Number of blobs announced by other peers", namespace="dht_node",
labelnames=("scope",),
)
def __init__(self, protocol: 'KademliaProtocol', loop: asyncio.AbstractEventLoop, peer_port: int = 3333):
self.protocol = protocol
self.loop = loop
self.peer_port = peer_port
self.old_token_secret: bytes = None
self.token_secret = constants.generate_id()
def compact_address(self):
compact_ip = functools.reduce(lambda buff, x: buff + bytearray([int(x)]),
self.protocol.external_ip.split('.'), bytearray())
compact_port = self.peer_port.to_bytes(2, 'big')
return compact_ip + compact_port + self.protocol.node_id
@staticmethod
def ping():
return b'pong'
def store(self, rpc_contact: 'KademliaPeer', blob_hash: bytes, token: bytes, port: int) -> bytes:
if len(blob_hash) != constants.HASH_BITS // 8:
raise ValueError(f"invalid length of blob hash: {len(blob_hash)}")
if not 0 < port < 65535:
raise ValueError(f"invalid tcp port: {port}")
rpc_contact.update_tcp_port(port)
if not self.verify_token(token, rpc_contact.compact_ip()):
if self.loop.time() - self.protocol.started_listening_time < constants.TOKEN_SECRET_REFRESH_INTERVAL:
pass
else:
raise ValueError("Invalid token")
self.protocol.data_store.add_peer_to_blob(
rpc_contact, blob_hash
)
self.stored_blob_metric.labels("global").set(len(self.protocol.data_store))
return b'OK'
def find_node(self, rpc_contact: 'KademliaPeer', key: bytes) -> typing.List[typing.Tuple[bytes, str, int]]:
if len(key) != constants.HASH_LENGTH:
raise ValueError("invalid contact node_id length: %i" % len(key))
contacts = self.protocol.routing_table.find_close_peers(key, sender_node_id=rpc_contact.node_id)
contact_triples = []
for contact in contacts[:constants.K * 2]:
contact_triples.append((contact.node_id, contact.address, contact.udp_port))
return contact_triples
def find_value(self, rpc_contact: 'KademliaPeer', key: bytes, page: int = 0):
page = page if page > 0 else 0
if len(key) != constants.HASH_LENGTH:
raise ValueError("invalid blob_exchange hash length: %i" % len(key))
response = {
b'token': self.make_token(rpc_contact.compact_ip()),
}
if not page:
response[b'contacts'] = self.find_node(rpc_contact, key)[:constants.K]
if self.protocol.protocol_version:
response[b'protocolVersion'] = self.protocol.protocol_version
# get peers we have stored for this blob_exchange
peers = [
peer.compact_address_tcp()
for peer in self.protocol.data_store.get_peers_for_blob(key)
if not rpc_contact.tcp_port or peer.compact_address_tcp() != rpc_contact.compact_address_tcp()
]
# if we don't have k storing peers to return and we have this hash locally, include our contact information
if len(peers) < constants.K and key.hex() in self.protocol.data_store.completed_blobs:
peers.append(self.compact_address())
if not peers:
response[PAGE_KEY] = 0
else:
response[PAGE_KEY] = (len(peers) // (constants.K + 1)) + 1 # how many pages of peers we have for the blob
if len(peers) > constants.K:
random.Random(self.protocol.node_id).shuffle(peers)
if page * constants.K < len(peers):
response[key] = peers[page * constants.K:page * constants.K + constants.K]
return response
def refresh_token(self): # TODO: this needs to be called periodically
self.old_token_secret = self.token_secret
self.token_secret = constants.generate_id()
def make_token(self, compact_ip):
h = hashlib.new('sha384')
h.update(self.token_secret + compact_ip)
return h.digest()
def verify_token(self, token, compact_ip):
h = hashlib.new('sha384')
h.update(self.token_secret + compact_ip)
if self.old_token_secret and not token == h.digest(): # TODO: why should we be accepting the previous token?
h = hashlib.new('sha384')
h.update(self.old_token_secret + compact_ip)
if not token == h.digest():
return False
return True
class RemoteKademliaRPC:
"""
Encapsulates RPC calls to remote Peers
"""
def __init__(self, loop: asyncio.AbstractEventLoop, peer_tracker: 'PeerManager', protocol: 'KademliaProtocol',
peer: 'KademliaPeer'):
self.loop = loop
self.peer_tracker = peer_tracker
self.protocol = protocol
self.peer = peer
async def ping(self) -> bytes:
"""
:return: b'pong'
"""
response = await self.protocol.send_request(
self.peer, RequestDatagram.make_ping(self.protocol.node_id)
)
return response.response
async def store(self, blob_hash: bytes) -> bytes:
"""
:param blob_hash: blob hash as bytes
:return: b'OK'
"""
if len(blob_hash) != constants.HASH_BITS // 8:
raise ValueError(f"invalid length of blob hash: {len(blob_hash)}")
if not self.protocol.peer_port or not 0 < self.protocol.peer_port < 65535:
raise ValueError(f"invalid tcp port: {self.protocol.peer_port}")
token = self.peer_tracker.get_node_token(self.peer.node_id)
if not token:
find_value_resp = await self.find_value(blob_hash)
token = find_value_resp[b'token']
response = await self.protocol.send_request(
self.peer, RequestDatagram.make_store(self.protocol.node_id, blob_hash, token, self.protocol.peer_port)
)
return response.response
async def find_node(self, key: bytes) -> typing.List[typing.Tuple[bytes, str, int]]:
"""
:return: [(node_id, address, udp_port), ...]
"""
if len(key) != constants.HASH_BITS // 8:
raise ValueError(f"invalid length of find node key: {len(key)}")
response = await self.protocol.send_request(
self.peer, RequestDatagram.make_find_node(self.protocol.node_id, key)
)
return [(node_id, address.decode(), udp_port) for node_id, address, udp_port in response.response]
async def find_value(self, key: bytes, page: int = 0) -> typing.Union[typing.Dict]:
"""
:return: {
b'token': ,
b'contacts': [(node_id, address, udp_port), ...]
: [ RemoteKademliaRPC:
return RemoteKademliaRPC(self.loop, self.peer_manager, self, peer)
def start(self):
self.maintaing_routing_task = self.loop.create_task(self.routing_table_task())
def stop(self):
if self.maintaing_routing_task:
self.maintaing_routing_task.cancel()
if self.transport:
self.disconnect()
def disconnect(self):
self.transport.close()
def connection_made(self, transport: DatagramTransport):
self.transport = transport
def connection_lost(self, exc):
self.stop()
@staticmethod
def _migrate_incoming_rpc_args(peer: 'KademliaPeer', method: bytes, *args) -> typing.Tuple[typing.Tuple,
typing.Dict]:
if method == b'store' and peer.protocol_version == 0:
if isinstance(args[1], dict):
blob_hash = args[0]
token = args[1].pop(b'token', None)
port = args[1].pop(b'port', -1)
original_publisher_id = args[1].pop(b'lbryid', None)
age = 0
return (blob_hash, token, port, original_publisher_id, age), {}
return args, {}
async def _add_peer(self, peer: 'KademliaPeer'):
async def probe(some_peer: 'KademliaPeer'):
rpc_peer = self.get_rpc_peer(some_peer)
await rpc_peer.ping()
return await self.routing_table.add_peer(peer, probe)
def add_peer(self, peer: 'KademliaPeer'):
if peer.node_id == self.node_id:
return False
self._to_add.add(peer)
self._wakeup_routing_task.set()
def remove_peer(self, peer: 'KademliaPeer'):
self._to_remove.add(peer)
self._wakeup_routing_task.set()
async def routing_table_task(self):
while True:
while self._to_remove:
async with self._split_lock:
peer = self._to_remove.pop()
self.routing_table.remove_peer(peer)
while self._to_add:
async with self._split_lock:
await self._add_peer(self._to_add.pop())
await asyncio.gather(self._wakeup_routing_task.wait(), asyncio.sleep(.1))
self._wakeup_routing_task.clear()
def _handle_rpc(self, sender_contact: 'KademliaPeer', message: RequestDatagram):
assert sender_contact.node_id != self.node_id, (sender_contact.node_id.hex()[:8],
self.node_id.hex()[:8])
method = message.method
if method not in [b'ping', b'store', b'findNode', b'findValue']:
raise AttributeError('Invalid method: %s' % message.method.decode())
if message.args and isinstance(message.args[-1], dict) and b'protocolVersion' in message.args[-1]:
# args don't need reformatting
args, kwargs = tuple(message.args[:-1]), message.args[-1]
else:
args, kwargs = self._migrate_incoming_rpc_args(sender_contact, message.method, *message.args)
log.debug("%s:%i RECV CALL %s %s:%i", self.external_ip, self.udp_port, message.method.decode(),
sender_contact.address, sender_contact.udp_port)
if method == b'ping':
result = self.node_rpc.ping()
elif method == b'store':
blob_hash, token, port, original_publisher_id, age = args[:5] # pylint: disable=unused-variable
result = self.node_rpc.store(sender_contact, blob_hash, token, port)
else:
key = args[0]
page = kwargs.get(PAGE_KEY, 0)
if method == b'findNode':
result = self.node_rpc.find_node(sender_contact, key)
else:
assert method == b'findValue'
result = self.node_rpc.find_value(sender_contact, key, page)
self.send_response(
sender_contact, ResponseDatagram(RESPONSE_TYPE, message.rpc_id, self.node_id, result),
)
def handle_request_datagram(self, address: typing.Tuple[str, int], request_datagram: RequestDatagram):
# This is an RPC method request
self.received_request_metric.labels(method=request_datagram.method).inc()
self.peer_manager.report_last_requested(address[0], address[1])
peer = self.routing_table.get_peer(request_datagram.node_id)
if not peer:
try:
peer = make_kademlia_peer(request_datagram.node_id, address[0], address[1])
except ValueError as err:
log.warning("error replying to %s: %s", address[0], str(err))
return
try:
self._handle_rpc(peer, request_datagram)
# if the contact is not known to be bad (yet) and we haven't yet queried it, send it a ping so that it
# will be added to our routing table if successful
is_good = self.peer_manager.peer_is_good(peer)
if is_good is None:
self.ping_queue.enqueue_maybe_ping(peer)
# only add a requesting contact to the routing table if it has replied to one of our requests
elif is_good is True:
self.add_peer(peer)
except ValueError as err:
log.debug("error raised handling %s request from %s:%i - %s(%s)",
request_datagram.method, peer.address, peer.udp_port, str(type(err)),
str(err))
self.send_error(
peer,
ErrorDatagram(ERROR_TYPE, request_datagram.rpc_id, self.node_id, str(type(err)).encode(),
str(err).encode())
)
except Exception as err:
log.warning("error raised handling %s request from %s:%i - %s(%s)",
request_datagram.method, peer.address, peer.udp_port, str(type(err)),
str(err))
self.send_error(
peer,
ErrorDatagram(ERROR_TYPE, request_datagram.rpc_id, self.node_id, str(type(err)).encode(),
str(err).encode())
)
def handle_response_datagram(self, address: typing.Tuple[str, int], response_datagram: ResponseDatagram):
# Find the message that triggered this response
if response_datagram.rpc_id in self.sent_messages:
peer, future, _ = self.sent_messages[response_datagram.rpc_id]
if peer.address != address[0]:
future.set_exception(
RemoteException(f"response from {address[0]}, expected {peer.address}")
)
return
# We got a result from the RPC
if peer.node_id == self.node_id:
future.set_exception(RemoteException("node has our node id"))
return
elif response_datagram.node_id == self.node_id:
future.set_exception(RemoteException("incoming message is from our node id"))
return
peer = make_kademlia_peer(response_datagram.node_id, address[0], address[1])
self.peer_manager.report_last_replied(address[0], address[1])
self.peer_manager.update_contact_triple(peer.node_id, address[0], address[1])
if not future.cancelled():
future.set_result(response_datagram)
self.add_peer(peer)
else:
log.warning("%s:%i replied, but after we cancelled the request attempt",
peer.address, peer.udp_port)
else:
# If the original message isn't found, it must have timed out
# TODO: we should probably do something with this...
pass
def handle_error_datagram(self, address, error_datagram: ErrorDatagram):
# The RPC request raised a remote exception; raise it locally
remote_exception = RemoteException(f"{error_datagram.exception_type}({error_datagram.response})")
if error_datagram.rpc_id in self.sent_messages:
peer, future, request = self.sent_messages.pop(error_datagram.rpc_id)
if (peer.address, peer.udp_port) != address:
future.set_exception(
RemoteException(
f"response from {address[0]}:{address[1]}, "
f"expected {peer.address}:{peer.udp_port}"
)
)
return
error_msg = f"" \
f"Error sending '{request.method}' to {peer.address}:{peer.udp_port}\n" \
f"Args: {request.args}\n" \
f"Raised: {str(remote_exception)}"
if 'Invalid token' in error_msg:
log.debug(error_msg)
elif error_datagram.response not in OLD_PROTOCOL_ERRORS:
log.warning(error_msg)
else:
log.debug(
"known dht protocol backwards compatibility error with %s:%i (lbrynet v%s)",
peer.address, peer.udp_port, OLD_PROTOCOL_ERRORS[error_datagram.response]
)
future.set_exception(remote_exception)
return
else:
if error_datagram.response not in OLD_PROTOCOL_ERRORS:
msg = f"Received error from {address[0]}:{address[1]}, but it isn't in response to a " \
f"pending request: {str(remote_exception)}"
log.warning(msg)
else:
log.debug(
"known dht protocol backwards compatibility error with %s:%i (lbrynet v%s)",
address[0], address[1], OLD_PROTOCOL_ERRORS[error_datagram.response]
)
def datagram_received(self, datagram: bytes, address: typing.Tuple[str, int]) -> None: # pylint: disable=arguments-renamed
try:
message = decode_datagram(datagram)
except (ValueError, TypeError, DecodeError):
self.peer_manager.report_failure(address[0], address[1])
log.warning("Couldn't decode dht datagram from %s: %s", address, datagram.hex())
return
if isinstance(message, RequestDatagram):
self.handle_request_datagram(address, message)
elif isinstance(message, ErrorDatagram):
self.handle_error_datagram(address, message)
else:
assert isinstance(message, ResponseDatagram), "sanity"
self.handle_response_datagram(address, message)
async def send_request(self, peer: 'KademliaPeer', request: RequestDatagram) -> ResponseDatagram:
self._send(peer, request)
response_fut = self.sent_messages[request.rpc_id][1]
try:
self.request_sent_metric.labels(method=request.method).inc()
start = time.perf_counter()
response = await asyncio.wait_for(response_fut, self.rpc_timeout)
self.response_time_metric.labels(method=request.method).observe(time.perf_counter() - start)
self.peer_manager.report_last_replied(peer.address, peer.udp_port)
self.request_success_metric.labels(method=request.method).inc()
return response
except asyncio.CancelledError:
if not response_fut.done():
response_fut.cancel()
raise
except (asyncio.TimeoutError, RemoteException):
self.request_error_metric.labels(method=request.method).inc()
self.peer_manager.report_failure(peer.address, peer.udp_port)
if self.peer_manager.peer_is_good(peer) is False:
self.remove_peer(peer)
raise
def send_response(self, peer: 'KademliaPeer', response: ResponseDatagram):
self._send(peer, response)
def send_error(self, peer: 'KademliaPeer', error: ErrorDatagram):
self._send(peer, error)
def _send(self, peer: 'KademliaPeer', message: typing.Union[RequestDatagram, ResponseDatagram, ErrorDatagram]):
if not self.transport or self.transport.is_closing():
raise TransportNotConnected()
data = message.bencode()
if len(data) > constants.MSG_SIZE_LIMIT:
log.warning("cannot send datagram larger than %i bytes (packet is %i bytes)",
constants.MSG_SIZE_LIMIT, len(data))
log.debug("Packet is too large to send: %s", data[:3500].hex())
raise ValueError(
f"cannot send datagram larger than {constants.MSG_SIZE_LIMIT} bytes (packet is {len(data)} bytes)"
)
if isinstance(message, (RequestDatagram, ResponseDatagram)):
assert message.node_id == self.node_id, message
if isinstance(message, RequestDatagram):
assert self.node_id != peer.node_id
def pop_from_sent_messages(_):
if message.rpc_id in self.sent_messages:
self.sent_messages.pop(message.rpc_id)
if isinstance(message, RequestDatagram):
response_fut = self.loop.create_future()
response_fut.add_done_callback(pop_from_sent_messages)
self.sent_messages[message.rpc_id] = (peer, response_fut, message)
try:
self.transport.sendto(data, (peer.address, peer.udp_port))
except OSError as err:
# TODO: handle ENETUNREACH
if err.errno == socket.EWOULDBLOCK:
# i'm scared this may swallow important errors, but i get a million of these
# on Linux and it doesn't seem to affect anything -grin
log.warning("Can't send data to dht: EWOULDBLOCK")
else:
log.error("DHT socket error sending %i bytes to %s:%i - %s (code %i)",
len(data), peer.address, peer.udp_port, str(err), err.errno)
if isinstance(message, RequestDatagram):
self.sent_messages[message.rpc_id][1].set_exception(err)
else:
raise err
if isinstance(message, RequestDatagram):
self.peer_manager.report_last_sent(peer.address, peer.udp_port)
elif isinstance(message, ErrorDatagram):
self.peer_manager.report_failure(peer.address, peer.udp_port)
def change_token(self):
self.old_token_secret = self.token_secret
self.token_secret = constants.generate_id()
def make_token(self, compact_ip):
return constants.digest(self.token_secret + compact_ip)
def verify_token(self, token, compact_ip):
h = constants.HASH_CLASS()
h.update(self.token_secret + compact_ip)
if self.old_token_secret and not token == h.digest(): # TODO: why should we be accepting the previous token?
h = constants.HASH_CLASS()
h.update(self.old_token_secret + compact_ip)
if not token == h.digest():
return False
return True
async def store_to_peer(self, hash_value: bytes, peer: 'KademliaPeer', # pylint: disable=too-many-return-statements
retry: bool = True) -> typing.Tuple[bytes, bool]:
async def __store():
res = await self.get_rpc_peer(peer).store(hash_value)
if res != b"OK":
raise ValueError(res)
log.debug("Stored %s to %s", hash_value.hex()[:8], peer)
return peer.node_id, True
try:
return await __store()
except asyncio.TimeoutError:
log.debug("Timeout while storing blob_hash %s at %s", hash_value.hex()[:8], peer)
return peer.node_id, False
except ValueError as err:
log.error("Unexpected response: %s", err)
return peer.node_id, False
except RemoteException as err:
if 'findValue() takes exactly 2 arguments (5 given)' in str(err):
log.debug("peer %s:%i is running an incompatible version of lbrynet", peer.address, peer.udp_port)
return peer.node_id, False
if 'Invalid token' not in str(err):
log.warning("Unexpected error while storing blob_hash: %s", err)
return peer.node_id, False
self.peer_manager.clear_token(peer.node_id)
if not retry:
return peer.node_id, False
return await self.store_to_peer(hash_value, peer, retry=False)
================================================
FILE: lbry/dht/protocol/routing_table.py
================================================
import asyncio
import random
import logging
import typing
import itertools
from prometheus_client import Gauge
from lbry import utils
from lbry.dht import constants
from lbry.dht.error import RemoteException
from lbry.dht.protocol.distance import Distance
if typing.TYPE_CHECKING:
from lbry.dht.peer import KademliaPeer, PeerManager
log = logging.getLogger(__name__)
class KBucket:
"""
Kademlia K-bucket implementation.
"""
peer_in_routing_table_metric = Gauge(
"peers_in_routing_table", "Number of peers on routing table", namespace="dht_node",
labelnames=("scope",)
)
peer_with_x_bit_colliding_metric = Gauge(
"peer_x_bit_colliding", "Number of peers with at least X bits colliding with this node id",
namespace="dht_node", labelnames=("amount",)
)
def __init__(self, peer_manager: 'PeerManager', range_min: int, range_max: int,
node_id: bytes, capacity: int = constants.K):
"""
@param range_min: The lower boundary for the range in the n-bit ID
space covered by this k-bucket
@param range_max: The upper boundary for the range in the ID space
covered by this k-bucket
"""
self._peer_manager = peer_manager
self.range_min = range_min
self.range_max = range_max
self.peers: typing.List['KademliaPeer'] = []
self._node_id = node_id
self._distance_to_self = Distance(node_id)
self.capacity = capacity
def add_peer(self, peer: 'KademliaPeer') -> bool:
""" Add contact to _contact list in the right order. This will move the
contact to the end of the k-bucket if it is already present.
@raise kademlia.kbucket.BucketFull: Raised when the bucket is full and
the contact isn't in the bucket
already
@param peer: The contact to add
@type peer: dht.contact._Contact
"""
if peer in self.peers:
# Move the existing contact to the end of the list
# - using the new contact to allow add-on data
# (e.g. optimization-specific stuff) to pe updated as well
self.peers.remove(peer)
self.peers.append(peer)
return True
else:
for i, _ in enumerate(self.peers):
local_peer = self.peers[i]
if local_peer.node_id == peer.node_id:
self.peers.remove(local_peer)
self.peers.append(peer)
return True
if len(self.peers) < self.capacity:
self.peers.append(peer)
self.peer_in_routing_table_metric.labels("global").inc()
bits_colliding = utils.get_colliding_prefix_bits(peer.node_id, self._node_id)
self.peer_with_x_bit_colliding_metric.labels(amount=bits_colliding).inc()
return True
else:
return False
def get_peer(self, node_id: bytes) -> 'KademliaPeer':
for peer in self.peers:
if peer.node_id == node_id:
return peer
def get_peers(self, count=-1, exclude_contact=None, sort_distance_to=None) -> typing.List['KademliaPeer']:
""" Returns a list containing up to the first count number of contacts
@param count: The amount of contacts to return (if 0 or less, return
all contacts)
@type count: int
@param exclude_contact: A node node_id to exclude; if this contact is in
the list of returned values, it will be
discarded before returning. If a C{str} is
passed as this argument, it must be the
contact's ID.
@type exclude_contact: str
@param sort_distance_to: Sort distance to the node_id, defaulting to the parent node node_id. If False don't
sort the contacts
@raise IndexError: If the number of requested contacts is too large
@return: Return up to the first count number of contacts in a list
If no contacts are present an empty is returned
@rtype: list
"""
peers = [peer for peer in self.peers if peer.node_id != exclude_contact]
# Return all contacts in bucket
if count <= 0:
count = len(peers)
# Get current contact number
current_len = len(peers)
# If count greater than k - return only k contacts
if count > constants.K:
count = constants.K
if not current_len:
return peers
if sort_distance_to is False:
pass
else:
sort_distance_to = sort_distance_to or self._node_id
peers.sort(key=lambda c: Distance(sort_distance_to)(c.node_id))
return peers[:min(current_len, count)]
def get_bad_or_unknown_peers(self) -> typing.List['KademliaPeer']:
peer = self.get_peers(sort_distance_to=False)
return [
peer for peer in peer
if self._peer_manager.contact_triple_is_good(peer.node_id, peer.address, peer.udp_port) is not True
]
def remove_peer(self, peer: 'KademliaPeer') -> None:
self.peers.remove(peer)
self.peer_in_routing_table_metric.labels("global").dec()
bits_colliding = utils.get_colliding_prefix_bits(peer.node_id, self._node_id)
self.peer_with_x_bit_colliding_metric.labels(amount=bits_colliding).dec()
def key_in_range(self, key: bytes) -> bool:
""" Tests whether the specified key (i.e. node ID) is in the range
of the n-bit ID space covered by this k-bucket (in otherwords, it
returns whether or not the specified key should be placed in this
k-bucket)
@param key: The key to test
@type key: str or int
@return: C{True} if the key is in this k-bucket's range, or C{False}
if not.
@rtype: bool
"""
return self.range_min <= self._distance_to_self(key) < self.range_max
def __len__(self) -> int:
return len(self.peers)
def __contains__(self, item) -> bool:
return item in self.peers
class TreeRoutingTable:
""" This class implements a routing table used by a Node class.
The Kademlia routing table is a binary tree whose leaves are k-buckets,
where each k-bucket contains nodes with some common prefix of their IDs.
This prefix is the k-bucket's position in the binary tree; it therefore
covers some range of ID values, and together all of the k-buckets cover
the entire n-bit ID (or key) space (with no overlap).
@note: In this implementation, nodes in the tree (the k-buckets) are
added dynamically, as needed; this technique is described in the 13-page
version of the Kademlia paper, in section 2.4. It does, however, use the
ping RPC-based k-bucket eviction algorithm described in section 2.2 of
that paper.
BOOTSTRAP MODE: if set to True, we always add all peers. This is so a
bootstrap node does not get a bias towards its own node id and replies are
the best it can provide (joining peer knows its neighbors immediately).
Over time, this will need to be optimized so we use the disk as holding
everything in memory won't be feasible anymore.
See: https://github.com/bittorrent/bootstrap-dht
"""
bucket_in_routing_table_metric = Gauge(
"buckets_in_routing_table", "Number of buckets on routing table", namespace="dht_node",
labelnames=("scope",)
)
def __init__(self, loop: asyncio.AbstractEventLoop, peer_manager: 'PeerManager', parent_node_id: bytes,
split_buckets_under_index: int = constants.SPLIT_BUCKETS_UNDER_INDEX, is_bootstrap_node: bool = False):
self._loop = loop
self._peer_manager = peer_manager
self._parent_node_id = parent_node_id
self._split_buckets_under_index = split_buckets_under_index
self.buckets: typing.List[KBucket] = [
KBucket(
self._peer_manager, range_min=0, range_max=2 ** constants.HASH_BITS, node_id=self._parent_node_id,
capacity=1 << 32 if is_bootstrap_node else constants.K
)
]
def get_peers(self) -> typing.List['KademliaPeer']:
return list(itertools.chain.from_iterable(map(lambda bucket: bucket.peers, self.buckets)))
def _should_split(self, bucket_index: int, to_add: bytes) -> bool:
# https://stackoverflow.com/questions/32129978/highly-unbalanced-kademlia-routing-table/32187456#32187456
if bucket_index < self._split_buckets_under_index:
return True
contacts = self.get_peers()
distance = Distance(self._parent_node_id)
contacts.sort(key=lambda c: distance(c.node_id))
kth_contact = contacts[-1] if len(contacts) < constants.K else contacts[constants.K - 1]
return distance(to_add) < distance(kth_contact.node_id)
def find_close_peers(self, key: bytes, count: typing.Optional[int] = None,
sender_node_id: typing.Optional[bytes] = None) -> typing.List['KademliaPeer']:
exclude = [self._parent_node_id]
if sender_node_id:
exclude.append(sender_node_id)
count = count or constants.K
distance = Distance(key)
contacts = self.get_peers()
contacts = [c for c in contacts if c.node_id not in exclude]
if contacts:
contacts.sort(key=lambda c: distance(c.node_id))
return contacts[:min(count, len(contacts))]
return []
def get_peer(self, contact_id: bytes) -> 'KademliaPeer':
return self.buckets[self._kbucket_index(contact_id)].get_peer(contact_id)
def get_refresh_list(self, start_index: int = 0, force: bool = False) -> typing.List[bytes]:
refresh_ids = []
for offset, _ in enumerate(self.buckets[start_index:]):
refresh_ids.append(self._midpoint_id_in_bucket_range(start_index + offset))
# if we have 3 or fewer populated buckets get two random ids in the range of each to try and
# populate/split the buckets further
buckets_with_contacts = self.buckets_with_contacts()
if buckets_with_contacts <= 3:
for i in range(buckets_with_contacts):
refresh_ids.append(self._random_id_in_bucket_range(i))
refresh_ids.append(self._random_id_in_bucket_range(i))
return refresh_ids
def remove_peer(self, peer: 'KademliaPeer') -> None:
if not peer.node_id:
return
bucket_index = self._kbucket_index(peer.node_id)
try:
self.buckets[bucket_index].remove_peer(peer)
self._join_buckets()
except ValueError:
return
def _kbucket_index(self, key: bytes) -> int:
i = 0
for bucket in self.buckets:
if bucket.key_in_range(key):
return i
else:
i += 1
return i
def _random_id_in_bucket_range(self, bucket_index: int) -> bytes:
random_id = int(random.randrange(self.buckets[bucket_index].range_min, self.buckets[bucket_index].range_max))
return Distance(
self._parent_node_id
)(random_id.to_bytes(constants.HASH_LENGTH, 'big')).to_bytes(constants.HASH_LENGTH, 'big')
def _midpoint_id_in_bucket_range(self, bucket_index: int) -> bytes:
half = int((self.buckets[bucket_index].range_max - self.buckets[bucket_index].range_min) // 2)
return Distance(self._parent_node_id)(
int(self.buckets[bucket_index].range_min + half).to_bytes(constants.HASH_LENGTH, 'big')
).to_bytes(constants.HASH_LENGTH, 'big')
def _split_bucket(self, old_bucket_index: int) -> None:
""" Splits the specified k-bucket into two new buckets which together
cover the same range in the key/ID space
@param old_bucket_index: The index of k-bucket to split (in this table's
list of k-buckets)
@type old_bucket_index: int
"""
# Resize the range of the current (old) k-bucket
old_bucket = self.buckets[old_bucket_index]
split_point = old_bucket.range_max - (old_bucket.range_max - old_bucket.range_min) // 2
# Create a new k-bucket to cover the range split off from the old bucket
new_bucket = KBucket(self._peer_manager, split_point, old_bucket.range_max, self._parent_node_id)
old_bucket.range_max = split_point
# Now, add the new bucket into the routing table tree
self.buckets.insert(old_bucket_index + 1, new_bucket)
# Finally, copy all nodes that belong to the new k-bucket into it...
for contact in old_bucket.peers:
if new_bucket.key_in_range(contact.node_id):
new_bucket.add_peer(contact)
# ...and remove them from the old bucket
for contact in new_bucket.peers:
old_bucket.remove_peer(contact)
self.bucket_in_routing_table_metric.labels("global").set(len(self.buckets))
def _join_buckets(self):
if len(self.buckets) == 1:
return
to_pop = [i for i, bucket in enumerate(self.buckets) if len(bucket) == 0]
if not to_pop:
return
log.info("join buckets %i", len(to_pop))
bucket_index_to_pop = to_pop[0]
assert len(self.buckets[bucket_index_to_pop]) == 0
can_go_lower = bucket_index_to_pop - 1 >= 0
can_go_higher = bucket_index_to_pop + 1 < len(self.buckets)
assert can_go_higher or can_go_lower
bucket = self.buckets[bucket_index_to_pop]
if can_go_lower and can_go_higher:
midpoint = ((bucket.range_max - bucket.range_min) // 2) + bucket.range_min
self.buckets[bucket_index_to_pop - 1].range_max = midpoint - 1
self.buckets[bucket_index_to_pop + 1].range_min = midpoint
elif can_go_lower:
self.buckets[bucket_index_to_pop - 1].range_max = bucket.range_max
elif can_go_higher:
self.buckets[bucket_index_to_pop + 1].range_min = bucket.range_min
self.buckets.remove(bucket)
self.bucket_in_routing_table_metric.labels("global").set(len(self.buckets))
return self._join_buckets()
def buckets_with_contacts(self) -> int:
count = 0
for bucket in self.buckets:
if len(bucket) > 0:
count += 1
return count
async def add_peer(self, peer: 'KademliaPeer', probe: typing.Callable[['KademliaPeer'], typing.Awaitable]):
if not peer.node_id:
log.warning("Tried adding a peer with no node id!")
return False
for my_peer in self.get_peers():
if (my_peer.address, my_peer.udp_port) == (peer.address, peer.udp_port) and my_peer.node_id != peer.node_id:
self.remove_peer(my_peer)
self._join_buckets()
bucket_index = self._kbucket_index(peer.node_id)
if self.buckets[bucket_index].add_peer(peer):
return True
# The bucket is full; see if it can be split (by checking if its range includes the host node's node_id)
if self._should_split(bucket_index, peer.node_id):
self._split_bucket(bucket_index)
# Retry the insertion attempt
result = await self.add_peer(peer, probe)
self._join_buckets()
return result
else:
# We can't split the k-bucket
#
# The 13 page kademlia paper specifies that the least recently contacted node in the bucket
# shall be pinged. If it fails to reply it is replaced with the new contact. If the ping is successful
# the new contact is ignored and not added to the bucket (sections 2.2 and 2.4).
#
# A reasonable extension to this is BEP 0005, which extends the above:
#
# Not all nodes that we learn about are equal. Some are "good" and some are not.
# Many nodes using the DHT are able to send queries and receive responses,
# but are not able to respond to queries from other nodes. It is important that
# each node's routing table must contain only known good nodes. A good node is
# a node has responded to one of our queries within the last 15 minutes. A node
# is also good if it has ever responded to one of our queries and has sent us a
# query within the last 15 minutes. After 15 minutes of inactivity, a node becomes
# questionable. Nodes become bad when they fail to respond to multiple queries
# in a row. Nodes that we know are good are given priority over nodes with unknown status.
#
# When there are bad or questionable nodes in the bucket, the least recent is selected for
# potential replacement (BEP 0005). When all nodes in the bucket are fresh, the head (least recent)
# contact is selected as described in section 2.2 of the kademlia paper. In both cases the new contact
# is ignored if the pinged node replies.
not_good_contacts = self.buckets[bucket_index].get_bad_or_unknown_peers()
not_recently_replied = []
for my_peer in not_good_contacts:
last_replied = self._peer_manager.get_last_replied(my_peer.address, my_peer.udp_port)
if not last_replied or last_replied + 60 < self._loop.time():
not_recently_replied.append(my_peer)
if not_recently_replied:
to_replace = not_recently_replied[0]
else:
to_replace = self.buckets[bucket_index].peers[0]
last_replied = self._peer_manager.get_last_replied(to_replace.address, to_replace.udp_port)
if last_replied and last_replied + 60 > self._loop.time():
return False
log.debug("pinging %s:%s", to_replace.address, to_replace.udp_port)
try:
await probe(to_replace)
return False
except (asyncio.TimeoutError, RemoteException):
log.debug("Replacing dead contact in bucket %i: %s:%i with %s:%i ", bucket_index,
to_replace.address, to_replace.udp_port, peer.address, peer.udp_port)
if to_replace in self.buckets[bucket_index]:
self.buckets[bucket_index].remove_peer(to_replace)
return await self.add_peer(peer, probe)
================================================
FILE: lbry/dht/serialization/__init__.py
================================================
================================================
FILE: lbry/dht/serialization/bencoding.py
================================================
import typing
from lbry.dht.error import DecodeError
def _bencode(data: typing.Union[int, bytes, bytearray, str, list, tuple, dict]) -> bytes:
if isinstance(data, int):
return b'i%de' % data
elif isinstance(data, (bytes, bytearray)):
return b'%d:%s' % (len(data), data)
elif isinstance(data, str):
return b'%d:%s' % (len(data), data.encode())
elif isinstance(data, (list, tuple)):
encoded_list_items = b''
for item in data:
encoded_list_items += _bencode(item)
return b'l%se' % encoded_list_items
elif isinstance(data, dict):
encoded_dict_items = b''
keys = data.keys()
for key in sorted(keys):
encoded_dict_items += _bencode(key)
encoded_dict_items += _bencode(data[key])
return b'd%se' % encoded_dict_items
else:
raise TypeError(f"Cannot bencode {type(data)}")
def _bdecode(data: bytes, start_index: int = 0) -> typing.Tuple[typing.Union[int, bytes, list, tuple, dict], int]:
if data[start_index] == ord('i'):
end_pos = data[start_index:].find(b'e') + start_index
return int(data[start_index + 1:end_pos]), end_pos + 1
elif data[start_index] == ord('l'):
start_index += 1
decoded_list = []
while data[start_index] != ord('e'):
list_data, start_index = _bdecode(data, start_index)
decoded_list.append(list_data)
return decoded_list, start_index + 1
elif data[start_index] == ord('d'):
start_index += 1
decoded_dict = {}
while data[start_index] != ord('e'):
key, start_index = _bdecode(data, start_index)
value, start_index = _bdecode(data, start_index)
decoded_dict[key] = value
return decoded_dict, start_index
else:
split_pos = data[start_index:].find(b':') + start_index
try:
length = int(data[start_index:split_pos])
except (ValueError, TypeError) as err:
raise DecodeError(err)
start_index = split_pos + 1
end_pos = start_index + length
return data[start_index:end_pos], end_pos
def bencode(data: typing.Dict) -> bytes:
if not isinstance(data, dict):
raise TypeError()
return _bencode(data)
def bdecode(data: bytes, allow_non_dict_return: typing.Optional[bool] = False) -> typing.Dict:
assert isinstance(data, bytes), DecodeError(f"invalid data type: {str(type(data))}")
if len(data) == 0:
raise DecodeError('Cannot decode empty string')
try:
result = _bdecode(data)[0]
if not allow_non_dict_return and not isinstance(result, dict):
raise ValueError(f'expected dict, got {type(result)}')
return result
except (ValueError, TypeError) as err:
raise DecodeError(err)
================================================
FILE: lbry/dht/serialization/datagram.py
================================================
import typing
from functools import reduce
from lbry.dht import constants
from lbry.dht.serialization.bencoding import bencode, bdecode
REQUEST_TYPE = 0
RESPONSE_TYPE = 1
ERROR_TYPE = 2
OPTIONAL_ARG_OFFSET = 100
# bencode representation of argument keys
PAGE_KEY = b'p'
OPTIONAL_FIELDS = ()
class KademliaDatagramBase:
"""
field names are used to unwrap/wrap the argument names to index integers that replace them in a datagram
all packets have an argument dictionary when bdecoded starting with {0: , 1: , 2: , ...}
these correspond to the packet_type, rpc_id, and node_id args
"""
required_fields = [
'packet_type',
'rpc_id',
'node_id'
]
expected_packet_type = -1
def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes):
self.packet_type = packet_type
if self.expected_packet_type != packet_type:
raise ValueError(f"invalid packet type: {packet_type}, expected {self.expected_packet_type}")
if len(rpc_id) != constants.RPC_ID_LENGTH:
raise ValueError(f"invalid rpc node_id: {len(rpc_id)} bytes (expected 20)")
if not len(node_id) == constants.HASH_LENGTH:
raise ValueError(f"invalid node node_id: {len(node_id)} bytes (expected 48)")
self.rpc_id = rpc_id
self.node_id = node_id
def bencode(self) -> bytes:
datagram = {
i: getattr(self, k) for i, k in enumerate(self.required_fields)
}
for i, k in enumerate(OPTIONAL_FIELDS):
value = getattr(self, k, None)
if value is not None:
datagram[i + OPTIONAL_ARG_OFFSET] = value
return bencode(datagram)
class RequestDatagram(KademliaDatagramBase):
required_fields = [
'packet_type',
'rpc_id',
'node_id',
'method',
'args'
]
expected_packet_type = REQUEST_TYPE
def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes, method: bytes,
args: typing.Optional[typing.List] = None):
super().__init__(packet_type, rpc_id, node_id)
self.method = method
self.args = args or []
if not self.args:
self.args.append({})
if isinstance(self.args[-1], dict):
self.args[-1][b'protocolVersion'] = 1
else:
self.args.append({b'protocolVersion': 1})
@classmethod
def make_ping(cls, from_node_id: bytes, rpc_id: typing.Optional[bytes] = None) -> 'RequestDatagram':
rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]
return cls(REQUEST_TYPE, rpc_id, from_node_id, b'ping')
@classmethod
def make_store(cls, from_node_id: bytes, blob_hash: bytes, token: bytes, port: int,
rpc_id: typing.Optional[bytes] = None) -> 'RequestDatagram':
rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]
if len(blob_hash) != constants.HASH_BITS // 8:
raise ValueError(f"invalid blob hash length: {len(blob_hash)}")
if not 0 < port < 65536:
raise ValueError(f"invalid port: {port}")
if len(token) != constants.HASH_BITS // 8:
raise ValueError(f"invalid token length: {len(token)}")
store_args = [blob_hash, token, port, from_node_id, 0]
return cls(REQUEST_TYPE, rpc_id, from_node_id, b'store', store_args)
@classmethod
def make_find_node(cls, from_node_id: bytes, key: bytes,
rpc_id: typing.Optional[bytes] = None) -> 'RequestDatagram':
rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]
if len(key) != constants.HASH_BITS // 8:
raise ValueError(f"invalid key length: {len(key)}")
return cls(REQUEST_TYPE, rpc_id, from_node_id, b'findNode', [key])
@classmethod
def make_find_value(cls, from_node_id: bytes, key: bytes,
rpc_id: typing.Optional[bytes] = None, page: int = 0) -> 'RequestDatagram':
rpc_id = rpc_id or constants.generate_id()[:constants.RPC_ID_LENGTH]
if len(key) != constants.HASH_BITS // 8:
raise ValueError(f"invalid key length: {len(key)}")
if page < 0:
raise ValueError(f"cannot request a negative page ({page})")
return cls(REQUEST_TYPE, rpc_id, from_node_id, b'findValue', [key, {PAGE_KEY: page}])
class ResponseDatagram(KademliaDatagramBase):
required_fields = [
'packet_type',
'rpc_id',
'node_id',
'response'
]
expected_packet_type = RESPONSE_TYPE
def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes, response):
super().__init__(packet_type, rpc_id, node_id)
self.response = response
class ErrorDatagram(KademliaDatagramBase):
required_fields = [
'packet_type',
'rpc_id',
'node_id',
'exception_type',
'response',
]
expected_packet_type = ERROR_TYPE
def __init__(self, packet_type: int, rpc_id: bytes, node_id: bytes, exception_type: bytes, response: bytes):
super().__init__(packet_type, rpc_id, node_id)
self.exception_type = exception_type.decode()
self.response = response.decode()
def _decode_datagram(datagram: bytes):
msg_types = {
REQUEST_TYPE: RequestDatagram,
RESPONSE_TYPE: ResponseDatagram,
ERROR_TYPE: ErrorDatagram
}
primitive: typing.Dict = bdecode(datagram)
converted = {
str(k).encode() if not isinstance(k, bytes) else k: v for k, v in primitive.items()
}
if converted[b'0'] in [REQUEST_TYPE, ERROR_TYPE, RESPONSE_TYPE]: # pylint: disable=unsubscriptable-object
datagram_type = converted[b'0'] # pylint: disable=unsubscriptable-object
else:
raise ValueError("invalid datagram type")
datagram_class = msg_types[datagram_type]
decoded = {
k: converted[str(i).encode()] # pylint: disable=unsubscriptable-object
for i, k in enumerate(datagram_class.required_fields)
if str(i).encode() in converted # pylint: disable=unsupported-membership-test
}
for i, _ in enumerate(OPTIONAL_FIELDS):
if str(i + OPTIONAL_ARG_OFFSET).encode() in converted:
decoded[i + OPTIONAL_ARG_OFFSET] = converted[str(i + OPTIONAL_ARG_OFFSET).encode()]
return decoded, datagram_class
def decode_datagram(datagram: bytes) -> typing.Union[RequestDatagram, ResponseDatagram, ErrorDatagram]:
decoded, datagram_class = _decode_datagram(datagram)
return datagram_class(**decoded)
def make_compact_ip(address: str) -> bytearray:
compact_ip = reduce(lambda buff, x: buff + bytearray([int(x)]), address.split('.'), bytearray())
if len(compact_ip) != 4:
raise ValueError("invalid IPv4 length")
return compact_ip
def make_compact_address(node_id: bytes, address: str, port: int) -> bytearray:
compact_ip = make_compact_ip(address)
if not 0 < port < 65536:
raise ValueError(f'Invalid port: {port}')
if len(node_id) != constants.HASH_BITS // 8:
raise ValueError("invalid node node_id length")
return compact_ip + port.to_bytes(2, 'big') + node_id
def decode_compact_address(compact_address: bytes) -> typing.Tuple[bytes, str, int]:
address = "{}.{}.{}.{}".format(*compact_address[:4])
port = int.from_bytes(compact_address[4:6], 'big')
node_id = compact_address[6:]
if not 0 < port < 65536:
raise ValueError(f'Invalid port: {port}')
if len(node_id) != constants.HASH_BITS // 8:
raise ValueError("invalid node node_id length")
return node_id, address, port
================================================
FILE: lbry/error/Makefile
================================================
generate:
python generate.py generate > __init__.py
analyze:
python generate.py analyze
================================================
FILE: lbry/error/README.md
================================================
# Exceptions
Exceptions in LBRY are defined and generated from the Markdown table at the end of this README.
## Guidelines
When possible, use [built-in Python exceptions](https://docs.python.org/3/library/exceptions.html) or `aiohttp` [general client](https://docs.aiohttp.org/en/latest/client_reference.html#client-exceptions) / [HTTP](https://docs.aiohttp.org/en/latest/web_exceptions.html) exceptions, unless:
1. You want to provide a better error message (extend the closest built-in/`aiohttp` exception in this case).
2. You need to represent a new situation.
When defining your own exceptions, consider:
1. Extending a built-in Python or `aiohttp` exception.
2. Using contextual variables in the error message.
## Table Column Definitions
Column | Meaning
---|---
Code | Codes are used only to define the hierarchy of exceptions and do not end up in the generated output, it is okay to re-number things as necessary at anytime to achieve the desired hierarchy.
Name | Becomes the class name of the exception with "Error" appended to the end. Changing names of existing exceptions makes the API backwards incompatible. When extending other exceptions you must specify the full class name, manually adding "Error" as necessary (if extending another SDK exception).
Message | User friendly error message explaining the exceptional event. Supports Python formatted strings: any variables used in the string will be generated as arguments in the `__init__` method. Use `--` to provide a doc string after the error message to be added to the class definition.
## Exceptions Table
Code | Name | Message
---:|---|---
**1xx** | UserInput | User input errors.
**10x** | Command | Errors preparing to execute commands.
101 | CommandDoesNotExist | Command '{command}' does not exist.
102 | CommandDeprecated | Command '{command}' is deprecated.
103 | CommandInvalidArgument | Invalid argument '{argument}' to command '{command}'.
104 | CommandTemporarilyUnavailable | Command '{command}' is temporarily unavailable. -- Such as waiting for required components to start.
105 | CommandPermanentlyUnavailable | Command '{command}' is permanently unavailable. -- such as when required component was intentionally configured not to start.
**11x** | InputValue(ValueError) | Invalid argument value provided to command.
111 | GenericInputValue | The value '{value}' for argument '{argument}' is not valid.
112 | InputValueIsNone | None or null is not valid value for argument '{argument}'.
113 | ConflictingInputValue | Only '{first_argument}' or '{second_argument}' is allowed, not both.
114 | InputStringIsBlank | {argument} cannot be blank.
115 | EmptyPublishedFile | Cannot publish empty file: {file_path}
116 | MissingPublishedFile | File does not exist: {file_path}
117 | InvalidStreamURL | Invalid LBRY stream URL: '{url}' -- When an URL cannot be downloaded, such as '@Channel/' or a collection
**2xx** | Configuration | Configuration errors.
201 | ConfigWrite | Cannot write configuration file '{path}'. -- When writing the default config fails on startup, such as due to permission issues.
202 | ConfigRead | Cannot find provided configuration file '{path}'. -- Can't open the config file user provided via command line args.
203 | ConfigParse | Failed to parse the configuration file '{path}'. -- Includes the syntax error / line number to help user fix it.
204 | ConfigMissing | Configuration file '{path}' is missing setting that has no default / fallback.
205 | ConfigInvalid | Configuration file '{path}' has setting with invalid value.
**3xx** | Network | **Networking**
301 | NoInternet | No internet connection.
302 | NoUPnPSupport | Router does not support UPnP.
**4xx** | Wallet | **Wallet Errors**
401 | TransactionRejected | Transaction rejected, unknown reason.
402 | TransactionFeeTooLow | Fee too low.
403 | TransactionInvalidSignature | Invalid signature.
404 | InsufficientFunds | Not enough funds to cover this transaction. -- determined by wallet prior to attempting to broadcast a tx; this is different for example from a TX being created and sent but then rejected by lbrycrd for unspendable utxos.
405 | ChannelKeyNotFound | Channel signing key not found.
406 | ChannelKeyInvalid | Channel signing key is out of date. -- For example, channel was updated but you don't have the updated key.
407 | DataDownload | Failed to download blob. *generic*
408 | PrivateKeyNotFound | Couldn't find private key for {key} '{value}'.
410 | Resolve | Failed to resolve '{url}'.
411 | ResolveTimeout | Failed to resolve '{url}' within the timeout.
411 | ResolveCensored | Resolve of '{url}' was censored by channel with claim id '{censor_id}'.
420 | KeyFeeAboveMaxAllowed | {message}
421 | InvalidPassword | Password is invalid.
422 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version.
423 | TooManyClaimSearchParameters | {key} cant have more than {limit} items.
424 | AlreadyPurchased | You already have a purchase for claim_id '{claim_id_hex}'. Use --allow-duplicate-purchase flag to override.
431 | ServerPaymentInvalidAddress | Invalid address from wallet server: '{address}' - skipping payment round.
432 | ServerPaymentWalletLocked | Cannot spend funds with locked wallet, skipping payment round.
433 | ServerPaymentFeeAboveMaxAllowed | Daily server fee of {daily_fee} exceeds maximum configured of {max_fee} LBC.
434 | WalletNotLoaded | Wallet {wallet_id} is not loaded.
435 | WalletAlreadyLoaded | Wallet {wallet_path} is already loaded.
436 | WalletNotFound | Wallet not found at {wallet_path}.
437 | WalletAlreadyExists | Wallet {wallet_path} already exists, use `wallet_add` to load it.
**5xx** | Blob | **Blobs**
500 | BlobNotFound | Blob not found.
501 | BlobPermissionDenied | Permission denied to read blob.
502 | BlobTooBig | Blob is too big.
503 | BlobEmpty | Blob is empty.
510 | BlobFailedDecryption | Failed to decrypt blob.
511 | CorruptBlob | Blobs is corrupted.
520 | BlobFailedEncryption | Failed to encrypt blob.
531 | DownloadCancelled | Download was canceled.
532 | DownloadSDTimeout | Failed to download sd blob {download} within timeout.
533 | DownloadDataTimeout | Failed to download data blobs for sd hash {download} within timeout.
534 | InvalidStreamDescriptor | {message}
535 | InvalidData | {message}
536 | InvalidBlobHash | {message}
**6xx** | Component | **Components**
601 | ComponentStartConditionNotMet | Unresolved dependencies for: {components}
602 | ComponentsNotStarted | {message}
**7xx** | CurrencyExchange | **Currency Exchange**
701 | InvalidExchangeRateResponse | Failed to get exchange rate from {source}: {reason}
702 | CurrencyConversion | {message}
703 | InvalidCurrency | Invalid currency: {currency} is not a supported currency.
================================================
FILE: lbry/error/__init__.py
================================================
from .base import BaseError, claim_id
class UserInputError(BaseError):
"""
User input errors.
"""
class CommandError(UserInputError):
"""
Errors preparing to execute commands.
"""
class CommandDoesNotExistError(CommandError):
def __init__(self, command):
self.command = command
super().__init__(f"Command '{command}' does not exist.")
class CommandDeprecatedError(CommandError):
def __init__(self, command):
self.command = command
super().__init__(f"Command '{command}' is deprecated.")
class CommandInvalidArgumentError(CommandError):
def __init__(self, argument, command):
self.argument = argument
self.command = command
super().__init__(f"Invalid argument '{argument}' to command '{command}'.")
class CommandTemporarilyUnavailableError(CommandError):
"""
Such as waiting for required components to start.
"""
def __init__(self, command):
self.command = command
super().__init__(f"Command '{command}' is temporarily unavailable.")
class CommandPermanentlyUnavailableError(CommandError):
"""
such as when required component was intentionally configured not to start.
"""
def __init__(self, command):
self.command = command
super().__init__(f"Command '{command}' is permanently unavailable.")
class InputValueError(UserInputError, ValueError):
"""
Invalid argument value provided to command.
"""
class GenericInputValueError(InputValueError):
def __init__(self, value, argument):
self.value = value
self.argument = argument
super().__init__(f"The value '{value}' for argument '{argument}' is not valid.")
class InputValueIsNoneError(InputValueError):
def __init__(self, argument):
self.argument = argument
super().__init__(f"None or null is not valid value for argument '{argument}'.")
class ConflictingInputValueError(InputValueError):
def __init__(self, first_argument, second_argument):
self.first_argument = first_argument
self.second_argument = second_argument
super().__init__(f"Only '{first_argument}' or '{second_argument}' is allowed, not both.")
class InputStringIsBlankError(InputValueError):
def __init__(self, argument):
self.argument = argument
super().__init__(f"{argument} cannot be blank.")
class EmptyPublishedFileError(InputValueError):
def __init__(self, file_path):
self.file_path = file_path
super().__init__(f"Cannot publish empty file: {file_path}")
class MissingPublishedFileError(InputValueError):
def __init__(self, file_path):
self.file_path = file_path
super().__init__(f"File does not exist: {file_path}")
class InvalidStreamURLError(InputValueError):
"""
When an URL cannot be downloaded, such as '@Channel/' or a collection
"""
def __init__(self, url):
self.url = url
super().__init__(f"Invalid LBRY stream URL: '{url}'")
class ConfigurationError(BaseError):
"""
Configuration errors.
"""
class ConfigWriteError(ConfigurationError):
"""
When writing the default config fails on startup, such as due to permission issues.
"""
def __init__(self, path):
self.path = path
super().__init__(f"Cannot write configuration file '{path}'.")
class ConfigReadError(ConfigurationError):
"""
Can't open the config file user provided via command line args.
"""
def __init__(self, path):
self.path = path
super().__init__(f"Cannot find provided configuration file '{path}'.")
class ConfigParseError(ConfigurationError):
"""
Includes the syntax error / line number to help user fix it.
"""
def __init__(self, path):
self.path = path
super().__init__(f"Failed to parse the configuration file '{path}'.")
class ConfigMissingError(ConfigurationError):
def __init__(self, path):
self.path = path
super().__init__(f"Configuration file '{path}' is missing setting that has no default / fallback.")
class ConfigInvalidError(ConfigurationError):
def __init__(self, path):
self.path = path
super().__init__(f"Configuration file '{path}' has setting with invalid value.")
class NetworkError(BaseError):
"""
**Networking**
"""
class NoInternetError(NetworkError):
def __init__(self):
super().__init__("No internet connection.")
class NoUPnPSupportError(NetworkError):
def __init__(self):
super().__init__("Router does not support UPnP.")
class WalletError(BaseError):
"""
**Wallet Errors**
"""
class TransactionRejectedError(WalletError):
def __init__(self):
super().__init__("Transaction rejected, unknown reason.")
class TransactionFeeTooLowError(WalletError):
def __init__(self):
super().__init__("Fee too low.")
class TransactionInvalidSignatureError(WalletError):
def __init__(self):
super().__init__("Invalid signature.")
class InsufficientFundsError(WalletError):
"""
determined by wallet prior to attempting to broadcast a tx; this is different for example from a TX
being created and sent but then rejected by lbrycrd for unspendable utxos.
"""
def __init__(self):
super().__init__("Not enough funds to cover this transaction.")
class ChannelKeyNotFoundError(WalletError):
def __init__(self):
super().__init__("Channel signing key not found.")
class ChannelKeyInvalidError(WalletError):
"""
For example, channel was updated but you don't have the updated key.
"""
def __init__(self):
super().__init__("Channel signing key is out of date.")
class DataDownloadError(WalletError):
def __init__(self):
super().__init__("Failed to download blob. *generic*")
class PrivateKeyNotFoundError(WalletError):
def __init__(self, key, value):
self.key = key
self.value = value
super().__init__(f"Couldn't find private key for {key} '{value}'.")
class ResolveError(WalletError):
def __init__(self, url):
self.url = url
super().__init__(f"Failed to resolve '{url}'.")
class ResolveTimeoutError(WalletError):
def __init__(self, url):
self.url = url
super().__init__(f"Failed to resolve '{url}' within the timeout.")
class ResolveCensoredError(WalletError):
def __init__(self, url, censor_id, censor_row):
self.url = url
self.censor_id = censor_id
self.censor_row = censor_row
super().__init__(f"Resolve of '{url}' was censored by channel with claim id '{censor_id}'.")
class KeyFeeAboveMaxAllowedError(WalletError):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
class InvalidPasswordError(WalletError):
def __init__(self):
super().__init__("Password is invalid.")
class IncompatibleWalletServerError(WalletError):
def __init__(self, server, port):
self.server = server
self.port = port
super().__init__(f"'{server}:{port}' has an incompatibly old version.")
class TooManyClaimSearchParametersError(WalletError):
def __init__(self, key, limit):
self.key = key
self.limit = limit
super().__init__(f"{key} cant have more than {limit} items.")
class AlreadyPurchasedError(WalletError):
"""
allow-duplicate-purchase flag to override.
"""
def __init__(self, claim_id_hex):
self.claim_id_hex = claim_id_hex
super().__init__(f"You already have a purchase for claim_id '{claim_id_hex}'. Use")
class ServerPaymentInvalidAddressError(WalletError):
def __init__(self, address):
self.address = address
super().__init__(f"Invalid address from wallet server: '{address}' - skipping payment round.")
class ServerPaymentWalletLockedError(WalletError):
def __init__(self):
super().__init__("Cannot spend funds with locked wallet, skipping payment round.")
class ServerPaymentFeeAboveMaxAllowedError(WalletError):
def __init__(self, daily_fee, max_fee):
self.daily_fee = daily_fee
self.max_fee = max_fee
super().__init__(f"Daily server fee of {daily_fee} exceeds maximum configured of {max_fee} LBC.")
class WalletNotLoadedError(WalletError):
def __init__(self, wallet_id):
self.wallet_id = wallet_id
super().__init__(f"Wallet {wallet_id} is not loaded.")
class WalletAlreadyLoadedError(WalletError):
def __init__(self, wallet_path):
self.wallet_path = wallet_path
super().__init__(f"Wallet {wallet_path} is already loaded.")
class WalletNotFoundError(WalletError):
def __init__(self, wallet_path):
self.wallet_path = wallet_path
super().__init__(f"Wallet not found at {wallet_path}.")
class WalletAlreadyExistsError(WalletError):
def __init__(self, wallet_path):
self.wallet_path = wallet_path
super().__init__(f"Wallet {wallet_path} already exists, use `wallet_add` to load it.")
class BlobError(BaseError):
"""
**Blobs**
"""
class BlobNotFoundError(BlobError):
def __init__(self):
super().__init__("Blob not found.")
class BlobPermissionDeniedError(BlobError):
def __init__(self):
super().__init__("Permission denied to read blob.")
class BlobTooBigError(BlobError):
def __init__(self):
super().__init__("Blob is too big.")
class BlobEmptyError(BlobError):
def __init__(self):
super().__init__("Blob is empty.")
class BlobFailedDecryptionError(BlobError):
def __init__(self):
super().__init__("Failed to decrypt blob.")
class CorruptBlobError(BlobError):
def __init__(self):
super().__init__("Blobs is corrupted.")
class BlobFailedEncryptionError(BlobError):
def __init__(self):
super().__init__("Failed to encrypt blob.")
class DownloadCancelledError(BlobError):
def __init__(self):
super().__init__("Download was canceled.")
class DownloadSDTimeoutError(BlobError):
def __init__(self, download):
self.download = download
super().__init__(f"Failed to download sd blob {download} within timeout.")
class DownloadDataTimeoutError(BlobError):
def __init__(self, download):
self.download = download
super().__init__(f"Failed to download data blobs for sd hash {download} within timeout.")
class InvalidStreamDescriptorError(BlobError):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
class InvalidDataError(BlobError):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
class InvalidBlobHashError(BlobError):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
class ComponentError(BaseError):
"""
**Components**
"""
class ComponentStartConditionNotMetError(ComponentError):
def __init__(self, components):
self.components = components
super().__init__(f"Unresolved dependencies for: {components}")
class ComponentsNotStartedError(ComponentError):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
class CurrencyExchangeError(BaseError):
"""
**Currency Exchange**
"""
class InvalidExchangeRateResponseError(CurrencyExchangeError):
def __init__(self, source, reason):
self.source = source
self.reason = reason
super().__init__(f"Failed to get exchange rate from {source}: {reason}")
class CurrencyConversionError(CurrencyExchangeError):
def __init__(self, message):
self.message = message
super().__init__(f"{message}")
class InvalidCurrencyError(CurrencyExchangeError):
def __init__(self, currency):
self.currency = currency
super().__init__(f"Invalid currency: {currency} is not a supported currency.")
================================================
FILE: lbry/error/base.py
================================================
from binascii import hexlify
def claim_id(claim_hash):
return hexlify(claim_hash[::-1]).decode()
class BaseError(Exception):
pass
================================================
FILE: lbry/error/generate.py
================================================
import re
import sys
import argparse
from pathlib import Path
from textwrap import fill, indent
INDENT = ' ' * 4
CLASS = """
class {name}({parents}):{doc}
"""
INIT = """
def __init__({args}):{fields}
super().__init__({format}"{message}")
"""
FUNCTIONS = ['claim_id']
class ErrorClass:
def __init__(self, hierarchy, name, message):
self.hierarchy = hierarchy.replace('**', '')
self.other_parents = []
if '(' in name:
assert ')' in name, f"Missing closing parenthesis in '{name}'."
self.other_parents = name[name.find('(')+1:name.find(')')].split(',')
name = name[:name.find('(')]
self.name = name
self.class_name = name+'Error'
self.message = message
self.comment = ""
if '--' in message:
self.message, self.comment = message.split('--')
self.message = self.message.strip()
self.comment = self.comment.strip()
@property
def is_leaf(self):
return 'x' not in self.hierarchy
@property
def code(self):
return self.hierarchy.replace('x', '')
@property
def parent_codes(self):
return self.hierarchy[0:2], self.hierarchy[0]
def get_arguments(self):
args = ['self']
for arg in re.findall('{([a-z0-1_()]+)}', self.message):
for func in FUNCTIONS:
if arg.startswith(f'{func}('):
arg = arg[len(f'{func}('):-1]
break
args.append(arg)
return args
@staticmethod
def get_fields(args):
if len(args) > 1:
return ''.join(f'\n{INDENT*2}self.{field} = {field}' for field in args[1:])
return ''
@staticmethod
def get_doc_string(doc):
if doc:
return f'\n{INDENT}"""\n{indent(fill(doc, 100), INDENT)}\n{INDENT}"""'
return ""
def render(self, out, parent):
if not parent:
parents = ['BaseError']
else:
parents = [parent.class_name]
parents += self.other_parents
args = self.get_arguments()
if self.is_leaf:
out.write((CLASS + INIT).format(
name=self.class_name, parents=', '.join(parents),
args=', '.join(args), fields=self.get_fields(args),
message=self.message, doc=self.get_doc_string(self.comment), format='f' if len(args) > 1 else ''
))
else:
out.write(CLASS.format(
name=self.class_name, parents=', '.join(parents),
doc=self.get_doc_string(self.comment or self.message)
))
def get_errors():
with open('README.md', 'r') as readme:
lines = iter(readme.readlines())
for line in lines:
if line.startswith('## Exceptions Table'):
break
for line in lines:
if line.startswith('---:|'):
break
for line in lines:
if not line:
break
yield ErrorClass(*[c.strip() for c in line.split('|')])
def find_parent(stack, child):
for parent_code in child.parent_codes:
parent = stack.get(parent_code)
if parent:
return parent
def generate(out):
out.write(f"from .base import BaseError, {', '.join(FUNCTIONS)}\n")
stack = {}
for error in get_errors():
error.render(out, find_parent(stack, error))
if not error.is_leaf:
assert error.code not in stack, f"Duplicate code: {error.code}"
stack[error.code] = error
def analyze():
errors = {e.class_name: [] for e in get_errors() if e.is_leaf}
here = Path(__file__).absolute().parents[0]
module = here.parent
for file_path in module.glob('**/*.py'):
if here in file_path.parents:
continue
with open(file_path) as src_file:
src = src_file.read()
for error in errors.keys():
found = src.count(error)
if found > 0:
errors[error].append((file_path, found))
print('Unused Errors:\n')
for error, used in errors.items():
if used:
print(f' - {error}')
for use in used:
print(f' {use[0].relative_to(module.parent)} {use[1]}')
print('')
print('')
print('Unused Errors:')
for error, used in errors.items():
if not used:
print(f' - {error}')
def main():
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=['generate', 'analyze'])
args = parser.parse_args()
if args.action == "analyze":
analyze()
elif args.action == "generate":
generate(sys.stdout)
if __name__ == "__main__":
main()
================================================
FILE: lbry/extras/__init__.py
================================================
================================================
FILE: lbry/extras/cli.py
================================================
import os
import sys
import shutil
import signal
import pathlib
import json
import asyncio
import argparse
import logging
import logging.handlers
import aiohttp
from aiohttp.web import GracefulExit
from docopt import docopt
from lbry import __version__ as lbrynet_version
from lbry.extras.daemon.daemon import Daemon
from lbry.conf import Config, CLIConfig
log = logging.getLogger('lbry')
def display(data):
print(json.dumps(data, indent=2))
async def execute_command(conf, method, params, callback=display):
async with aiohttp.ClientSession() as session:
try:
message = {'method': method, 'params': params}
async with session.get(conf.api_connection_url, json=message) as resp:
try:
data = await resp.json()
if 'result' in data:
return callback(data['result'])
elif 'error' in data:
return callback(data['error'])
except Exception as e:
log.exception('Could not process response from server:', exc_info=e)
except aiohttp.ClientConnectionError:
print("Could not connect to daemon. Are you sure it's running?")
def normalize_value(x, key=None):
if not isinstance(x, str):
return x
if key in ('uri', 'channel_name', 'name', 'file_name', 'claim_name', 'download_directory'):
return x
if x.lower() == 'true':
return True
if x.lower() == 'false':
return False
if x.isdigit():
return int(x)
return x
def remove_brackets(key):
if key.startswith("<") and key.endswith(">"):
return str(key[1:-1])
return key
def set_kwargs(parsed_args):
kwargs = {}
for key, arg in parsed_args.items():
if arg is None:
continue
k = None
if key.startswith("--") and remove_brackets(key[2:]) not in kwargs:
k = remove_brackets(key[2:])
elif remove_brackets(key) not in kwargs:
k = remove_brackets(key)
kwargs[k] = normalize_value(arg, k)
return kwargs
def split_subparser_argument(parent, original, name, condition):
new_sub_parser = argparse._SubParsersAction(
original.option_strings,
original._prog_prefix,
original._parser_class,
metavar=original.metavar
)
new_sub_parser._name_parser_map = original._name_parser_map
new_sub_parser._choices_actions = [
a for a in original._choices_actions if condition(original._name_parser_map[a.dest])
]
group = argparse._ArgumentGroup(parent, name)
group._group_actions = [new_sub_parser]
return group
class ArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, group_name=None, **kwargs):
super().__init__(*args, formatter_class=HelpFormatter, add_help=False, **kwargs)
self.add_argument(
'--help', dest='help', action='store_true', default=False,
help='Show this help message and exit.'
)
self._optionals.title = 'Options'
if group_name is None:
self.epilog = (
"Run 'lbrynet COMMAND --help' for more information on a command or group."
)
else:
self.epilog = (
f"Run 'lbrynet {group_name} COMMAND --help' for more information on a command."
)
self.set_defaults(group=group_name, group_parser=self)
def format_help(self):
formatter = self._get_formatter()
formatter.add_usage(
self.usage, self._actions, self._mutually_exclusive_groups
)
formatter.add_text(self.description)
# positionals, optionals and user-defined groups
for action_group in self._granular_action_groups:
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
formatter.add_text(self.epilog)
return formatter.format_help()
@property
def _granular_action_groups(self):
if self.prog != 'lbrynet':
yield from self._action_groups
return
yield self._optionals
action: argparse._SubParsersAction = self._positionals._group_actions[0]
yield split_subparser_argument(
self, action, "Grouped Commands", lambda parser: 'group' in parser._defaults
)
yield split_subparser_argument(
self, action, "Commands", lambda parser: 'group' not in parser._defaults
)
def error(self, message):
self.print_help(argparse._sys.stderr)
self.exit(2, f"\n{message}\n")
class HelpFormatter(argparse.HelpFormatter):
def add_usage(self, usage, actions, groups, prefix='Usage: '):
super().add_usage(
usage, [a for a in actions if a.option_strings != ['--help']], groups, prefix
)
def add_command_parser(parent, command):
subcommand = parent.add_parser(
command['name'],
help=command['doc'].strip().splitlines()[0]
)
subcommand.set_defaults(
api_method_name=command['api_method_name'],
command=command['name'],
doc=command['doc'],
replaced_by=command.get('replaced_by', None)
)
def get_argument_parser():
root = ArgumentParser(
'lbrynet', description='An interface to the LBRY Network.', allow_abbrev=False,
)
root.add_argument(
'-v', '--version', dest='cli_version', action="store_true",
help='Show lbrynet CLI version and exit.'
)
root.set_defaults(group=None, command=None)
CLIConfig.contribute_to_argparse(root)
sub = root.add_subparsers(metavar='COMMAND')
start = sub.add_parser(
'start',
usage='lbrynet start [--config FILE] [--data-dir DIR] [--wallet-dir DIR] [--download-dir DIR] ...',
help='Start LBRY Network interface.'
)
start.add_argument(
'--quiet', dest='quiet', action="store_true",
help='Disable all console output.'
)
start.add_argument(
'--no-logging', dest='no_logging', action="store_true",
help='Disable all logging of any kind.'
)
start.add_argument(
'--verbose', nargs="*",
help=('Enable debug output for lbry logger and event loop. Optionally specify loggers for which debug output '
'should selectively be applied.')
)
start.add_argument(
'--initial-headers', dest='initial_headers',
help='Specify path to initial blockchain headers, faster than downloading them on first run.'
)
Config.contribute_to_argparse(start)
start.set_defaults(command='start', start_parser=start, doc=start.format_help())
api = Daemon.get_api_definitions()
groups = {}
for group_name in sorted(api['groups']):
group_parser = sub.add_parser(group_name, group_name=group_name, help=api['groups'][group_name])
groups[group_name] = group_parser.add_subparsers(metavar='COMMAND')
nicer_order = ['stop', 'get', 'publish', 'resolve']
for command_name in sorted(api['commands']):
if command_name not in nicer_order:
nicer_order.append(command_name)
for command_name in nicer_order:
command = api['commands'][command_name]
if command['group'] is None:
add_command_parser(sub, command)
else:
add_command_parser(groups[command['group']], command)
return root
def ensure_directory_exists(path: str):
if not os.path.isdir(path):
pathlib.Path(path).mkdir(parents=True, exist_ok=True)
use_effective_ids = os.access in os.supports_effective_ids
if not os.access(path, os.W_OK, effective_ids=use_effective_ids):
raise PermissionError(f"The following directory is not writable: {path}")
LOG_MODULES = 'lbry', 'aioupnp'
def setup_logging(logger: logging.Logger, args: argparse.Namespace, conf: Config):
default_formatter = logging.Formatter("%(asctime)s %(levelname)-8s %(name)s:%(lineno)d: %(message)s")
file_handler = logging.handlers.RotatingFileHandler(conf.log_file_path, maxBytes=2097152, backupCount=5)
file_handler.setFormatter(default_formatter)
for module_name in LOG_MODULES:
logger.getChild(module_name).addHandler(file_handler)
if not args.quiet:
handler = logging.StreamHandler()
handler.setFormatter(default_formatter)
for module_name in LOG_MODULES:
logger.getChild(module_name).addHandler(handler)
logger.getChild('lbry').setLevel(logging.INFO)
logger.getChild('aioupnp').setLevel(logging.WARNING)
logger.getChild('aiohttp').setLevel(logging.CRITICAL)
if args.verbose is not None:
if len(args.verbose) > 0:
for module in args.verbose:
logger.getChild(module).setLevel(logging.DEBUG)
else:
logger.getChild('lbry').setLevel(logging.DEBUG)
def run_daemon(args: argparse.Namespace, conf: Config):
loop = asyncio.get_event_loop()
if args.verbose is not None:
loop.set_debug(True)
if not args.no_logging:
setup_logging(logging.getLogger(), args, conf)
daemon = Daemon(conf)
def __exit():
raise GracefulExit()
try:
loop.add_signal_handler(signal.SIGINT, __exit)
loop.add_signal_handler(signal.SIGTERM, __exit)
except NotImplementedError:
pass # Not implemented on Windows
try:
loop.run_until_complete(daemon.start())
loop.run_forever()
except (GracefulExit, KeyboardInterrupt, asyncio.CancelledError):
pass
finally:
loop.run_until_complete(daemon.stop())
logging.shutdown()
if hasattr(loop, 'shutdown_asyncgens'):
loop.run_until_complete(loop.shutdown_asyncgens())
def main(argv=None):
argv = argv or sys.argv[1:]
parser = get_argument_parser()
args, command_args = parser.parse_known_args(argv)
conf = Config.create_from_arguments(args)
for directory in (conf.data_dir, conf.download_dir, conf.wallet_dir):
ensure_directory_exists(directory)
if args.cli_version:
print(f"lbrynet {lbrynet_version}")
elif args.command == 'start':
if args.help:
args.start_parser.print_help()
else:
if args.initial_headers:
ledger_path = os.path.join(conf.wallet_dir, 'lbc_mainnet')
ensure_directory_exists(ledger_path)
current_size = 0
headers_path = os.path.join(ledger_path, 'headers')
if os.path.exists(headers_path):
current_size = os.stat(headers_path).st_size
if os.stat(args.initial_headers).st_size > current_size:
log.info('Copying header from %s to %s', args.initial_headers, headers_path)
shutil.copy(args.initial_headers, headers_path)
run_daemon(args, conf)
elif args.command is not None:
doc = args.doc
api_method_name = args.api_method_name
if args.replaced_by:
print(f"{args.api_method_name} is deprecated, using {args.replaced_by['api_method_name']}.")
doc = args.replaced_by['doc']
api_method_name = args.replaced_by['api_method_name']
if args.help:
print(doc)
else:
parsed = docopt(doc, command_args)
params = set_kwargs(parsed)
asyncio.get_event_loop().run_until_complete(execute_command(conf, api_method_name, params))
elif args.group is not None:
args.group_parser.print_help()
else:
parser.print_help()
return 0
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: lbry/extras/daemon/__init__.py
================================================
================================================
FILE: lbry/extras/daemon/analytics.py
================================================
import asyncio
import collections
import logging
import typing
import aiohttp
from lbry import utils
from lbry.conf import Config
from lbry.extras import system_info
ANALYTICS_ENDPOINT = 'https://api.segment.io/v1'
ANALYTICS_TOKEN = 'Ax5LZzR1o3q3Z3WjATASDwR5rKyHH0qOIRIbLmMXn2H='
# Things We Track
SERVER_STARTUP = 'Server Startup'
SERVER_STARTUP_SUCCESS = 'Server Startup Success'
SERVER_STARTUP_ERROR = 'Server Startup Error'
DOWNLOAD_STARTED = 'Download Started'
DOWNLOAD_ERRORED = 'Download Errored'
DOWNLOAD_FINISHED = 'Download Finished'
HEARTBEAT = 'Heartbeat'
DISK_SPACE = 'Disk Space'
CLAIM_ACTION = 'Claim Action' # publish/create/update/abandon
NEW_CHANNEL = 'New Channel'
CREDITS_SENT = 'Credits Sent'
UPNP_SETUP = "UPnP Setup"
BLOB_BYTES_UPLOADED = 'Blob Bytes Uploaded'
TIME_TO_FIRST_BYTES = "Time To First Bytes"
log = logging.getLogger(__name__)
def _event_properties(installation_id: str, session_id: str,
event_properties: typing.Optional[typing.Dict]) -> typing.Dict:
properties = {
'lbry_id': installation_id,
'session_id': session_id,
}
properties.update(event_properties or {})
return properties
def _download_properties(conf: Config, external_ip: str, resolve_duration: float,
total_duration: typing.Optional[float], download_id: str, name: str,
outpoint: str, active_peer_count: typing.Optional[int],
tried_peers_count: typing.Optional[int], connection_failures_count: typing.Optional[int],
added_fixed_peers: bool, fixed_peer_delay: float, sd_hash: str,
sd_download_duration: typing.Optional[float] = None,
head_blob_hash: typing.Optional[str] = None,
head_blob_length: typing.Optional[int] = None,
head_blob_download_duration: typing.Optional[float] = None,
error: typing.Optional[str] = None, error_msg: typing.Optional[str] = None,
wallet_server: typing.Optional[str] = None) -> typing.Dict:
return {
"external_ip": external_ip,
"download_id": download_id,
"total_duration": round(total_duration, 4),
"resolve_duration": None if not resolve_duration else round(resolve_duration, 4),
"error": error,
"error_message": error_msg,
'name': name,
"outpoint": outpoint,
"node_rpc_timeout": conf.node_rpc_timeout,
"peer_connect_timeout": conf.peer_connect_timeout,
"blob_download_timeout": conf.blob_download_timeout,
"use_fixed_peers": len(conf.fixed_peers) > 0,
"fixed_peer_delay": fixed_peer_delay,
"added_fixed_peers": added_fixed_peers,
"active_peer_count": active_peer_count,
"tried_peers_count": tried_peers_count,
"sd_blob_hash": sd_hash,
"sd_blob_duration": None if not sd_download_duration else round(sd_download_duration, 4),
"head_blob_hash": head_blob_hash,
"head_blob_length": head_blob_length,
"head_blob_duration": None if not head_blob_download_duration else round(head_blob_download_duration, 4),
"connection_failures_count": connection_failures_count,
"wallet_server": wallet_server
}
def _make_context(platform):
# see https://segment.com/docs/spec/common/#context
# they say they'll ignore fields outside the spec, but evidently they don't
context = {
'app': {
'version': platform['lbrynet_version'],
'build': platform['build'],
},
# TODO: expand os info to give linux/osx specific info
'os': {
'name': platform['os_system'],
'version': platform['os_release']
},
}
if 'desktop' in platform and 'distro' in platform:
context['os']['desktop'] = platform['desktop']
context['os']['distro'] = platform['distro']
return context
class AnalyticsManager:
def __init__(self, conf: Config, installation_id: str, session_id: str):
self.conf = conf
self.cookies = {}
self.url = ANALYTICS_ENDPOINT
self._write_key = utils.deobfuscate(ANALYTICS_TOKEN)
self._tracked_data = collections.defaultdict(list)
self.context = _make_context(system_info.get_platform())
self.installation_id = installation_id
self.session_id = session_id
self.task: typing.Optional[asyncio.Task] = None
self.external_ip: typing.Optional[str] = None
@property
def enabled(self):
return self.conf.share_usage_data
@property
def is_started(self):
return self.task is not None
async def start(self):
if self.task is None:
self.task = asyncio.create_task(self.run())
async def run(self):
while True:
if self.enabled:
self.external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)
await self._send_heartbeat()
await asyncio.sleep(1800)
def stop(self):
if self.task is not None and not self.task.done():
self.task.cancel()
async def _post(self, data: typing.Dict):
request_kwargs = {
'method': 'POST',
'url': self.url + '/track',
'headers': {'Connection': 'Close'},
'auth': aiohttp.BasicAuth(self._write_key, ''),
'json': data,
'cookies': self.cookies
}
try:
async with utils.aiohttp_request(**request_kwargs) as response:
self.cookies.update(response.cookies)
except Exception as e:
log.debug('Encountered an exception while POSTing to %s: ', self.url + '/track', exc_info=e)
async def track(self, event: typing.Dict):
"""Send a single tracking event"""
if self.enabled:
log.debug('Sending track event: %s', event)
await self._post(event)
async def send_upnp_setup_success_fail(self, success, status):
await self.track(
self._event(UPNP_SETUP, {
'success': success,
'status': status,
})
)
async def send_disk_space_used(self, storage_used, storage_limit, is_from_network_quota):
await self.track(
self._event(DISK_SPACE, {
'used': storage_used,
'limit': storage_limit,
'from_network_quota': is_from_network_quota
})
)
async def send_server_startup(self):
await self.track(self._event(SERVER_STARTUP))
async def send_server_startup_success(self):
await self.track(self._event(SERVER_STARTUP_SUCCESS))
async def send_server_startup_error(self, message):
await self.track(self._event(SERVER_STARTUP_ERROR, {'message': message}))
async def send_time_to_first_bytes(self, resolve_duration: typing.Optional[float],
total_duration: typing.Optional[float], download_id: str,
name: str, outpoint: typing.Optional[str],
found_peers_count: typing.Optional[int],
tried_peers_count: typing.Optional[int],
connection_failures_count: typing.Optional[int],
added_fixed_peers: bool,
fixed_peers_delay: float, sd_hash: str,
sd_download_duration: typing.Optional[float] = None,
head_blob_hash: typing.Optional[str] = None,
head_blob_length: typing.Optional[int] = None,
head_blob_duration: typing.Optional[int] = None,
error: typing.Optional[str] = None,
error_msg: typing.Optional[str] = None,
wallet_server: typing.Optional[str] = None):
await self.track(self._event(TIME_TO_FIRST_BYTES, _download_properties(
self.conf, self.external_ip, resolve_duration, total_duration, download_id, name, outpoint,
found_peers_count, tried_peers_count, connection_failures_count, added_fixed_peers, fixed_peers_delay,
sd_hash, sd_download_duration, head_blob_hash, head_blob_length, head_blob_duration, error, error_msg,
wallet_server
)))
async def send_download_finished(self, download_id, name, sd_hash):
await self.track(
self._event(
DOWNLOAD_FINISHED, {
'download_id': download_id,
'name': name,
'stream_info': sd_hash
}
)
)
async def send_claim_action(self, action):
await self.track(self._event(CLAIM_ACTION, {'action': action}))
async def send_new_channel(self):
await self.track(self._event(NEW_CHANNEL))
async def send_credits_sent(self):
await self.track(self._event(CREDITS_SENT))
async def _send_heartbeat(self):
await self.track(self._event(HEARTBEAT))
def _event(self, event, properties: typing.Optional[typing.Dict] = None):
return {
'userId': 'lbry',
'event': event,
'properties': _event_properties(self.installation_id, self.session_id, properties),
'context': self.context,
'timestamp': utils.isonow()
}
================================================
FILE: lbry/extras/daemon/client.py
================================================
from lbry.extras.cli import execute_command
from lbry.conf import Config
def daemon_rpc(conf: Config, method: str, **kwargs):
return execute_command(conf, method, kwargs, callback=lambda data: data)
================================================
FILE: lbry/extras/daemon/component.py
================================================
import asyncio
import logging
from lbry.conf import Config
from lbry.extras.daemon.componentmanager import ComponentManager
log = logging.getLogger(__name__)
class ComponentType(type):
def __new__(mcs, name, bases, newattrs):
klass = type.__new__(mcs, name, bases, newattrs)
if name != "Component" and newattrs['__module__'] != 'lbry.testcase':
ComponentManager.default_component_classes[klass.component_name] = klass
return klass
class Component(metaclass=ComponentType):
"""
lbry-daemon component helper
Inheriting classes will be automatically registered with the ComponentManager and must implement setup and stop
methods
"""
depends_on = []
component_name = None
def __init__(self, component_manager):
self.conf: Config = component_manager.conf
self.component_manager = component_manager
self._running = False
def __lt__(self, other):
return self.component_name < other.component_name
@property
def running(self):
return self._running
async def get_status(self): # pylint: disable=no-self-use
return
async def start(self):
raise NotImplementedError()
async def stop(self):
raise NotImplementedError()
@property
def component(self):
raise NotImplementedError()
async def _setup(self):
try:
result = await self.start()
self._running = True
return result
except asyncio.CancelledError:
log.info("Cancelled setup of %s component", self.__class__.__name__)
raise
except Exception as err:
log.exception("Error setting up %s", self.component_name or self.__class__.__name__)
raise err
async def _stop(self):
try:
result = await self.stop()
self._running = False
return result
except asyncio.CancelledError:
log.info("Cancelled stop of %s component", self.__class__.__name__)
raise
except Exception as err:
log.exception("Error stopping %s", self.__class__.__name__)
raise err
================================================
FILE: lbry/extras/daemon/componentmanager.py
================================================
import logging
import asyncio
from lbry.conf import Config
from lbry.error import ComponentStartConditionNotMetError
from lbry.dht.peer import PeerManager
log = logging.getLogger(__name__)
class RegisteredConditions:
conditions = {}
class RequiredConditionType(type):
def __new__(mcs, name, bases, newattrs):
klass = type.__new__(mcs, name, bases, newattrs)
if name != "RequiredCondition":
if klass.name in RegisteredConditions.conditions:
raise SyntaxError("already have a component registered for \"%s\"" % klass.name)
RegisteredConditions.conditions[klass.name] = klass
return klass
class RequiredCondition(metaclass=RequiredConditionType):
name = ""
component = ""
message = ""
@staticmethod
def evaluate(component):
raise NotImplementedError()
class ComponentManager:
default_component_classes = {}
def __init__(self, conf: Config, analytics_manager=None, skip_components=None,
peer_manager=None, **override_components):
self.conf = conf
self.skip_components = skip_components or []
self.loop = asyncio.get_event_loop()
self.analytics_manager = analytics_manager
self.component_classes = {}
self.components = set()
self.started = asyncio.Event()
self.peer_manager = peer_manager or PeerManager(asyncio.get_event_loop_policy().get_event_loop())
for component_name, component_class in self.default_component_classes.items():
if component_name in override_components:
component_class = override_components.pop(component_name)
if component_name not in self.skip_components:
self.component_classes[component_name] = component_class
if override_components:
raise SyntaxError("unexpected components: %s" % override_components)
for component_class in self.component_classes.values():
self.components.add(component_class(self))
def evaluate_condition(self, condition_name):
if condition_name not in RegisteredConditions.conditions:
raise NameError(condition_name)
condition = RegisteredConditions.conditions[condition_name]
try:
component = self.get_component(condition.component)
result = condition.evaluate(component)
except Exception:
log.exception('failed to evaluate condition:')
result = False
return result, "" if result else condition.message
def sort_components(self, reverse=False):
"""
Sort components by requirements
"""
steps = []
staged = set()
components = set(self.components)
# components with no requirements
step = []
for component in set(components):
if not component.depends_on:
step.append(component)
staged.add(component.component_name)
components.remove(component)
if step:
step.sort()
steps.append(step)
while components:
step = []
to_stage = set()
for component in set(components):
reqs_met = 0
for needed in component.depends_on:
if needed in staged:
reqs_met += 1
if reqs_met == len(component.depends_on):
step.append(component)
to_stage.add(component.component_name)
components.remove(component)
if step:
step.sort()
staged.update(to_stage)
steps.append(step)
elif components:
raise ComponentStartConditionNotMetError(components)
if reverse:
steps.reverse()
return steps
async def start(self):
""" Start Components in sequence sorted by requirements """
for stage in self.sort_components():
needing_start = [
component._setup() for component in stage if not component.running
]
if needing_start:
await asyncio.wait(map(asyncio.create_task, needing_start))
self.started.set()
async def stop(self):
"""
Stop Components in reversed startup order
"""
stages = self.sort_components(reverse=True)
for stage in stages:
needing_stop = [
component._stop() for component in stage if component.running
]
if needing_stop:
await asyncio.wait(map(asyncio.create_task, needing_stop))
def all_components_running(self, *component_names):
"""
Check if components are running
:return: (bool) True if all specified components are running
"""
components = {component.component_name: component for component in self.components}
for component in component_names:
if component not in components:
raise NameError("%s is not a known Component" % component)
if not components[component].running:
return False
return True
def get_components_status(self):
"""
List status of all the components, whether they are running or not
:return: (dict) {(str) component_name: (bool) True is running else False}
"""
return {
component.component_name: component.running
for component in self.components
}
def get_actual_component(self, component_name):
for component in self.components:
if component.component_name == component_name:
return component
raise NameError(component_name)
def get_component(self, component_name):
return self.get_actual_component(component_name).component
def has_component(self, component_name):
return any(component for component in self.components if component_name == component.component_name)
================================================
FILE: lbry/extras/daemon/components.py
================================================
import math
import os
import asyncio
import logging
import binascii
import typing
import base58
from aioupnp import __version__ as aioupnp_version
from aioupnp.upnp import UPnP
from aioupnp.fault import UPnPError
from lbry import utils
from lbry.dht.node import Node
from lbry.dht.peer import is_valid_public_ipv4
from lbry.dht.blob_announcer import BlobAnnouncer
from lbry.blob.blob_manager import BlobManager
from lbry.blob.disk_space_manager import DiskSpaceManager
from lbry.blob_exchange.server import BlobServer
from lbry.stream.background_downloader import BackgroundDownloader
from lbry.stream.stream_manager import StreamManager
from lbry.file.file_manager import FileManager
from lbry.extras.daemon.component import Component
from lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager
from lbry.extras.daemon.storage import SQLiteStorage
from lbry.torrent.torrent_manager import TorrentManager
from lbry.wallet import WalletManager
from lbry.wallet.usage_payment import WalletServerPayer
from lbry.torrent.tracker import TrackerClient
from lbry.torrent.session import TorrentSession
log = logging.getLogger(__name__)
# settings must be initialized before this file is imported
DATABASE_COMPONENT = "database"
BLOB_COMPONENT = "blob_manager"
WALLET_COMPONENT = "wallet"
WALLET_SERVER_PAYMENTS_COMPONENT = "wallet_server_payments"
DHT_COMPONENT = "dht"
HASH_ANNOUNCER_COMPONENT = "hash_announcer"
FILE_MANAGER_COMPONENT = "file_manager"
DISK_SPACE_COMPONENT = "disk_space"
BACKGROUND_DOWNLOADER_COMPONENT = "background_downloader"
PEER_PROTOCOL_SERVER_COMPONENT = "peer_protocol_server"
UPNP_COMPONENT = "upnp"
EXCHANGE_RATE_MANAGER_COMPONENT = "exchange_rate_manager"
TRACKER_ANNOUNCER_COMPONENT = "tracker_announcer_component"
LIBTORRENT_COMPONENT = "libtorrent_component"
class DatabaseComponent(Component):
component_name = DATABASE_COMPONENT
def __init__(self, component_manager):
super().__init__(component_manager)
self.storage = None
@property
def component(self):
return self.storage
@staticmethod
def get_current_db_revision():
return 15
@property
def revision_filename(self):
return os.path.join(self.conf.data_dir, 'db_revision')
def _write_db_revision_file(self, version_num):
with open(self.revision_filename, mode='w') as db_revision:
db_revision.write(str(version_num))
async def start(self):
# check directories exist, create them if they don't
log.info("Loading databases")
if not os.path.exists(self.revision_filename):
log.info("db_revision file not found. Creating it")
self._write_db_revision_file(self.get_current_db_revision())
# check the db migration and run any needed migrations
with open(self.revision_filename, "r") as revision_read_handle:
old_revision = int(revision_read_handle.read().strip())
if old_revision > self.get_current_db_revision():
raise Exception('This version of lbrynet is not compatible with the database\n'
'Your database is revision %i, expected %i' %
(old_revision, self.get_current_db_revision()))
if old_revision < self.get_current_db_revision():
from lbry.extras.daemon.migrator import dbmigrator # pylint: disable=import-outside-toplevel
log.info("Upgrading your databases (revision %i to %i)", old_revision, self.get_current_db_revision())
await asyncio.get_event_loop().run_in_executor(
None, dbmigrator.migrate_db, self.conf, old_revision, self.get_current_db_revision()
)
self._write_db_revision_file(self.get_current_db_revision())
log.info("Finished upgrading the databases.")
self.storage = SQLiteStorage(
self.conf, os.path.join(self.conf.data_dir, "lbrynet.sqlite")
)
await self.storage.open()
async def stop(self):
await self.storage.close()
self.storage = None
class WalletComponent(Component):
component_name = WALLET_COMPONENT
depends_on = [DATABASE_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.wallet_manager = None
@property
def component(self):
return self.wallet_manager
async def get_status(self):
if self.wallet_manager is None:
return
is_connected = self.wallet_manager.ledger.network.is_connected
sessions = []
connected = None
if is_connected:
addr, port = self.wallet_manager.ledger.network.client.server
connected = f"{addr}:{port}"
sessions.append(self.wallet_manager.ledger.network.client)
result = {
'connected': connected,
'connected_features': self.wallet_manager.ledger.network.server_features,
'servers': [
{
'host': session.server[0],
'port': session.server[1],
'latency': session.connection_latency,
'availability': session.available,
} for session in sessions
],
'known_servers': len(self.wallet_manager.ledger.network.known_hubs),
'available_servers': 1 if is_connected else 0
}
if self.wallet_manager.ledger.network.remote_height:
local_height = self.wallet_manager.ledger.local_height_including_downloaded_height
disk_height = len(self.wallet_manager.ledger.headers)
remote_height = self.wallet_manager.ledger.network.remote_height
download_height, target_height = local_height - disk_height, remote_height - disk_height
if target_height > 0:
progress = min(max(math.ceil(float(download_height) / float(target_height) * 100), 0), 100)
else:
progress = 100
best_hash = await self.wallet_manager.get_best_blockhash()
result.update({
'headers_synchronization_progress': progress,
'blocks': max(local_height, 0),
'blocks_behind': max(remote_height - local_height, 0),
'best_blockhash': best_hash,
})
return result
async def start(self):
log.info("Starting wallet")
self.wallet_manager = await WalletManager.from_lbrynet_config(self.conf)
await self.wallet_manager.start()
async def stop(self):
await self.wallet_manager.stop()
self.wallet_manager = None
class WalletServerPaymentsComponent(Component):
component_name = WALLET_SERVER_PAYMENTS_COMPONENT
depends_on = [WALLET_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.usage_payment_service = WalletServerPayer(
max_fee=self.conf.max_wallet_server_fee, analytics_manager=self.component_manager.analytics_manager,
)
@property
def component(self) -> typing.Optional[WalletServerPayer]:
return self.usage_payment_service
async def start(self):
wallet_manager = self.component_manager.get_component(WALLET_COMPONENT)
await self.usage_payment_service.start(wallet_manager.ledger, wallet_manager.default_wallet)
async def stop(self):
await self.usage_payment_service.stop()
async def get_status(self):
return {
'max_fee': self.usage_payment_service.max_fee,
'running': self.usage_payment_service.running
}
class BlobComponent(Component):
component_name = BLOB_COMPONENT
depends_on = [DATABASE_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.blob_manager: typing.Optional[BlobManager] = None
@property
def component(self) -> typing.Optional[BlobManager]:
return self.blob_manager
async def start(self):
storage = self.component_manager.get_component(DATABASE_COMPONENT)
data_store = None
if DHT_COMPONENT not in self.component_manager.skip_components:
dht_node: Node = self.component_manager.get_component(DHT_COMPONENT)
if dht_node:
data_store = dht_node.protocol.data_store
blob_dir = os.path.join(self.conf.data_dir, 'blobfiles')
if not os.path.isdir(blob_dir):
os.mkdir(blob_dir)
self.blob_manager = BlobManager(self.component_manager.loop, blob_dir, storage, self.conf, data_store)
return await self.blob_manager.setup()
async def stop(self):
self.blob_manager.stop()
async def get_status(self):
count = 0
if self.blob_manager:
count = len(self.blob_manager.completed_blob_hashes)
return {
'finished_blobs': count,
'connections': {} if not self.blob_manager else self.blob_manager.connection_manager.status
}
class DHTComponent(Component):
component_name = DHT_COMPONENT
depends_on = [UPNP_COMPONENT, DATABASE_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.dht_node: typing.Optional[Node] = None
self.external_udp_port = None
self.external_peer_port = None
@property
def component(self) -> typing.Optional[Node]:
return self.dht_node
async def get_status(self):
return {
'node_id': None if not self.dht_node else binascii.hexlify(self.dht_node.protocol.node_id),
'peers_in_routing_table': 0 if not self.dht_node else len(self.dht_node.protocol.routing_table.get_peers())
}
def get_node_id(self):
node_id_filename = os.path.join(self.conf.data_dir, "node_id")
if os.path.isfile(node_id_filename):
with open(node_id_filename, "r") as node_id_file:
return base58.b58decode(str(node_id_file.read()).strip())
node_id = utils.generate_id()
with open(node_id_filename, "w") as node_id_file:
node_id_file.write(base58.b58encode(node_id).decode())
return node_id
async def start(self):
log.info("start the dht")
upnp_component = self.component_manager.get_component(UPNP_COMPONENT)
self.external_peer_port = upnp_component.upnp_redirects.get("TCP", self.conf.tcp_port)
self.external_udp_port = upnp_component.upnp_redirects.get("UDP", self.conf.udp_port)
external_ip = upnp_component.external_ip
storage = self.component_manager.get_component(DATABASE_COMPONENT)
if not external_ip:
external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)
if not external_ip:
log.warning("failed to get external ip")
self.dht_node = Node(
self.component_manager.loop,
self.component_manager.peer_manager,
node_id=self.get_node_id(),
internal_udp_port=self.conf.udp_port,
udp_port=self.external_udp_port,
external_ip=external_ip,
peer_port=self.external_peer_port,
rpc_timeout=self.conf.node_rpc_timeout,
split_buckets_under_index=self.conf.split_buckets_under_index,
is_bootstrap_node=self.conf.is_bootstrap_node,
storage=storage
)
self.dht_node.start(self.conf.network_interface, self.conf.known_dht_nodes)
log.info("Started the dht")
async def stop(self):
self.dht_node.stop()
class HashAnnouncerComponent(Component):
component_name = HASH_ANNOUNCER_COMPONENT
depends_on = [DHT_COMPONENT, DATABASE_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.hash_announcer: typing.Optional[BlobAnnouncer] = None
@property
def component(self) -> typing.Optional[BlobAnnouncer]:
return self.hash_announcer
async def start(self):
storage = self.component_manager.get_component(DATABASE_COMPONENT)
dht_node = self.component_manager.get_component(DHT_COMPONENT)
self.hash_announcer = BlobAnnouncer(self.component_manager.loop, dht_node, storage)
self.hash_announcer.start(self.conf.concurrent_blob_announcers)
log.info("Started blob announcer")
async def stop(self):
self.hash_announcer.stop()
log.info("Stopped blob announcer")
async def get_status(self):
return {
'announce_queue_size': 0 if not self.hash_announcer else len(self.hash_announcer.announce_queue)
}
class FileManagerComponent(Component):
component_name = FILE_MANAGER_COMPONENT
depends_on = [BLOB_COMPONENT, DATABASE_COMPONENT, WALLET_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.file_manager: typing.Optional[FileManager] = None
@property
def component(self) -> typing.Optional[FileManager]:
return self.file_manager
async def get_status(self):
if not self.file_manager:
return
return {
'managed_files': len(self.file_manager.get_filtered()),
}
async def start(self):
blob_manager = self.component_manager.get_component(BLOB_COMPONENT)
storage = self.component_manager.get_component(DATABASE_COMPONENT)
wallet = self.component_manager.get_component(WALLET_COMPONENT)
node = self.component_manager.get_component(DHT_COMPONENT) \
if self.component_manager.has_component(DHT_COMPONENT) else None
log.info('Starting the file manager')
loop = asyncio.get_event_loop()
self.file_manager = FileManager(
loop, self.conf, wallet, storage, self.component_manager.analytics_manager
)
self.file_manager.source_managers['stream'] = StreamManager(
loop, self.conf, blob_manager, wallet, storage, node,
)
if self.component_manager.has_component(LIBTORRENT_COMPONENT):
torrent = self.component_manager.get_component(LIBTORRENT_COMPONENT)
self.file_manager.source_managers['torrent'] = TorrentManager(
loop, self.conf, torrent, storage, self.component_manager.analytics_manager
)
await self.file_manager.start()
log.info('Done setting up file manager')
async def stop(self):
await self.file_manager.stop()
class BackgroundDownloaderComponent(Component):
MIN_PREFIX_COLLIDING_BITS = 8
component_name = BACKGROUND_DOWNLOADER_COMPONENT
depends_on = [DATABASE_COMPONENT, BLOB_COMPONENT, DISK_SPACE_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.background_task: typing.Optional[asyncio.Task] = None
self.download_loop_delay_seconds = 60
self.ongoing_download: typing.Optional[asyncio.Task] = None
self.space_manager: typing.Optional[DiskSpaceManager] = None
self.blob_manager: typing.Optional[BlobManager] = None
self.background_downloader: typing.Optional[BackgroundDownloader] = None
self.dht_node: typing.Optional[Node] = None
self.space_available: typing.Optional[int] = None
@property
def is_busy(self):
return bool(self.ongoing_download and not self.ongoing_download.done())
@property
def component(self) -> 'BackgroundDownloaderComponent':
return self
async def get_status(self):
return {'running': self.background_task is not None and not self.background_task.done(),
'available_free_space_mb': self.space_available,
'ongoing_download': self.is_busy}
async def download_blobs_in_background(self):
while True:
self.space_available = await self.space_manager.get_free_space_mb(True)
if not self.is_busy and self.space_available > 10:
self._download_next_close_blob_hash()
await asyncio.sleep(self.download_loop_delay_seconds)
def _download_next_close_blob_hash(self):
node_id = self.dht_node.protocol.node_id
for blob_hash in self.dht_node.stored_blob_hashes:
if blob_hash.hex() in self.blob_manager.completed_blob_hashes:
continue
if utils.get_colliding_prefix_bits(node_id, blob_hash) >= self.MIN_PREFIX_COLLIDING_BITS:
self.ongoing_download = asyncio.create_task(self.background_downloader.download_blobs(blob_hash.hex()))
return
async def start(self):
self.space_manager: DiskSpaceManager = self.component_manager.get_component(DISK_SPACE_COMPONENT)
if not self.component_manager.has_component(DHT_COMPONENT):
return
self.dht_node = self.component_manager.get_component(DHT_COMPONENT)
self.blob_manager = self.component_manager.get_component(BLOB_COMPONENT)
storage = self.component_manager.get_component(DATABASE_COMPONENT)
self.background_downloader = BackgroundDownloader(self.conf, storage, self.blob_manager, self.dht_node)
self.background_task = asyncio.create_task(self.download_blobs_in_background())
async def stop(self):
if self.ongoing_download and not self.ongoing_download.done():
self.ongoing_download.cancel()
if self.background_task:
self.background_task.cancel()
class DiskSpaceComponent(Component):
component_name = DISK_SPACE_COMPONENT
depends_on = [DATABASE_COMPONENT, BLOB_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.disk_space_manager: typing.Optional[DiskSpaceManager] = None
@property
def component(self) -> typing.Optional[DiskSpaceManager]:
return self.disk_space_manager
async def get_status(self):
if self.disk_space_manager:
space_used = await self.disk_space_manager.get_space_used_mb(cached=True)
return {
'total_used_mb': space_used['total'],
'published_blobs_storage_used_mb': space_used['private_storage'],
'content_blobs_storage_used_mb': space_used['content_storage'],
'seed_blobs_storage_used_mb': space_used['network_storage'],
'running': self.disk_space_manager.running,
}
return {'space_used': '0', 'network_seeding_space_used': '0', 'running': False}
async def start(self):
db = self.component_manager.get_component(DATABASE_COMPONENT)
blob_manager = self.component_manager.get_component(BLOB_COMPONENT)
self.disk_space_manager = DiskSpaceManager(
self.conf, db, blob_manager,
analytics=self.component_manager.analytics_manager
)
await self.disk_space_manager.start()
async def stop(self):
await self.disk_space_manager.stop()
class TorrentComponent(Component):
component_name = LIBTORRENT_COMPONENT
def __init__(self, component_manager):
super().__init__(component_manager)
self.torrent_session = None
@property
def component(self) -> typing.Optional[TorrentSession]:
return self.torrent_session
async def get_status(self):
if not self.torrent_session:
return
return {
'running': True, # TODO: what to return here?
}
async def start(self):
self.torrent_session = TorrentSession(asyncio.get_event_loop(), None)
await self.torrent_session.bind() # TODO: specify host/port
async def stop(self):
if self.torrent_session:
await self.torrent_session.pause()
class PeerProtocolServerComponent(Component):
component_name = PEER_PROTOCOL_SERVER_COMPONENT
depends_on = [UPNP_COMPONENT, BLOB_COMPONENT, WALLET_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.blob_server: typing.Optional[BlobServer] = None
@property
def component(self) -> typing.Optional[BlobServer]:
return self.blob_server
async def start(self):
log.info("start blob server")
blob_manager: BlobManager = self.component_manager.get_component(BLOB_COMPONENT)
wallet: WalletManager = self.component_manager.get_component(WALLET_COMPONENT)
peer_port = self.conf.tcp_port
address = await wallet.get_unused_address()
self.blob_server = BlobServer(asyncio.get_event_loop(), blob_manager, address)
self.blob_server.start_server(peer_port, interface=self.conf.network_interface)
await self.blob_server.started_listening.wait()
async def stop(self):
if self.blob_server:
self.blob_server.stop_server()
class UPnPComponent(Component):
component_name = UPNP_COMPONENT
def __init__(self, component_manager):
super().__init__(component_manager)
self._int_peer_port = self.conf.tcp_port
self._int_dht_node_port = self.conf.udp_port
self.use_upnp = self.conf.use_upnp
self.upnp: typing.Optional[UPnP] = None
self.upnp_redirects = {}
self.external_ip: typing.Optional[str] = None
self._maintain_redirects_task = None
@property
def component(self) -> 'UPnPComponent':
return self
async def _repeatedly_maintain_redirects(self, now=True):
while True:
if now:
await self._maintain_redirects()
await asyncio.sleep(360)
async def _maintain_redirects(self):
# setup the gateway if necessary
if not self.upnp:
try:
self.upnp = await UPnP.discover(loop=self.component_manager.loop)
log.info("found upnp gateway: %s", self.upnp.gateway.manufacturer_string)
except Exception as err:
log.warning("upnp discovery failed: %s", err)
self.upnp = None
# update the external ip
external_ip = None
if self.upnp:
try:
external_ip = await self.upnp.get_external_ip()
if external_ip != "0.0.0.0" and not self.external_ip:
log.info("got external ip from UPnP: %s", external_ip)
except (asyncio.TimeoutError, UPnPError, NotImplementedError):
pass
if external_ip and not is_valid_public_ipv4(external_ip):
log.warning("UPnP returned a private/reserved ip - %s, checking lbry.com fallback", external_ip)
external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)
if self.external_ip and self.external_ip != external_ip:
log.info("external ip changed from %s to %s", self.external_ip, external_ip)
if external_ip:
self.external_ip = external_ip
dht_component = self.component_manager.get_component(DHT_COMPONENT)
if dht_component:
dht_node = dht_component.component
dht_node.protocol.external_ip = external_ip
# assert self.external_ip is not None # TODO: handle going/starting offline
if not self.upnp_redirects and self.upnp: # setup missing redirects
log.info("add UPnP port mappings")
upnp_redirects = {}
if PEER_PROTOCOL_SERVER_COMPONENT not in self.component_manager.skip_components:
try:
upnp_redirects["TCP"] = await self.upnp.get_next_mapping(
self._int_peer_port, "TCP", "LBRY peer port", self._int_peer_port
)
except (UPnPError, asyncio.TimeoutError, NotImplementedError):
pass
if DHT_COMPONENT not in self.component_manager.skip_components:
try:
upnp_redirects["UDP"] = await self.upnp.get_next_mapping(
self._int_dht_node_port, "UDP", "LBRY DHT port", self._int_dht_node_port
)
except (UPnPError, asyncio.TimeoutError, NotImplementedError):
pass
if upnp_redirects:
log.info("set up redirects: %s", upnp_redirects)
self.upnp_redirects.update(upnp_redirects)
elif self.upnp: # check existing redirects are still active
found = set()
mappings = await self.upnp.get_redirects()
for mapping in mappings:
proto = mapping.protocol
if proto in self.upnp_redirects and mapping.external_port == self.upnp_redirects[proto]:
if mapping.lan_address == self.upnp.lan_address:
found.add(proto)
if 'UDP' not in found and DHT_COMPONENT not in self.component_manager.skip_components:
try:
udp_port = await self.upnp.get_next_mapping(self._int_dht_node_port, "UDP", "LBRY DHT port")
self.upnp_redirects['UDP'] = udp_port
log.info("refreshed upnp redirect for dht port: %i", udp_port)
except (asyncio.TimeoutError, UPnPError, NotImplementedError):
del self.upnp_redirects['UDP']
if 'TCP' not in found and PEER_PROTOCOL_SERVER_COMPONENT not in self.component_manager.skip_components:
try:
tcp_port = await self.upnp.get_next_mapping(self._int_peer_port, "TCP", "LBRY peer port")
self.upnp_redirects['TCP'] = tcp_port
log.info("refreshed upnp redirect for peer port: %i", tcp_port)
except (asyncio.TimeoutError, UPnPError, NotImplementedError):
del self.upnp_redirects['TCP']
if ('TCP' in self.upnp_redirects and
PEER_PROTOCOL_SERVER_COMPONENT not in self.component_manager.skip_components) and \
('UDP' in self.upnp_redirects and DHT_COMPONENT not in self.component_manager.skip_components):
if self.upnp_redirects:
log.debug("upnp redirects are still active")
async def start(self):
log.info("detecting external ip")
if not self.use_upnp:
self.external_ip, _ = await utils.get_external_ip(self.conf.lbryum_servers)
return
success = False
await self._maintain_redirects()
if self.upnp:
if not self.upnp_redirects and not all(
x in self.component_manager.skip_components
for x in (DHT_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT)
):
log.error("failed to setup upnp")
else:
success = True
if self.upnp_redirects:
log.debug("set up upnp port redirects for gateway: %s", self.upnp.gateway.manufacturer_string)
else:
log.error("failed to setup upnp")
if not self.external_ip:
self.external_ip, probed_url = await utils.get_external_ip(self.conf.lbryum_servers)
if self.external_ip:
log.info("detected external ip using %s fallback", probed_url)
if self.component_manager.analytics_manager:
self.component_manager.loop.create_task(
self.component_manager.analytics_manager.send_upnp_setup_success_fail(
success, await self.get_status()
)
)
self._maintain_redirects_task = self.component_manager.loop.create_task(
self._repeatedly_maintain_redirects(now=False)
)
async def stop(self):
if self.upnp_redirects:
log.info("Removing upnp redirects: %s", self.upnp_redirects)
await asyncio.wait([
self.upnp.delete_port_mapping(port, protocol) for protocol, port in self.upnp_redirects.items()
])
if self._maintain_redirects_task and not self._maintain_redirects_task.done():
self._maintain_redirects_task.cancel()
async def get_status(self):
return {
'aioupnp_version': aioupnp_version,
'redirects': self.upnp_redirects,
'gateway': 'No gateway found' if not self.upnp else self.upnp.gateway.manufacturer_string,
'dht_redirect_set': 'UDP' in self.upnp_redirects,
'peer_redirect_set': 'TCP' in self.upnp_redirects,
'external_ip': self.external_ip
}
class ExchangeRateManagerComponent(Component):
component_name = EXCHANGE_RATE_MANAGER_COMPONENT
def __init__(self, component_manager):
super().__init__(component_manager)
self.exchange_rate_manager = ExchangeRateManager()
@property
def component(self) -> ExchangeRateManager:
return self.exchange_rate_manager
async def start(self):
self.exchange_rate_manager.start()
async def stop(self):
self.exchange_rate_manager.stop()
class TrackerAnnouncerComponent(Component):
component_name = TRACKER_ANNOUNCER_COMPONENT
depends_on = [FILE_MANAGER_COMPONENT]
def __init__(self, component_manager):
super().__init__(component_manager)
self.file_manager = None
self.announce_task = None
self.tracker_client: typing.Optional[TrackerClient] = None
@property
def component(self):
return self.tracker_client
@property
def running(self):
return self._running and self.announce_task and not self.announce_task.done()
async def announce_forever(self):
while True:
sleep_seconds = 60.0
announce_sd_hashes = []
for file in self.file_manager.get_filtered():
if not file.downloader:
continue
announce_sd_hashes.append(bytes.fromhex(file.sd_hash))
await self.tracker_client.announce_many(*announce_sd_hashes)
await asyncio.sleep(sleep_seconds)
async def start(self):
node = self.component_manager.get_component(DHT_COMPONENT) \
if self.component_manager.has_component(DHT_COMPONENT) else None
node_id = node.protocol.node_id if node else None
self.tracker_client = TrackerClient(node_id, self.conf.tcp_port, lambda: self.conf.tracker_servers)
await self.tracker_client.start()
self.file_manager = self.component_manager.get_component(FILE_MANAGER_COMPONENT)
self.announce_task = asyncio.create_task(self.announce_forever())
async def stop(self):
self.file_manager = None
if self.announce_task and not self.announce_task.done():
self.announce_task.cancel()
self.announce_task = None
self.tracker_client.stop()
================================================
FILE: lbry/extras/daemon/daemon.py
================================================
import linecache
import os
import re
import asyncio
import logging
import json
import time
import inspect
import typing
import random
import tracemalloc
import itertools
from urllib.parse import urlencode, quote
from typing import Callable, Optional, List
from binascii import hexlify, unhexlify
from traceback import format_exc
from functools import wraps, partial
import base58
from aiohttp import web
from prometheus_client import generate_latest as prom_generate_latest, Gauge, Histogram, Counter
from google.protobuf.message import DecodeError
from lbry.wallet import (
Wallet, ENCRYPT_ON_DISK, SingleKey, HierarchicalDeterministic,
Transaction, Output, Input, Account, database
)
from lbry.wallet.dewies import dewies_to_lbc, lbc_to_dewies, dict_values_to_lbc
from lbry.wallet.constants import TXO_TYPES, CLAIM_TYPE_NAMES
from lbry.wallet.bip32 import PrivateKey
from lbry.crypto.base58 import Base58
from lbry import utils
from lbry.conf import Config, Setting, NOT_SET
from lbry.blob.blob_file import is_valid_blobhash, BlobBuffer
from lbry.blob_exchange.downloader import download_blob
from lbry.dht.peer import make_kademlia_peer
from lbry.error import (
DownloadSDTimeoutError, ComponentsNotStartedError, ComponentStartConditionNotMetError,
CommandDoesNotExistError, BaseError, WalletNotFoundError, WalletAlreadyLoadedError, WalletAlreadyExistsError,
ConflictingInputValueError, AlreadyPurchasedError, PrivateKeyNotFoundError, InputStringIsBlankError,
InputValueError
)
from lbry.extras import system_info
from lbry.extras.daemon import analytics
from lbry.extras.daemon.components import WALLET_COMPONENT, DATABASE_COMPONENT, DHT_COMPONENT, BLOB_COMPONENT
from lbry.extras.daemon.components import FILE_MANAGER_COMPONENT, DISK_SPACE_COMPONENT, TRACKER_ANNOUNCER_COMPONENT
from lbry.extras.daemon.components import EXCHANGE_RATE_MANAGER_COMPONENT, UPNP_COMPONENT
from lbry.extras.daemon.componentmanager import RequiredCondition
from lbry.extras.daemon.componentmanager import ComponentManager
from lbry.extras.daemon.json_response_encoder import JSONResponseEncoder
from lbry.extras.daemon.undecorated import undecorated
from lbry.extras.daemon.security import ensure_request_allowed
from lbry.file_analysis import VideoFileAnalyzer
from lbry.schema.claim import Claim
from lbry.schema.url import URL
if typing.TYPE_CHECKING:
from lbry.blob.blob_manager import BlobManager
from lbry.dht.node import Node
from lbry.extras.daemon.components import UPnPComponent, DiskSpaceManager
from lbry.extras.daemon.exchange_rate_manager import ExchangeRateManager
from lbry.extras.daemon.storage import SQLiteStorage
from lbry.wallet import WalletManager, Ledger
from lbry.file.file_manager import FileManager
log = logging.getLogger(__name__)
RANGE_FIELDS = {
'height', 'creation_height', 'activation_height', 'expiration_height',
'timestamp', 'creation_timestamp', 'duration', 'release_time', 'fee_amount',
'tx_position', 'repost_count', 'limit_claims_per_channel',
'amount', 'effective_amount', 'support_amount',
'trending_score', 'censor_type', 'tx_num'
}
MY_RANGE_FIELDS = RANGE_FIELDS - {"limit_claims_per_channel"}
REPLACEMENTS = {
'claim_name': 'normalized_name',
'name': 'normalized_name',
'txid': 'tx_id',
'nout': 'tx_nout',
'trending_group': 'trending_score',
'trending_mixed': 'trending_score',
'trending_global': 'trending_score',
'trending_local': 'trending_score',
'reposted': 'repost_count',
'stream_types': 'stream_type',
'media_types': 'media_type',
'valid_channel_signature': 'is_signature_valid'
}
def is_transactional_function(name):
for action in ('create', 'update', 'abandon', 'send', 'fund'):
if action in name:
return True
def requires(*components, **conditions):
if conditions and ["conditions"] != list(conditions.keys()):
raise SyntaxError("invalid conditions argument")
condition_names = conditions.get("conditions", [])
def _wrap(method):
@wraps(method)
def _inner(*args, **kwargs):
component_manager = args[0].component_manager
for condition_name in condition_names:
condition_result, err_msg = component_manager.evaluate_condition(condition_name)
if not condition_result:
raise ComponentStartConditionNotMetError(err_msg)
if not component_manager.all_components_running(*components):
raise ComponentsNotStartedError(
f"the following required components have not yet started: {json.dumps(components)}"
)
return method(*args, **kwargs)
return _inner
return _wrap
def deprecated(new_command=None):
def _deprecated_wrapper(f):
f.new_command = new_command
f._deprecated = True
return f
return _deprecated_wrapper
INITIALIZING_CODE = 'initializing'
# TODO: make this consistent with the stages in Downloader.py
DOWNLOAD_METADATA_CODE = 'downloading_metadata'
DOWNLOAD_TIMEOUT_CODE = 'timeout'
DOWNLOAD_RUNNING_CODE = 'running'
DOWNLOAD_STOPPED_CODE = 'stopped'
STREAM_STAGES = [
(INITIALIZING_CODE, 'Initializing'),
(DOWNLOAD_METADATA_CODE, 'Downloading metadata'),
(DOWNLOAD_RUNNING_CODE, 'Started %s, got %s/%s blobs, stream status: %s'),
(DOWNLOAD_STOPPED_CODE, 'Paused stream'),
(DOWNLOAD_TIMEOUT_CODE, 'Stream timed out')
]
SHORT_ID_LEN = 20
MAX_UPDATE_FEE_ESTIMATE = 0.3
DEFAULT_PAGE_SIZE = 20
VALID_FULL_CLAIM_ID = re.compile('[0-9a-fA-F]{40}')
def encode_pagination_doc(items):
return {
"page": "Page number of the current items.",
"page_size": "Number of items to show on a page.",
"total_pages": "Total number of pages.",
"total_items": "Total number of items.",
"items": [items],
}
async def paginate_rows(get_records: Callable, get_record_count: Optional[Callable],
page: Optional[int], page_size: Optional[int], **constraints):
page = max(1, page or 1)
page_size = max(1, page_size or DEFAULT_PAGE_SIZE)
constraints.update({
"offset": page_size * (page - 1),
"limit": page_size
})
items = await get_records(**constraints)
result = {"items": items, "page": page, "page_size": page_size}
if get_record_count is not None:
total_items = await get_record_count(**constraints)
result["total_pages"] = int((total_items + (page_size - 1)) / page_size)
result["total_items"] = total_items
return result
def paginate_list(items: List, page: Optional[int], page_size: Optional[int]):
page = max(1, page or 1)
page_size = max(1, page_size or DEFAULT_PAGE_SIZE)
total_items = len(items)
offset = page_size * (page - 1)
subitems = []
if offset <= total_items:
subitems = items[offset:offset+page_size]
return {
"items": subitems,
"total_pages": int((total_items + (page_size - 1)) / page_size),
"total_items": total_items,
"page": page, "page_size": page_size
}
DHT_HAS_CONTACTS = "dht_has_contacts"
class DHTHasContacts(RequiredCondition):
name = DHT_HAS_CONTACTS
component = DHT_COMPONENT
message = "your node is not connected to the dht"
@staticmethod
def evaluate(component):
return len(component.contacts) > 0
class JSONRPCError:
# http://www.jsonrpc.org/specification#error_object
CODE_PARSE_ERROR = -32700 # Invalid JSON. Error while parsing the JSON text.
CODE_INVALID_REQUEST = -32600 # The JSON sent is not a valid Request object.
CODE_METHOD_NOT_FOUND = -32601 # The method does not exist / is not available.
CODE_INVALID_PARAMS = -32602 # Invalid method parameter(s).
CODE_INTERNAL_ERROR = -32603 # Internal JSON-RPC error (I think this is like a 500?)
CODE_APPLICATION_ERROR = -32500 # Generic error with our app??
CODE_AUTHENTICATION_ERROR = -32501 # Authentication failed
MESSAGES = {
CODE_PARSE_ERROR: "Parse Error. Data is not valid JSON.",
CODE_INVALID_REQUEST: "JSON data is not a valid Request",
CODE_METHOD_NOT_FOUND: "Method Not Found",
CODE_INVALID_PARAMS: "Invalid Params",
CODE_INTERNAL_ERROR: "Internal Error",
CODE_AUTHENTICATION_ERROR: "Authentication Failed",
}
HTTP_CODES = {
CODE_INVALID_REQUEST: 400,
CODE_PARSE_ERROR: 400,
CODE_INVALID_PARAMS: 400,
CODE_METHOD_NOT_FOUND: 404,
CODE_INTERNAL_ERROR: 500,
CODE_APPLICATION_ERROR: 500,
CODE_AUTHENTICATION_ERROR: 401,
}
def __init__(self, code: int, message: str, data: dict = None):
assert code and isinstance(code, int), "'code' must be an int"
assert message and isinstance(message, str), "'message' must be a string"
assert data is None or isinstance(data, dict), "'data' must be None or a dict"
self.code = code
self.message = message
self.data = data or {}
def to_dict(self):
return {
'code': self.code,
'message': self.message,
'data': self.data,
}
@staticmethod
def filter_traceback(traceback):
result = []
if traceback is not None:
result = trace_lines = traceback.split("\n")
for i, t in enumerate(trace_lines):
if "--- ---" in t:
if len(trace_lines) > i + 1:
result = [j for j in trace_lines[i + 1:] if j]
break
return result
@classmethod
def create_command_exception(cls, command, args, kwargs, exception, traceback):
if 'password' in kwargs and isinstance(kwargs['password'], str):
kwargs['password'] = '*'*len(kwargs['password'])
return cls(
cls.CODE_APPLICATION_ERROR, str(exception), {
'name': exception.__class__.__name__,
'traceback': cls.filter_traceback(traceback),
'command': command,
'args': args,
'kwargs': kwargs,
}
)
class UnknownAPIMethodError(Exception):
pass
def jsonrpc_dumps_pretty(obj, **kwargs):
if isinstance(obj, JSONRPCError):
data = {"jsonrpc": "2.0", "error": obj.to_dict()}
else:
data = {"jsonrpc": "2.0", "result": obj}
return json.dumps(data, cls=JSONResponseEncoder, sort_keys=True, indent=2, **kwargs) + "\n"
def trap(err, *to_trap):
err.trap(*to_trap)
class JSONRPCServerType(type):
def __new__(mcs, name, bases, newattrs):
klass = type.__new__(mcs, name, bases, newattrs)
klass.callable_methods = {}
klass.deprecated_methods = {}
for methodname in dir(klass):
if methodname.startswith("jsonrpc_"):
method = getattr(klass, methodname)
if not hasattr(method, '_deprecated'):
klass.callable_methods.update({methodname.split("jsonrpc_")[1]: method})
else:
klass.deprecated_methods.update({methodname.split("jsonrpc_")[1]: method})
return klass
HISTOGRAM_BUCKETS = (
.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')
)
class Daemon(metaclass=JSONRPCServerType):
"""
LBRYnet daemon, a jsonrpc interface to lbry functions
"""
callable_methods: dict
deprecated_methods: dict
pending_requests_metric = Gauge(
"pending_requests", "Number of running api requests", namespace="daemon_api",
labelnames=("method",)
)
requests_count_metric = Counter(
"requests_count", "Number of requests received", namespace="daemon_api",
labelnames=("method",)
)
failed_request_metric = Counter(
"failed_request_count", "Number of failed requests", namespace="daemon_api",
labelnames=("method",)
)
cancelled_request_metric = Counter(
"cancelled_request_count", "Number of cancelled requests", namespace="daemon_api",
labelnames=("method",)
)
response_time_metric = Histogram(
"response_time", "Response times", namespace="daemon_api", buckets=HISTOGRAM_BUCKETS,
labelnames=("method",)
)
def __init__(self, conf: Config, component_manager: typing.Optional[ComponentManager] = None):
self.conf = conf
self.platform_info = system_info.get_platform()
self._video_file_analyzer = VideoFileAnalyzer(conf)
self._node_id = None
self._installation_id = None
self.session_id = base58.b58encode(utils.generate_id()).decode()
self.analytics_manager = analytics.AnalyticsManager(conf, self.installation_id, self.session_id)
self.component_manager = component_manager or ComponentManager(
conf, analytics_manager=self.analytics_manager,
skip_components=conf.components_to_skip or []
)
self.component_startup_task = None
logging.getLogger('aiohttp.access').setLevel(logging.WARN)
rpc_app = web.Application()
rpc_app.router.add_get('/lbryapi', self.handle_old_jsonrpc)
rpc_app.router.add_post('/lbryapi', self.handle_old_jsonrpc)
rpc_app.router.add_post('/', self.handle_old_jsonrpc)
rpc_app.router.add_options('/', self.add_cors_headers)
self.rpc_runner = web.AppRunner(rpc_app)
streaming_app = web.Application()
streaming_app.router.add_get('/get/{claim_name}', self.handle_stream_get_request)
streaming_app.router.add_get('/get/{claim_name}/{claim_id}', self.handle_stream_get_request)
streaming_app.router.add_get('/stream/{sd_hash}', self.handle_stream_range_request)
self.streaming_runner = web.AppRunner(streaming_app)
prom_app = web.Application()
prom_app.router.add_get('/metrics', self.handle_metrics_get_request)
self.metrics_runner = web.AppRunner(prom_app)
@property
def dht_node(self) -> typing.Optional['Node']:
return self.component_manager.get_component(DHT_COMPONENT)
@property
def wallet_manager(self) -> typing.Optional['WalletManager']:
return self.component_manager.get_component(WALLET_COMPONENT)
@property
def storage(self) -> typing.Optional['SQLiteStorage']:
return self.component_manager.get_component(DATABASE_COMPONENT)
@property
def file_manager(self) -> typing.Optional['FileManager']:
return self.component_manager.get_component(FILE_MANAGER_COMPONENT)
@property
def exchange_rate_manager(self) -> typing.Optional['ExchangeRateManager']:
return self.component_manager.get_component(EXCHANGE_RATE_MANAGER_COMPONENT)
@property
def blob_manager(self) -> typing.Optional['BlobManager']:
return self.component_manager.get_component(BLOB_COMPONENT)
@property
def disk_space_manager(self) -> typing.Optional['DiskSpaceManager']:
return self.component_manager.get_component(DISK_SPACE_COMPONENT)
@property
def upnp(self) -> typing.Optional['UPnPComponent']:
return self.component_manager.get_component(UPNP_COMPONENT)
@classmethod
def get_api_definitions(cls):
prefix = 'jsonrpc_'
not_grouped = ['routing_table_get', 'ffmpeg_find']
api = {
'groups': {
group_name[:-len('_DOC')].lower(): getattr(cls, group_name).strip()
for group_name in dir(cls) if group_name.endswith('_DOC')
},
'commands': {}
}
for jsonrpc_method in dir(cls):
if jsonrpc_method.startswith(prefix):
full_name = jsonrpc_method[len(prefix):]
method = getattr(cls, jsonrpc_method)
if full_name in not_grouped:
name_parts = [full_name]
else:
name_parts = full_name.split('_', 1)
if len(name_parts) == 1:
group = None
name, = name_parts
elif len(name_parts) == 2:
group, name = name_parts
assert group in api['groups'], \
f"Group {group} does not have doc string for command {full_name}."
else:
raise NameError(f'Could not parse method name: {jsonrpc_method}')
api['commands'][full_name] = {
'api_method_name': full_name,
'name': name,
'group': group,
'doc': method.__doc__,
'method': method,
}
if hasattr(method, '_deprecated'):
api['commands'][full_name]['replaced_by'] = method.new_command
for command in api['commands'].values():
if 'replaced_by' in command:
command['replaced_by'] = api['commands'][command['replaced_by']]
return api
@property
def db_revision_file_path(self):
return os.path.join(self.conf.data_dir, 'db_revision')
@property
def installation_id(self):
install_id_filename = os.path.join(self.conf.data_dir, "install_id")
if not self._installation_id:
if os.path.isfile(install_id_filename):
with open(install_id_filename, "r") as install_id_file:
self._installation_id = str(install_id_file.read()).strip()
if not self._installation_id:
self._installation_id = base58.b58encode(utils.generate_id()).decode()
with open(install_id_filename, "w") as install_id_file:
install_id_file.write(self._installation_id)
return self._installation_id
def ensure_data_dir(self):
if not os.path.isdir(self.conf.data_dir):
os.makedirs(self.conf.data_dir)
if not os.path.isdir(os.path.join(self.conf.data_dir, "blobfiles")):
os.makedirs(os.path.join(self.conf.data_dir, "blobfiles"))
return self.conf.data_dir
def ensure_wallet_dir(self):
if not os.path.isdir(self.conf.wallet_dir):
os.makedirs(self.conf.wallet_dir)
def ensure_download_dir(self):
if not os.path.isdir(self.conf.download_dir):
os.makedirs(self.conf.download_dir)
async def start(self):
log.info("Starting LBRYNet Daemon")
log.debug("Settings: %s", json.dumps(self.conf.settings_dict, indent=2))
log.info("Platform: %s", json.dumps(self.platform_info, indent=2))
await self.analytics_manager.send_server_startup()
await self.rpc_runner.setup()
await self.streaming_runner.setup()
await self.metrics_runner.setup()
try:
rpc_site = web.TCPSite(self.rpc_runner, self.conf.api_host, self.conf.api_port, shutdown_timeout=.5)
await rpc_site.start()
log.info('RPC server listening on TCP %s:%i', *rpc_site._server.sockets[0].getsockname()[:2])
except OSError as e:
log.error('RPC server failed to bind TCP %s:%i', self.conf.api_host, self.conf.api_port)
await self.analytics_manager.send_server_startup_error(str(e))
raise SystemExit()
try:
streaming_site = web.TCPSite(self.streaming_runner, self.conf.streaming_host, self.conf.streaming_port,
shutdown_timeout=.5)
await streaming_site.start()
log.info('media server listening on TCP %s:%i', *streaming_site._server.sockets[0].getsockname()[:2])
except OSError as e:
log.error('media server failed to bind TCP %s:%i', self.conf.streaming_host, self.conf.streaming_port)
await self.analytics_manager.send_server_startup_error(str(e))
raise SystemExit()
if self.conf.prometheus_port:
try:
prom_site = web.TCPSite(self.metrics_runner, "0.0.0.0", self.conf.prometheus_port, shutdown_timeout=.5)
await prom_site.start()
log.info('metrics server listening on TCP %s:%i', *prom_site._server.sockets[0].getsockname()[:2])
except OSError as e:
log.error('metrics server failed to bind TCP :%i', self.conf.prometheus_port)
await self.analytics_manager.send_server_startup_error(str(e))
raise SystemExit()
try:
await self.initialize()
except asyncio.CancelledError:
log.info("shutting down before finished starting")
await self.analytics_manager.send_server_startup_error("shutting down before finished starting")
raise
except Exception as e:
await self.analytics_manager.send_server_startup_error(str(e))
log.exception('Failed to start lbrynet')
raise SystemExit()
await self.analytics_manager.send_server_startup_success()
async def initialize(self):
self.ensure_data_dir()
self.ensure_wallet_dir()
self.ensure_download_dir()
if not self.analytics_manager.is_started:
await self.analytics_manager.start()
self.component_startup_task = asyncio.create_task(self.component_manager.start())
await self.component_startup_task
async def stop(self):
if self.component_startup_task is not None:
if self.component_startup_task.done():
await self.component_manager.stop()
else:
self.component_startup_task.cancel()
# the wallet component might have not started
try:
wallet_component = self.component_manager.get_actual_component('wallet')
except NameError:
pass
else:
await wallet_component.stop()
await self.component_manager.stop()
log.info("stopped api components")
await self.rpc_runner.cleanup()
await self.streaming_runner.cleanup()
await self.metrics_runner.cleanup()
log.info("stopped api server")
if self.analytics_manager.is_started:
self.analytics_manager.stop()
log.info("finished shutting down")
async def add_cors_headers(self, request):
if self.conf.allowed_origin:
return web.Response(
headers={
'Access-Control-Allow-Origin': self.conf.allowed_origin,
'Access-Control-Allow-Methods': self.conf.allowed_origin,
'Access-Control-Allow-Headers': self.conf.allowed_origin,
}
)
return None
async def handle_old_jsonrpc(self, request):
ensure_request_allowed(request, self.conf)
data = await request.json()
params = data.get('params', {})
include_protobuf = params.pop('include_protobuf', False) if isinstance(params, dict) else False
result = await self._process_rpc_call(data)
ledger = None
if 'wallet' in self.component_manager.get_components_status():
# self.ledger only available if wallet component is not skipped
ledger = self.ledger
try:
encoded_result = jsonrpc_dumps_pretty(
result, ledger=ledger, include_protobuf=include_protobuf)
except Exception:
log.exception('Failed to encode JSON RPC result:')
encoded_result = jsonrpc_dumps_pretty(JSONRPCError(
JSONRPCError.CODE_APPLICATION_ERROR,
'After successfully executing the command, failed to encode result for JSON RPC response.',
{'traceback': format_exc()}
), ledger=ledger)
headers = {}
if self.conf.allowed_origin:
headers.update({
'Access-Control-Allow-Origin': self.conf.allowed_origin,
'Access-Control-Allow-Methods': self.conf.allowed_origin,
'Access-Control-Allow-Headers': self.conf.allowed_origin,
})
return web.Response(
text=encoded_result,
headers=headers,
content_type='application/json'
)
@staticmethod
async def handle_metrics_get_request(request: web.Request):
try:
return web.Response(
text=prom_generate_latest().decode(),
content_type='text/plain; version=0.0.4'
)
except Exception:
log.exception('could not generate prometheus data')
raise
async def handle_stream_get_request(self, request: web.Request):
if not self.conf.streaming_get:
log.warning("streaming_get is disabled, rejecting request")
raise web.HTTPForbidden()
name_and_claim_id = request.path.split("/get/")[1]
if "/" not in name_and_claim_id:
uri = f"lbry://{name_and_claim_id}"
else:
name, claim_id = name_and_claim_id.split("/")
uri = f"lbry://{name}#{claim_id}"
if not self.file_manager.started.is_set():
await self.file_manager.started.wait()
stream = await self.jsonrpc_get(uri)
if isinstance(stream, dict):
raise web.HTTPServerError(text=stream['error'])
raise web.HTTPFound(f"/stream/{stream.sd_hash}")
async def handle_stream_range_request(self, request: web.Request):
try:
return await self._handle_stream_range_request(request)
except web.HTTPException as err:
log.warning("http code during /stream range request: %s", err)
raise err
except asyncio.CancelledError:
# if not excepted here, it would bubble up the error to the console. every time you closed
# a running tab, you'd get this error in the console
log.debug("/stream range request cancelled")
except Exception:
log.exception("error handling /stream range request")
raise
finally:
log.debug("finished handling /stream range request")
async def _handle_stream_range_request(self, request: web.Request):
sd_hash = request.path.split("/stream/")[1]
if not self.file_manager.started.is_set():
await self.file_manager.started.wait()
if sd_hash not in self.file_manager.streams:
return web.HTTPNotFound()
return await self.file_manager.stream_partial_content(request, sd_hash)
async def _process_rpc_call(self, data):
args = data.get('params', {})
try:
function_name = data['method']
except KeyError:
return JSONRPCError(
JSONRPCError.CODE_METHOD_NOT_FOUND,
"Missing 'method' value in request."
)
try:
method = self._get_jsonrpc_method(function_name)
except UnknownAPIMethodError:
return JSONRPCError(
JSONRPCError.CODE_METHOD_NOT_FOUND,
str(CommandDoesNotExistError(function_name))
)
if args in ([{}], []):
_args, _kwargs = (), {}
elif isinstance(args, dict):
_args, _kwargs = (), args
elif isinstance(args, list) and len(args) == 1 and isinstance(args[0], dict):
# TODO: this is for backwards compatibility. Remove this once API and UI are updated
# TODO: also delete EMPTY_PARAMS then
_args, _kwargs = (), args[0]
elif isinstance(args, list) and len(args) == 2 and \
isinstance(args[0], list) and isinstance(args[1], dict):
_args, _kwargs = args
else:
return JSONRPCError(
JSONRPCError.CODE_INVALID_PARAMS,
f"Invalid parameters format: {args}"
)
if is_transactional_function(function_name):
log.info("%s %s %s", function_name, _args, _kwargs)
params_error, erroneous_params = self._check_params(method, _args, _kwargs)
if params_error is not None:
params_error_message = '{} for {} command: {}'.format(
params_error, function_name, ', '.join(erroneous_params)
)
log.warning(params_error_message)
return JSONRPCError(
JSONRPCError.CODE_INVALID_PARAMS,
params_error_message,
)
self.pending_requests_metric.labels(method=function_name).inc()
self.requests_count_metric.labels(method=function_name).inc()
start = time.perf_counter()
try:
result = method(self, *_args, **_kwargs)
if asyncio.iscoroutine(result):
result = await result
return result
except asyncio.CancelledError:
self.cancelled_request_metric.labels(method=function_name).inc()
log.info("cancelled API call for: %s", function_name)
raise
except Exception as e: # pylint: disable=broad-except
self.failed_request_metric.labels(method=function_name).inc()
if not isinstance(e, BaseError):
log.exception("error handling api request")
else:
log.error("error handling api request: %s", e)
return JSONRPCError.create_command_exception(
command=function_name, args=_args, kwargs=_kwargs, exception=e, traceback=format_exc()
)
finally:
self.pending_requests_metric.labels(method=function_name).dec()
self.response_time_metric.labels(method=function_name).observe(time.perf_counter() - start)
def _verify_method_is_callable(self, function_path):
if function_path not in self.callable_methods:
raise UnknownAPIMethodError(function_path)
def _get_jsonrpc_method(self, function_path):
if function_path in self.deprecated_methods:
new_command = self.deprecated_methods[function_path].new_command
log.warning('API function \"%s\" is deprecated, please update to use \"%s\"',
function_path, new_command)
function_path = new_command
self._verify_method_is_callable(function_path)
return self.callable_methods.get(function_path)
@staticmethod
def _check_params(function, args_tup, args_dict):
argspec = inspect.getfullargspec(undecorated(function))
num_optional_params = 0 if argspec.defaults is None else len(argspec.defaults)
duplicate_params = [
duplicate_param
for duplicate_param in argspec.args[1:len(args_tup) + 1]
if duplicate_param in args_dict
]
if duplicate_params:
return 'Duplicate parameters', duplicate_params
missing_required_params = [
required_param
for required_param in argspec.args[len(args_tup) + 1:-num_optional_params]
if required_param not in args_dict
]
if len(missing_required_params) > 0:
return 'Missing required parameters', missing_required_params
extraneous_params = [] if argspec.varkw is not None else [
extra_param
for extra_param in args_dict
if extra_param not in argspec.args[1:]
]
if len(extraneous_params) > 0:
return 'Extraneous parameters', extraneous_params
return None, None
@property
def ledger(self) -> Optional['Ledger']:
try:
return self.wallet_manager.default_account.ledger
except AttributeError:
return None
async def get_est_cost_from_uri(self, uri: str) -> typing.Optional[float]:
"""
Resolve a name and return the estimated stream cost
"""
resolved = await self.resolve([], uri)
if resolved:
claim_response = resolved[uri]
else:
claim_response = None
if claim_response and 'claim' in claim_response:
if 'value' in claim_response['claim'] and claim_response['claim']['value'] is not None:
claim_value = Claim.from_bytes(claim_response['claim']['value'])
if not claim_value.stream.has_fee:
return 0.0
return round(
self.exchange_rate_manager.convert_currency(
claim_value.stream.fee.currency, "LBC", claim_value.stream.fee.amount
), 5
)
else:
log.warning("Failed to estimate cost for %s", uri)
############################################################################
# #
# JSON-RPC API methods start here #
# #
############################################################################
def jsonrpc_stop(self): # pylint: disable=no-self-use
"""
Stop lbrynet API server.
Usage:
stop
Options:
None
Returns:
(string) Shutdown message
"""
def shutdown():
raise web.GracefulExit()
log.info("Shutting down lbrynet daemon")
asyncio.get_event_loop().call_later(0, shutdown)
return "Shutting down"
async def jsonrpc_ffmpeg_find(self):
"""
Get ffmpeg installation information
Usage:
ffmpeg_find
Options:
None
Returns:
(dict) Dictionary of ffmpeg information
{
'available': (bool) found ffmpeg,
'which': (str) path to ffmpeg,
'analyze_audio_volume': (bool) should ffmpeg analyze audio
}
"""
return await self._video_file_analyzer.status(reset=True, recheck=True)
async def jsonrpc_status(self):
"""
Get daemon status
Usage:
status
Options:
None
Returns:
(dict) lbrynet-daemon status
{
'installation_id': (str) installation id - base58,
'is_running': (bool),
'skipped_components': (list) [names of skipped components (str)],
'startup_status': { Does not include components which have been skipped
'blob_manager': (bool),
'blockchain_headers': (bool),
'database': (bool),
'dht': (bool),
'exchange_rate_manager': (bool),
'hash_announcer': (bool),
'peer_protocol_server': (bool),
'file_manager': (bool),
'libtorrent_component': (bool),
'upnp': (bool),
'wallet': (bool),
},
'connection_status': {
'code': (str) connection status code,
'message': (str) connection status message
},
'blockchain_headers': {
'downloading_headers': (bool),
'download_progress': (float) 0-100.0
},
'wallet': {
'connected': (str) host and port of the connected spv server,
'blocks': (int) local blockchain height,
'blocks_behind': (int) remote_height - local_height,
'best_blockhash': (str) block hash of most recent block,
'is_encrypted': (bool),
'is_locked': (bool),
'connected_servers': (list) [
{
'host': (str) server hostname,
'port': (int) server port,
'latency': (int) milliseconds
}
],
},
'libtorrent_component': {
'running': (bool) libtorrent was detected and started successfully,
},
'dht': {
'node_id': (str) lbry dht node id - hex encoded,
'peers_in_routing_table': (int) the number of peers in the routing table,
},
'blob_manager': {
'finished_blobs': (int) number of finished blobs in the blob manager,
'connections': {
'incoming_bps': {
: (int) bytes per second received,
},
'outgoing_bps': {
: (int) bytes per second sent,
},
'total_outgoing_mps': (float) megabytes per second sent,
'total_incoming_mps': (float) megabytes per second received,
'max_outgoing_mbs': (float) maximum bandwidth (megabytes per second) sent, since the
daemon was started
'max_incoming_mbs': (float) maximum bandwidth (megabytes per second) received, since the
daemon was started
'total_sent' : (int) total number of bytes sent since the daemon was started
'total_received' : (int) total number of bytes received since the daemon was started
}
},
'hash_announcer': {
'announce_queue_size': (int) number of blobs currently queued to be announced
},
'file_manager': {
'managed_files': (int) count of files in the stream manager,
},
'upnp': {
'aioupnp_version': (str),
'redirects': {
: (int) external_port,
},
'gateway': (str) manufacturer and model,
'dht_redirect_set': (bool),
'peer_redirect_set': (bool),
'external_ip': (str) external ip address,
}
}
"""
ffmpeg_status = await self._video_file_analyzer.status()
running_components = self.component_manager.get_components_status()
response = {
'installation_id': self.installation_id,
'is_running': all(running_components.values()),
'skipped_components': self.component_manager.skip_components,
'startup_status': running_components,
'ffmpeg_status': ffmpeg_status
}
for component in self.component_manager.components:
status = await component.get_status()
if status:
response[component.component_name] = status
return response
def jsonrpc_version(self): # pylint: disable=no-self-use
"""
Get lbrynet API server version information
Usage:
version
Options:
None
Returns:
(dict) Dictionary of lbry version information
{
'processor': (str) processor type,
'python_version': (str) python version,
'platform': (str) platform string,
'os_release': (str) os release string,
'os_system': (str) os name,
'version': (str) lbrynet version,
'build': (str) "dev" | "qa" | "rc" | "release",
}
"""
return self.platform_info
@requires(WALLET_COMPONENT)
async def jsonrpc_resolve(self, urls: typing.Union[str, list], wallet_id=None, **kwargs):
"""
Get the claim that a URL refers to.
Usage:
resolve ... [--wallet_id=]
[--include_purchase_receipt]
[--include_is_my_output]
[--include_sent_supports]
[--include_sent_tips]
[--include_received_tips]
[--new_sdk_server=]
Options:
--urls= : (str, list) one or more urls to resolve
--wallet_id= : (str) wallet to check for claim purchase receipts
--new_sdk_server= : (str) URL of the new SDK server (EXPERIMENTAL)
--include_purchase_receipt : (bool) lookup and include a receipt if this wallet
has purchased the claim being resolved
--include_is_my_output : (bool) lookup and include a boolean indicating
if claim being resolved is yours
--include_sent_supports : (bool) lookup and sum the total amount
of supports you've made to this claim
--include_sent_tips : (bool) lookup and sum the total amount
of tips you've made to this claim
(only makes sense when claim is not yours)
--include_received_tips : (bool) lookup and sum the total amount
of tips you've received to this claim
(only makes sense when claim is yours)
Returns:
Dictionary of results, keyed by url
'': {
If a resolution error occurs:
'error': Error message
If the url resolves to a channel or a claim in a channel:
'certificate': {
'address': (str) claim address,
'amount': (float) claim amount,
'effective_amount': (float) claim amount including supports,
'claim_id': (str) claim id,
'claim_sequence': (int) claim sequence number (or -1 if unknown),
'decoded_claim': (bool) whether or not the claim value was decoded,
'height': (int) claim height,
'confirmations': (int) claim depth,
'timestamp': (int) timestamp of the block that included this claim tx,
'has_signature': (bool) included if decoded_claim
'name': (str) claim name,
'permanent_url': (str) permanent url of the certificate claim,
'supports: (list) list of supports [{'txid': (str) txid,
'nout': (int) nout,
'amount': (float) amount}],
'txid': (str) claim txid,
'nout': (str) claim nout,
'signature_is_valid': (bool), included if has_signature,
'value': ClaimDict if decoded, otherwise hex string
}
If the url resolves to a channel:
'claims_in_channel': (int) number of claims in the channel,
If the url resolves to a claim:
'claim': {
'address': (str) claim address,
'amount': (float) claim amount,
'effective_amount': (float) claim amount including supports,
'claim_id': (str) claim id,
'claim_sequence': (int) claim sequence number (or -1 if unknown),
'decoded_claim': (bool) whether or not the claim value was decoded,
'height': (int) claim height,
'depth': (int) claim depth,
'has_signature': (bool) included if decoded_claim
'name': (str) claim name,
'permanent_url': (str) permanent url of the claim,
'channel_name': (str) channel name if claim is in a channel
'supports: (list) list of supports [{'txid': (str) txid,
'nout': (int) nout,
'amount': (float) amount}]
'txid': (str) claim txid,
'nout': (str) claim nout,
'signature_is_valid': (bool), included if has_signature,
'value': ClaimDict if decoded, otherwise hex string
}
}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if isinstance(urls, str):
urls = [urls]
results = {}
valid_urls = set()
for url in urls:
try:
URL.parse(url)
valid_urls.add(url)
except ValueError:
results[url] = {"error": f"{url} is not a valid url"}
resolved = await self.resolve(wallet.accounts, list(valid_urls), **kwargs)
for resolved_uri in resolved:
results[resolved_uri] = resolved[resolved_uri] if resolved[resolved_uri] is not None else \
{"error": f"{resolved_uri} did not resolve to a claim"}
return results
@requires(WALLET_COMPONENT, EXCHANGE_RATE_MANAGER_COMPONENT, BLOB_COMPONENT, DATABASE_COMPONENT,
FILE_MANAGER_COMPONENT)
async def jsonrpc_get(
self, uri, file_name=None, download_directory=None, timeout=None, save_file=None, wallet_id=None):
"""
Download stream from a LBRY name.
Usage:
get [ | --file_name=]
[ | --download_directory=] [ | --timeout=]
[--save_file=] [--wallet_id=]
Options:
--uri= : (str) uri of the content to download
--file_name= : (str) specified name for the downloaded file, overrides the stream file name
--download_directory= : (str) full path to the directory to download into
--timeout= : (int) download timeout in number of seconds
--save_file= : (bool) save the file to the downloads directory
--wallet_id= : (str) wallet to check for claim purchase receipts
Returns: {File}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if download_directory and not os.path.isdir(download_directory):
return {"error": f"specified download directory \"{download_directory}\" does not exist"}
try:
stream = await self.file_manager.download_from_uri(
uri, self.exchange_rate_manager, timeout, file_name, download_directory,
save_file=save_file, wallet=wallet
)
if not stream:
raise DownloadSDTimeoutError(uri)
except Exception as e:
# TODO: use error from lbry.error
log.warning("Error downloading %s: %s", uri, str(e))
return {"error": str(e)}
return stream
SETTINGS_DOC = """
Settings management.
"""
def jsonrpc_settings_get(self):
"""
Get daemon settings
Usage:
settings_get
Options:
None
Returns:
(dict) Dictionary of daemon settings
See ADJUSTABLE_SETTINGS in lbry/conf.py for full list of settings
"""
return self.conf.settings_dict
def jsonrpc_settings_set(self, key, value):
"""
Set daemon settings
Usage:
settings_set () ()
Options:
None
Returns:
(dict) Updated dictionary of daemon settings
"""
with self.conf.update_config() as c:
if value and isinstance(value, str) and value[0] in ('[', '{'):
value = json.loads(value)
attr: Setting = getattr(type(c), key)
cleaned = attr.deserialize(value)
setattr(c, key, cleaned)
return {key: cleaned}
def jsonrpc_settings_clear(self, key):
"""
Clear daemon settings
Usage:
settings_clear ()
Options:
None
Returns:
(dict) Updated dictionary of daemon settings
"""
with self.conf.update_config() as c:
setattr(c, key, NOT_SET)
return {key: self.conf.settings_dict[key]}
PREFERENCE_DOC = """
Preferences management.
"""
def jsonrpc_preference_get(self, key=None, wallet_id=None):
"""
Get preference value for key or all values if not key is passed in.
Usage:
preference_get [] [--wallet_id=]
Options:
--key= : (str) key associated with value
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(dict) Dictionary of preference(s)
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if key:
if key in wallet.preferences:
return {key: wallet.preferences[key]}
return
return wallet.preferences.to_dict_without_ts()
def jsonrpc_preference_set(self, key, value, wallet_id=None):
"""
Set preferences
Usage:
preference_set () () [--wallet_id=]
Options:
--key= : (str) key associated with value
--value= : (str) key associated with value
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(dict) Dictionary with key/value of new preference
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if value and isinstance(value, str) and value[0] in ('[', '{'):
value = json.loads(value)
wallet.preferences[key] = value
wallet.save()
return {key: value}
WALLET_DOC = """
Create, modify and inspect wallets.
"""
@requires("wallet")
def jsonrpc_wallet_list(self, wallet_id=None, page=None, page_size=None):
"""
List wallets.
Usage:
wallet_list [--wallet_id=] [--page=] [--page_size=]
Options:
--wallet_id= : (str) show specific wallet only
--page= : (int) page to return during paginating
--page_size= : (int) number of items on page during pagination
Returns: {Paginated[Wallet]}
"""
if wallet_id:
return paginate_list([self.wallet_manager.get_wallet_or_error(wallet_id)], 1, 1)
return paginate_list(self.wallet_manager.wallets, page, page_size)
def jsonrpc_wallet_reconnect(self):
"""
Reconnects ledger network client, applying new configurations.
Usage:
wallet_reconnect
Options:
Returns: None
"""
return self.wallet_manager.reset()
@requires("wallet")
async def jsonrpc_wallet_create(
self, wallet_id, skip_on_startup=False, create_account=False, single_key=False):
"""
Create a new wallet.
Usage:
wallet_create ( | --wallet_id=) [--skip_on_startup]
[--create_account] [--single_key]
Options:
--wallet_id= : (str) wallet file name
--skip_on_startup : (bool) don't add wallet to daemon_settings.yml
--create_account : (bool) generates the default account
--single_key : (bool) used with --create_account, creates single-key account
Returns: {Wallet}
"""
wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id)
for wallet in self.wallet_manager.wallets:
if wallet.id == wallet_id:
raise WalletAlreadyLoadedError(wallet_path)
if os.path.exists(wallet_path):
raise WalletAlreadyExistsError(wallet_path)
wallet = self.wallet_manager.import_wallet(wallet_path)
if not wallet.accounts and create_account:
account = Account.generate(
self.ledger, wallet, address_generator={
'name': SingleKey.name if single_key else HierarchicalDeterministic.name
}
)
if self.ledger.network.is_connected:
await self.ledger.subscribe_account(account)
wallet.save()
if not skip_on_startup:
with self.conf.update_config() as c:
c.wallets += [wallet_id]
return wallet
@requires("wallet")
async def jsonrpc_wallet_export(self, password=None, wallet_id=None):
"""
Exports encrypted wallet data if password is supplied; otherwise plain JSON.
Wallet must be unlocked to perform this operation.
Usage:
wallet_export [--password=] [--wallet_id=]
Options:
--password= : (str) password to encrypt outgoing data
--wallet_id= : (str) wallet being exported
Returns:
(str) data: base64-encoded encrypted wallet, or cleartext JSON
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if password is None:
return wallet.to_json()
return wallet.pack(password).decode()
@requires("wallet")
async def jsonrpc_wallet_import(self, data, password=None, wallet_id=None, blocking=False):
"""
Import wallet data and merge accounts and preferences. Data is expected to be JSON if
password is not supplied.
Wallet must be unlocked to perform this operation.
Usage:
wallet_import ( | --data=) [ | --password=]
[--wallet_id=] [--blocking]
Options:
--data= : (str) incoming wallet data
--password= : (str) password to decrypt incoming data
--wallet_id= : (str) wallet being merged into
--blocking : (bool) wait until any new accounts have merged
Returns:
(str) base64-encoded encrypted wallet, or cleartext JSON
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
added_accounts, merged_accounts = wallet.merge(self.wallet_manager, password, data)
for new_account in itertools.chain(added_accounts, merged_accounts):
await new_account.maybe_migrate_certificates()
if added_accounts and self.ledger.network.is_connected:
if blocking:
await asyncio.wait([
a.ledger.subscribe_account(a) for a in added_accounts
])
else:
for new_account in added_accounts:
asyncio.create_task(self.ledger.subscribe_account(new_account))
wallet.save()
return await self.jsonrpc_wallet_export(password=password, wallet_id=wallet_id)
@requires("wallet")
async def jsonrpc_wallet_add(self, wallet_id):
"""
Add existing wallet.
Usage:
wallet_add ( | --wallet_id=)
Options:
--wallet_id= : (str) wallet file name
Returns: {Wallet}
"""
wallet_path = os.path.join(self.conf.wallet_dir, 'wallets', wallet_id)
for wallet in self.wallet_manager.wallets:
if wallet.id == wallet_id:
raise WalletAlreadyLoadedError(wallet_path)
if not os.path.exists(wallet_path):
raise WalletNotFoundError(wallet_path)
wallet = self.wallet_manager.import_wallet(wallet_path)
if self.ledger.network.is_connected:
for account in wallet.accounts:
await self.ledger.subscribe_account(account)
return wallet
@requires("wallet")
async def jsonrpc_wallet_remove(self, wallet_id):
"""
Remove an existing wallet.
Usage:
wallet_remove ( | --wallet_id=)
Options:
--wallet_id= : (str) name of wallet to remove
Returns: {Wallet}
"""
wallet = self.wallet_manager.get_wallet_or_error(wallet_id)
self.wallet_manager.wallets.remove(wallet)
for account in wallet.accounts:
await self.ledger.unsubscribe_account(account)
return wallet
@requires("wallet")
async def jsonrpc_wallet_balance(self, wallet_id=None, confirmations=0):
"""
Return the balance of a wallet
Usage:
wallet_balance [--wallet_id=] [--confirmations=]
Options:
--wallet_id= : (str) balance for specific wallet
--confirmations= : (int) Only include transactions with this many
confirmed blocks.
Returns:
(decimal) amount of lbry credits in wallet
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
balance = await self.ledger.get_detailed_balance(
accounts=wallet.accounts, confirmations=confirmations
)
return dict_values_to_lbc(balance)
def jsonrpc_wallet_status(self, wallet_id=None):
"""
Status of wallet including encryption/lock state.
Usage:
wallet_status [ | --wallet_id=]
Options:
--wallet_id= : (str) status of specific wallet
Returns:
Dictionary of wallet status information.
"""
if self.wallet_manager is None:
return {'is_encrypted': None, 'is_syncing': None, 'is_locked': None}
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
return {
'is_encrypted': wallet.is_encrypted,
'is_syncing': len(self.ledger._update_tasks) > 0,
'is_locked': wallet.is_locked
}
@requires(WALLET_COMPONENT)
def jsonrpc_wallet_unlock(self, password, wallet_id=None):
"""
Unlock an encrypted wallet
Usage:
wallet_unlock ( | --password=) [--wallet_id=]
Options:
--password= : (str) password to use for unlocking
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(bool) true if wallet is unlocked, otherwise false
"""
return self.wallet_manager.get_wallet_or_default(wallet_id).unlock(password)
@requires(WALLET_COMPONENT)
def jsonrpc_wallet_lock(self, wallet_id=None):
"""
Lock an unlocked wallet
Usage:
wallet_lock [--wallet_id=]
Options:
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(bool) true if wallet is locked, otherwise false
"""
return self.wallet_manager.get_wallet_or_default(wallet_id).lock()
@requires(WALLET_COMPONENT)
def jsonrpc_wallet_decrypt(self, wallet_id=None):
"""
Decrypt an encrypted wallet, this will remove the wallet password. The wallet must be unlocked to decrypt it
Usage:
wallet_decrypt [--wallet_id=]
Options:
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(bool) true if wallet is decrypted, otherwise false
"""
return self.wallet_manager.get_wallet_or_default(wallet_id).decrypt()
@requires(WALLET_COMPONENT)
def jsonrpc_wallet_encrypt(self, new_password, wallet_id=None):
"""
Encrypt an unencrypted wallet with a password
Usage:
wallet_encrypt ( | --new_password=)
[--wallet_id=]
Options:
--new_password= : (str) password to encrypt account
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(bool) true if wallet is decrypted, otherwise false
"""
return self.wallet_manager.get_wallet_or_default(wallet_id).encrypt(new_password)
@requires(WALLET_COMPONENT)
async def jsonrpc_wallet_send(
self, amount, addresses, wallet_id=None,
change_account_id=None, funding_account_ids=None, preview=False, blocking=True):
"""
Send the same number of credits to multiple addresses using all accounts in wallet to
fund the transaction and the default account to receive any change.
Usage:
wallet_send ... [--wallet_id=] [--preview]
[--change_account_id=None] [--funding_account_ids=...]
[--blocking]
Options:
--wallet_id= : (str) restrict operation to specific wallet
--change_account_id= : (str) account where change will go
--funding_account_ids= : (str) accounts to fund the transaction
--preview : (bool) do not broadcast the transaction
--blocking : (bool) wait until tx has synced
Returns: {Transaction}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
assert not wallet.is_locked, "Cannot spend funds with locked wallet, unlock first."
account = wallet.get_account_or_default(change_account_id)
accounts = wallet.get_accounts_or_all(funding_account_ids)
amount = self.get_dewies_or_error("amount", amount)
if addresses and not isinstance(addresses, list):
addresses = [addresses]
outputs = []
for address in addresses:
self.valid_address_or_error(address, allow_script_address=True)
if self.ledger.is_pubkey_address(address):
outputs.append(
Output.pay_pubkey_hash(
amount, self.ledger.address_to_hash160(address)
)
)
elif self.ledger.is_script_address(address):
outputs.append(
Output.pay_script_hash(
amount, self.ledger.address_to_hash160(address)
)
)
else:
raise ValueError(f"Unsupported address: '{address}'") # TODO: use error from lbry.error
tx = await Transaction.create(
[], outputs, accounts, account
)
if not preview:
await self.broadcast_or_release(tx, blocking)
self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())
else:
await self.ledger.release_tx(tx)
return tx
ACCOUNT_DOC = """
Create, modify and inspect wallet accounts.
"""
@requires("wallet")
async def jsonrpc_account_list(
self, account_id=None, wallet_id=None, confirmations=0,
include_claims=False, show_seed=False, page=None, page_size=None):
"""
List details of all of the accounts or a specific account.
Usage:
account_list [] [--wallet_id=]
[--confirmations=]
[--include_claims] [--show_seed]
[--page=] [--page_size=]
Options:
--account_id= : (str) If provided only the balance for this
account will be given
--wallet_id= : (str) accounts in specific wallet
--confirmations= : (int) required confirmations (default: 0)
--include_claims : (bool) include claims, requires than a
LBC account is specified (default: false)
--show_seed : (bool) show the seed for the account
--page= : (int) page to return during paginating
--page_size= : (int) number of items on page during pagination
Returns: {Paginated[Account]}
"""
kwargs = {
'confirmations': confirmations,
'show_seed': show_seed
}
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
if account_id:
return paginate_list([await wallet.get_account_or_error(account_id).get_details(**kwargs)], 1, 1)
else:
return paginate_list(await wallet.get_detailed_accounts(**kwargs), page, page_size)
@requires("wallet")
async def jsonrpc_account_balance(self, account_id=None, wallet_id=None, confirmations=0):
"""
Return the balance of an account
Usage:
account_balance [] [ | --address=] [--wallet_id=]
[ | --confirmations=]
Options:
--account_id= : (str) If provided only the balance for this
account will be given. Otherwise default account.
--wallet_id= : (str) balance for specific wallet
--confirmations= : (int) Only include transactions with this many
confirmed blocks.
Returns:
(decimal) amount of lbry credits in wallet
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_default(account_id)
balance = await account.get_detailed_balance(
confirmations=confirmations, read_only=True
)
return dict_values_to_lbc(balance)
@requires("wallet")
async def jsonrpc_account_add(
self, account_name, wallet_id=None, single_key=False,
seed=None, private_key=None, public_key=None):
"""
Add a previously created account from a seed, private key or public key (read-only).
Specify --single_key for single address or vanity address accounts.
Usage:
account_add ( | --account_name=)
(--seed= | --private_key= | --public_key=)
[--single_key] [--wallet_id=]
Options:
--account_name= : (str) name of the account to add
--seed= : (str) seed to generate new account from
--private_key= : (str) private key for new account
--public_key= : (str) public key for new account
--single_key : (bool) create single key account, default is multi-key
--wallet_id= : (str) restrict operation to specific wallet
Returns: {Account}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = Account.from_dict(
self.ledger, wallet, {
'name': account_name,
'seed': seed,
'private_key': private_key,
'public_key': public_key,
'address_generator': {
'name': SingleKey.name if single_key else HierarchicalDeterministic.name
}
}
)
wallet.save()
if self.ledger.network.is_connected:
await self.ledger.subscribe_account(account)
return account
@requires("wallet")
async def jsonrpc_account_create(self, account_name, single_key=False, wallet_id=None):
"""
Create a new account. Specify --single_key if you want to use
the same address for all transactions (not recommended).
Usage:
account_create ( | --account_name=)
[--single_key] [--wallet_id=]
Options:
--account_name= : (str) name of the account to create
--single_key : (bool) create single key account, default is multi-key
--wallet_id= : (str) restrict operation to specific wallet
Returns: {Account}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = Account.generate(
self.ledger, wallet, account_name, {
'name': SingleKey.name if single_key else HierarchicalDeterministic.name
}
)
wallet.save()
if self.ledger.network.is_connected:
await self.ledger.subscribe_account(account)
return account
@requires("wallet")
def jsonrpc_account_remove(self, account_id, wallet_id=None):
"""
Remove an existing account.
Usage:
account_remove ( | --account_id=) [--wallet_id=]
Options:
--account_id= : (str) id of the account to remove
--wallet_id= : (str) restrict operation to specific wallet
Returns: {Account}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_error(account_id)
wallet.accounts.remove(account)
wallet.save()
return account
@requires("wallet")
def jsonrpc_account_set(
self, account_id, wallet_id=None, default=False, new_name=None,
change_gap=None, change_max_uses=None, receiving_gap=None, receiving_max_uses=None):
"""
Change various settings on an account.
Usage:
account_set ( | --account_id=) [--wallet_id=]
[--default] [--new_name=]
[--change_gap=] [--change_max_uses=]
[--receiving_gap=] [--receiving_max_uses=]
Options:
--account_id= : (str) id of the account to change
--wallet_id= : (str) restrict operation to specific wallet
--default : (bool) make this account the default
--new_name= : (str) new name for the account
--receiving_gap= : (int) set the gap for receiving addresses
--receiving_max_uses= : (int) set the maximum number of times to
use a receiving address
--change_gap= : (int) set the gap for change addresses
--change_max_uses= : (int) set the maximum number of times to
use a change address
Returns: {Account}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_error(account_id)
change_made = False
if account.receiving.name == HierarchicalDeterministic.name:
address_changes = {
'change': {'gap': change_gap, 'maximum_uses_per_address': change_max_uses},
'receiving': {'gap': receiving_gap, 'maximum_uses_per_address': receiving_max_uses},
}
for chain_name, changes in address_changes.items():
chain = getattr(account, chain_name)
for attr, value in changes.items():
if value is not None:
setattr(chain, attr, value)
change_made = True
if new_name is not None:
account.name = new_name
change_made = True
if default and wallet.default_account != account:
wallet.accounts.remove(account)
wallet.accounts.insert(0, account)
change_made = True
if change_made:
account.modified_on = int(time.time())
wallet.save()
return account
@requires("wallet")
def jsonrpc_account_max_address_gap(self, account_id, wallet_id=None):
"""
Finds ranges of consecutive addresses that are unused and returns the length
of the longest such range: for change and receiving address chains. This is
useful to figure out ideal values to set for 'receiving_gap' and 'change_gap'
account settings.
Usage:
account_max_address_gap ( | --account_id=)
[--wallet_id=]
Options:
--account_id= : (str) account for which to get max gaps
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(map) maximum gap for change and receiving addresses
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
return wallet.get_account_or_error(account_id).get_max_gap()
@requires("wallet")
def jsonrpc_account_fund(self, to_account=None, from_account=None, amount='0.0',
everything=False, outputs=1, broadcast=False, wallet_id=None):
"""
Transfer some amount (or --everything) to an account from another
account (can be the same account). Amounts are interpreted as LBC.
You can also spread the transfer across a number of --outputs (cannot
be used together with --everything).
Usage:
account_fund [ | --to_account=]
[ | --from_account=]
( | --amount= | --everything)
[ | --outputs=] [--wallet_id=]
[--broadcast]
Options:
--to_account= : (str) send to this account
--from_account= : (str) spend from this account
--amount= : (decimal) the amount to transfer lbc
--everything : (bool) transfer everything (excluding claims), default: false.
--outputs= : (int) split payment across many outputs, default: 1.
--wallet_id= : (str) limit operation to specific wallet.
--broadcast : (bool) actually broadcast the transaction, default: false.
Returns: {Transaction}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
to_account = wallet.get_account_or_default(to_account)
from_account = wallet.get_account_or_default(from_account)
amount = self.get_dewies_or_error('amount', amount) if amount else None
if not isinstance(outputs, int):
# TODO: use error from lbry.error
raise ValueError("--outputs must be an integer.")
if everything and outputs > 1:
# TODO: use error from lbry.error
raise ValueError("Using --everything along with --outputs is not supported.")
return from_account.fund(
to_account=to_account, amount=amount, everything=everything,
outputs=outputs, broadcast=broadcast
)
@requires("wallet")
async def jsonrpc_account_deposit(
self, txid, nout, redeem_script, private_key,
to_account=None, wallet_id=None, preview=False, blocking=False
):
"""
Spend a time locked transaction into your account.
Usage:
account_deposit
[ | --to_account=]
[--wallet_id=] [--preview] [--blocking]
Options:
--txid= : (str) id of the transaction
--nout= : (int) output number in the transaction
--redeem_script= : (str) redeem script for output
--private_key= : (str) private key to sign transaction
--to_account= : (str) deposit to this account
--wallet_id= : (str) limit operation to specific wallet.
--preview : (bool) do not broadcast the transaction
--blocking : (bool) wait until tx has synced
Returns: {Transaction}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_default(to_account)
other_tx = await self.wallet_manager.get_transaction(txid)
tx = await Transaction.spend_time_lock(
other_tx.outputs[nout], unhexlify(redeem_script), account
)
pk = PrivateKey.from_bytes(
account.ledger, Base58.decode_check(private_key)[1:-1]
)
await tx.sign([account], {pk.address: pk})
if not preview:
await self.broadcast_or_release(tx, blocking)
self.component_manager.loop.create_task(self.analytics_manager.send_credits_sent())
else:
await self.ledger.release_tx(tx)
return tx
@requires(WALLET_COMPONENT)
def jsonrpc_account_send(self, amount, addresses, account_id=None, wallet_id=None, preview=False, blocking=False):
"""
Send the same number of credits to multiple addresses from a specific account (or default account).
Usage:
account_send ... [--account_id=] [--wallet_id=] [--preview]
[--blocking]
Options:
--account_id= : (str) account to fund the transaction
--wallet_id= : (str) restrict operation to specific wallet
--preview : (bool) do not broadcast the transaction
--blocking : (bool) wait until tx has synced
Returns: {Transaction}
"""
return self.jsonrpc_wallet_send(
amount=amount, addresses=addresses, wallet_id=wallet_id,
change_account_id=account_id, funding_account_ids=[account_id] if account_id else [],
preview=preview, blocking=blocking
)
SYNC_DOC = """
Wallet synchronization.
"""
@requires("wallet")
def jsonrpc_sync_hash(self, wallet_id=None):
"""
Deterministic hash of the wallet.
Usage:
sync_hash [ | --wallet_id=]
Options:
--wallet_id= : (str) wallet for which to generate hash
Returns:
(str) sha256 hash of wallet
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
return hexlify(wallet.hash).decode()
@requires("wallet")
async def jsonrpc_sync_apply(self, password, data=None, wallet_id=None, blocking=False):
"""
Apply incoming synchronization data, if provided, and return a sync hash and update wallet data.
Wallet must be unlocked to perform this operation.
If "encrypt-on-disk" preference is True and supplied password is different from local password,
or there is no local password (because local wallet was not encrypted), then the supplied password
will be used for local encryption (overwriting previous local encryption password).
Usage:
sync_apply [--data=] [--wallet_id=] [--blocking]
Options:
--password= : (str) password to decrypt incoming and encrypt outgoing data
--data= : (str) incoming sync data, if any
--wallet_id= : (str) wallet being sync'ed
--blocking : (bool) wait until any new accounts have sync'ed
Returns:
(map) sync hash and data
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
wallet_changed = False
if data is not None:
added_accounts, merged_accounts = wallet.merge(self.wallet_manager, password, data)
for new_account in itertools.chain(added_accounts, merged_accounts):
await new_account.maybe_migrate_certificates()
if added_accounts and self.ledger.network.is_connected:
if blocking:
await asyncio.wait([
a.ledger.subscribe_account(a) for a in added_accounts
])
else:
for new_account in added_accounts:
asyncio.create_task(self.ledger.subscribe_account(new_account))
wallet_changed = True
if wallet.preferences.get(ENCRYPT_ON_DISK, False) and password != wallet.encryption_password:
wallet.encryption_password = password
wallet_changed = True
if wallet_changed:
wallet.save()
encrypted = wallet.pack(password)
return {
'hash': self.jsonrpc_sync_hash(wallet_id),
'data': encrypted.decode()
}
ADDRESS_DOC = """
List, generate and verify addresses.
"""
@requires(WALLET_COMPONENT)
async def jsonrpc_address_is_mine(self, address, account_id=None, wallet_id=None):
"""
Checks if an address is associated with the current wallet.
Usage:
address_is_mine ( | --address=)
[ | --account_id=] [--wallet_id=]
Options:
--address= : (str) address to check
--account_id= : (str) id of the account to use
--wallet_id= : (str) restrict operation to specific wallet
Returns:
(bool) true, if address is associated with current wallet
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
account = wallet.get_account_or_default(account_id)
match = await self.ledger.db.get_address(read_only=True, address=address, accounts=[account])
if match is not None:
return True
return False
@requires(WALLET_COMPONENT)
def jsonrpc_address_list(self, address=None, account_id=None, wallet_id=None, page=None, page_size=None):
"""
List account addresses or details of single address.
Usage:
address_list [--address=] [--account_id=] [--wallet_id=]
[--page=] [--page_size=]
Options:
--address= : (str) just show details for single address
--account_id= : (str) id of the account to use
--wallet_id= : (str) restrict operation to specific wallet
--page= : (int) page to return during paginating
--page_size= : (int) number of items on page during pagination
Returns: {Paginated[Address]}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
constraints = {
'cols': ('address', 'account', 'used_times', 'pubkey', 'chain_code', 'n', 'depth')
}
if address:
constraints['address'] = address
if account_id:
constraints['accounts'] = [wallet.get_account_or_error(account_id)]
else:
constraints['accounts'] = wallet.accounts
return paginate_rows(
self.ledger.get_addresses,
self.ledger.get_address_count,
page, page_size, read_only=True, **constraints
)
@requires(WALLET_COMPONENT)
def jsonrpc_address_unused(self, account_id=None, wallet_id=None):
"""
Return an address containing no balance, will create
a new address if there is none.
Usage:
address_unused [--account_id=] [--wallet_id=]
Options:
--account_id= : (str) id of the account to use
--wallet_id= : (str) restrict operation to specific wallet
Returns: {Address}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
return wallet.get_account_or_default(account_id).receiving.get_or_create_usable_address()
FILE_DOC = """
File management.
"""
@requires(FILE_MANAGER_COMPONENT)
async def jsonrpc_file_list(self, sort=None, reverse=False, comparison=None, wallet_id=None, page=None,
page_size=None, **kwargs):
"""
List files limited by optional filters
Usage:
file_list [--sd_hash=] [--file_name=] [--stream_hash=]
[--rowid=] [--added_on=] [--claim_id=]
[--outpoint=] [--txid=] [--nout=]
[--channel_claim_id=] [--channel_name=]
[--claim_name=